Skip to content

Commit cd69c4f

Browse files
committed
Merge remote-tracking branch 'origin/main' into jacob/lstm
2 parents 3c09283 + f814127 commit cd69c4f

20 files changed

Lines changed: 2172 additions & 0 deletions

data/gcb/process_data.ipynb

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 11,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"import pandas as pd\n",
10+
"import regionmask\n",
11+
"import xarray as xr"
12+
]
13+
},
14+
{
15+
"cell_type": "code",
16+
"execution_count": 12,
17+
"metadata": {},
18+
"outputs": [],
19+
"source": [
20+
"def process_ds(path):\n",
21+
" ds = xr.open_zarr(path, consolidated=True)\n",
22+
"\n",
23+
" ds['ELUC'] = ds['ELUC'].shift(time=1)\n",
24+
" ds['ELUC_diff'] = ds['ELUC_diff'].shift(time=1)\n",
25+
" ds['time'] = ds.time - 1\n",
26+
"\n",
27+
" ds = ds.stack(latlon=('lat', 'lon'))\n",
28+
"\n",
29+
" mask = ds['ELUC_diff'].isnull().compute()\n",
30+
" ds = ds.where(~mask, drop=True)\n",
31+
"\n",
32+
" country_mask = regionmask.defined_regions.natural_earth_v5_0_0.countries_110.mask(ds)\n",
33+
" ds = ds.assign_coords(country=country_mask)\n",
34+
" \n",
35+
" return ds"
36+
]
37+
},
38+
{
39+
"cell_type": "code",
40+
"execution_count": 13,
41+
"metadata": {},
42+
"outputs": [],
43+
"source": [
44+
"def convert_to_dataframe(da):\n",
45+
" df = da.to_dataframe()\n",
46+
" df.index = df.index.set_names(['time', 'i_lat', 'i_lon'])\n",
47+
" df = df.reset_index()\n",
48+
" return df"
49+
]
50+
},
51+
{
52+
"cell_type": "code",
53+
"execution_count": 19,
54+
"metadata": {},
55+
"outputs": [],
56+
"source": [
57+
"PATH_TO_DATASET = \"../merged_aggregated_dataset_1850_2022.zarr.zip\"\n",
58+
"ds = process_ds(PATH_TO_DATASET)\n",
59+
"\n",
60+
"countries = regionmask.defined_regions.natural_earth_v5_0_0.countries_110\n",
61+
"countries_df = countries.to_dataframe()\n",
62+
"\n",
63+
"# Change this\n",
64+
"country_list = [\"GB\", \"BR\", \"CH\"]\n",
65+
"country_id_list = countries_df.index[countries_df['abbrevs'].isin(country_list)].tolist()\n",
66+
"\n",
67+
"da = ds.where(ds.country.isin(country_id_list), drop=True)\n",
68+
"df = convert_to_dataframe(da)\n",
69+
"path = \"\"\n",
70+
"for c in country_list:\n",
71+
" path += c + \"_\"\n",
72+
"path += \"eluc.csv\"\n",
73+
"df.to_csv(f\"processed/path.lower()\")"
74+
]
75+
}
76+
],
77+
"metadata": {
78+
"kernelspec": {
79+
"display_name": "leaf",
80+
"language": "python",
81+
"name": "python3"
82+
},
83+
"language_info": {
84+
"codemirror_mode": {
85+
"name": "ipython",
86+
"version": 3
87+
},
88+
"file_extension": ".py",
89+
"mimetype": "text/x-python",
90+
"name": "python",
91+
"nbconvert_exporter": "python",
92+
"pygments_lexer": "ipython3",
93+
"version": "3.10.4"
94+
},
95+
"orig_nbformat": 4
96+
},
97+
"nbformat": 4,
98+
"nbformat_minor": 2
99+
}

demo/Predictor.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import warnings
2+
import json
3+
4+
import pandas as pd
5+
import torch
6+
7+
from xgboost import XGBRegressor
8+
from unileaf_util.framework.transformers.data_encoder import DataEncoder
9+
10+
from constants import fields
11+
from constants import cao_mapping
12+
from constants import LAND_USE_COLS
13+
from constants import COLS_MAP
14+
from constants import DIFF_LAND_USE_COLS
15+
from constants import XGBOOST_FILE_PATH
16+
from constants import LSTM_CONFIG_PATH
17+
from constants import LSTM_FILE_PATH
18+
from constants import CONTEXT_COLUMNS
19+
from constants import ALL_DIFF_LAND_USE_COLS
20+
from constants import XGBOOST_FEATURES
21+
22+
# Silence xgboost warnings
23+
warnings.filterwarnings("ignore")
24+
25+
class Predictor:
26+
"""
27+
Wraps XGBoost model and DataEncoder.
28+
To be updated later to handle LSTM/other models.
29+
"""
30+
31+
def __init__(self):
32+
pass
33+
34+
def run_predictor(self, context: pd.DataFrame, prescribed: pd.DataFrame) -> float:
35+
"""
36+
Runs predictor using context and prescription.
37+
"""
38+
pass
39+
40+
41+
class XGBoostPredictor(Predictor):
42+
def __init__(self, model_path=XGBOOST_FILE_PATH):
43+
"""
44+
:param model_path: Path to XGBoost model file
45+
"""
46+
super().__init__()
47+
self.predictor_model = XGBRegressor()
48+
self.predictor_model.load_model(model_path)
49+
50+
self.encoder = DataEncoder(fields, cao_mapping)
51+
52+
53+
def run_predictor(self, context: pd.DataFrame, prescribed: pd.DataFrame) -> float:
54+
"""
55+
Runs predictor model.
56+
:param context: DataFrame of context.
57+
:param prescribed: DataFrame of prescribed land usage to be diffed.
58+
:return: Tuple of predicted ELUC and percentage of land use changed.
59+
"""
60+
61+
# TODO: Encoder deletes columns not in it. Manual add c4per
62+
encoded_sample_context_df = self.encoder.encode_as_df(context)
63+
64+
prescribed_actions_df = prescribed[LAND_USE_COLS].reset_index(drop=True) \
65+
- context[LAND_USE_COLS].reset_index(drop=True)
66+
prescribed_actions_df.rename(COLS_MAP, axis=1, inplace=True)
67+
68+
encoded_prescribed_actions_df = self.encoder.encode_as_df(prescribed_actions_df)
69+
70+
# TODO: Hacky
71+
encoded_prescribed_actions_df["primn_diff"] = 0
72+
encoded_prescribed_actions_df["primf_diff"] = 0
73+
encoded_prescribed_actions_df["c4per_diff"] = 0
74+
encoded_sample_context_df["c4per"] = 0
75+
76+
context_actions = pd.concat([encoded_sample_context_df, encoded_prescribed_actions_df], axis=1)
77+
78+
pred = self.predictor_model.predict(context_actions[XGBOOST_FEATURES])
79+
pred_df = pd.DataFrame(pred, columns=["ELUC"])
80+
# Decode output
81+
out_df = self.encoder.decode_as_df(pred_df)
82+
return out_df.iloc[0, 0]
83+
84+
85+
class LSTMPredictor(Predictor):
86+
87+
class LSTMModel(torch.nn.Module):
88+
def __init__(self, in_features, layers=1, hidden_dim=64, dropout=0):
89+
super().__init__()
90+
self.norm = torch.nn.LayerNorm(in_features)
91+
self.lstm = torch.nn.LSTM(in_features, hidden_dim, num_layers=layers, batch_first=True, dropout=dropout)
92+
self.ff = torch.nn.Linear(hidden_dim, 1)
93+
94+
def forward(self, x):
95+
x = self.norm(x)
96+
x, _ = self.lstm(x)
97+
x = self.ff(x[:,-1,:])
98+
x = torch.squeeze(x)
99+
return x
100+
101+
def __init__(self, config_path=LSTM_CONFIG_PATH, model_path=LSTM_FILE_PATH):
102+
"""
103+
:param model_path: Path to XGBoost model file
104+
"""
105+
super().__init__()
106+
config = json.load(open(LSTM_CONFIG_PATH))
107+
self.predictor_model = self.LSTMModel(in_features=len(CONTEXT_COLUMNS + ALL_DIFF_LAND_USE_COLS), **config)
108+
self.predictor_model.load_state_dict(torch.load(model_path))
109+
self.predictor_model.eval()
110+
111+
self.encoder = DataEncoder(fields, cao_mapping)
112+
113+
114+
def run_predictor(self, context: pd.DataFrame, prescribed: pd.DataFrame) -> float:
115+
"""
116+
Runs predictor model.
117+
:param context: DataFrame of context.
118+
:param prescribed: DataFrame of prescribed land usage to be diffed.
119+
:return: Tuple of predicted ELUC and percentage of land use changed.
120+
"""
121+
122+
context_df = context.reset_index(drop=True)
123+
encoded_context_df = self.encoder.encode_as_df(context_df)
124+
# TODO: This is yucky because we have to add primn and primf 0 diffs since our
125+
# prescriptor doesn't handle them.
126+
prescribed_actions_df = prescribed[LAND_USE_COLS].reset_index(drop=True) \
127+
- context_df[LAND_USE_COLS].iloc[[-1]].reset_index(drop=True)
128+
prescribed_actions_df.rename(COLS_MAP, inplace=True, axis=1)
129+
encoded_prescribed_actions_df = self.encoder.encode_as_df(prescribed_actions_df)
130+
131+
# TODO: @IMPORTANT WE DONT PRESCRIBE C4PER. Does this destroy our df?
132+
encoded_prescribed_actions_df["primn_diff"] = 0
133+
encoded_prescribed_actions_df["primf_diff"] = 0
134+
encoded_prescribed_actions_df["c4per_diff"] = 0
135+
encoded_context_df["c4per"] = 0
136+
encoded_context_df["primn_diff"] = 0
137+
encoded_context_df["primf_diff"] = 0
138+
encoded_context_df["c4per_diff"] = 0
139+
140+
encoded_context_df.loc[encoded_context_df.index[-1], DIFF_LAND_USE_COLS] = encoded_prescribed_actions_df.loc[encoded_prescribed_actions_df.index[0], DIFF_LAND_USE_COLS]
141+
df_np = encoded_context_df[CONTEXT_COLUMNS + ALL_DIFF_LAND_USE_COLS].to_numpy()
142+
inp = torch.from_numpy(df_np)
143+
inp = inp.type(torch.FloatTensor)
144+
inp = inp.unsqueeze(0)
145+
146+
out = self.predictor_model(inp)
147+
148+
return out.item()

demo/Prescriptor.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import os
2+
from typing import List
3+
import pandas as pd
4+
import numpy as np
5+
from keras.models import load_model
6+
7+
from unileaf_util.framework.transformers.data_encoder import DataEncoder
8+
from constants import fields
9+
from constants import cao_mapping
10+
from constants import PRESCRIPTOR_OUTPUT_COLS
11+
12+
class Prescriptor:
13+
"""
14+
Wrapper for Keras prescriptor and encoder.
15+
"""
16+
17+
def __init__(self, prescriptor_id: str):
18+
"""
19+
:param prescriptor_id: ID of Keras prescriptor to load.
20+
"""
21+
prescriptor_model_filename = os.path.join("prescriptors",
22+
prescriptor_id + '.h5')
23+
24+
print(f'Loading prescriptor model: {prescriptor_model_filename}')
25+
self.prescriptor_model = load_model(prescriptor_model_filename, compile=False)
26+
27+
self.encoder = DataEncoder(fields, cao_mapping)
28+
29+
30+
def _is_single_action_prescriptor(self, actions):
31+
"""
32+
Checks how many Actions have been defined in the Context, Actions, Outcomes mapping.
33+
:return: True if only 1 action is defined, False otherwise
34+
"""
35+
return len(actions) == 1
36+
37+
def _is_scalar(self, prescribed_action):
38+
"""
39+
Checks if the prescribed action contains a single value, i.e. a scalar, or an array.
40+
A prescribed action contains a single value if it has been prescribed for a single context sample
41+
:param prescribed_action: a scalar or an array
42+
:return: True if the prescribed action contains a scalar, False otherwise.
43+
"""
44+
return prescribed_action.shape[0] == 1 and prescribed_action.shape[1] == 1
45+
46+
def _convert_to_nn_input(self, context_df: pd.DataFrame) -> List[np.ndarray]:
47+
"""
48+
Converts a context DataFrame to a list of numpy arrays a neural network can ingest
49+
:param context_df: a DataFrame containing inputs for a neural network. Number of inputs and size must match
50+
:return: a list of numpy ndarray, on ndarray per neural network input
51+
"""
52+
# The NN expects a list of i inputs by s samples (e.g. 9 x 299).
53+
# So convert the data frame to a numpy array (gives shape 299 x 9), transpose it (gives 9 x 299)
54+
# and convert to list(list of 9 arrays of 299)
55+
context_as_nn_input = list(context_df.to_numpy().transpose())
56+
# Convert each column's list of 1D array to a 2D array
57+
context_as_nn_input = [np.stack(context_as_nn_input[i], axis=0) for i in
58+
range(len(context_as_nn_input))]
59+
return context_as_nn_input
60+
61+
def __prescribe_from_model(self, context_df: pd.DataFrame) -> pd.DataFrame:
62+
"""
63+
Generates prescriptions using the passed neural network candidate and context
64+
::param context_df: a DataFrame containing the context to prescribe for,
65+
:return: a pandas DataFrame of action name to action value or list of action values
66+
"""
67+
action_list = ['recommended_land_use']
68+
69+
# Convert the input df
70+
context_as_nn_input = self._convert_to_nn_input(context_df)
71+
row_index = context_df.index
72+
73+
# Get the prescrib?ed actions
74+
prescribed_actions = self.prescriptor_model.predict(context_as_nn_input)
75+
actions = {}
76+
77+
if self._is_single_action_prescriptor(action_list):
78+
# Put the single action in an array to process it like multiple actions
79+
prescribed_actions = [prescribed_actions]
80+
81+
for idx, action_col in enumerate(action_list):
82+
if self._is_scalar(prescribed_actions[idx]):
83+
# We have a single row and this action is numerical. Convert it to a scalar.
84+
actions[action_col] = prescribed_actions[idx].item()
85+
else:
86+
actions[action_col] = prescribed_actions[idx].tolist()
87+
88+
# Convert the prescribed actions to a DataFrame
89+
prescribed_actions_df = pd.DataFrame(actions,
90+
columns=action_list,
91+
index=row_index)
92+
return prescribed_actions_df
93+
94+
95+
def run_prescriptor(self, sample_context_df):
96+
"""
97+
Runs prescriptor on context. Then re-scales prescribed land
98+
use to match how much was used in the sample.
99+
100+
:param sample_context_df: a DataFrame containing the context
101+
:return: DataFrame of prescribed land use
102+
"""
103+
encoded_sample_context_df = self.encoder.encode_as_df(sample_context_df)
104+
prescribed_actions_df = self.__prescribe_from_model(encoded_sample_context_df)
105+
reco_land_use_df = pd.DataFrame(prescribed_actions_df.recommended_land_use.tolist(),
106+
columns=PRESCRIPTOR_OUTPUT_COLS)
107+
108+
# Re-scales our prescribed land to match the amount of land used in the sample
109+
used = sum(sample_context_df[PRESCRIPTOR_OUTPUT_COLS].iloc[0].tolist())
110+
for col in PRESCRIPTOR_OUTPUT_COLS:
111+
reco_land_use_df[col] *= used
112+
113+
# Assuming there's no primary land left in this cell
114+
# TODO: not correct. Need to account for primf and primn, that can't increase
115+
# (no way to return to primary forest)
116+
prescribed_land_use_pct = reco_land_use_df.iloc[0][PRESCRIPTOR_OUTPUT_COLS].sum() * 100
117+
print(f"Presribed land usage: {prescribed_land_use_pct:.2f}% of land")
118+
119+
return reco_land_use_df[PRESCRIPTOR_OUTPUT_COLS]

demo/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# MVP Climate Change Demo
2+
3+
This is a demo of the MVP Climate Change app. It allows users to select a location and year from a map of the UK, Switzerland, or Brazil, see its land use composition, and prescribe or manually make changes to it and see the predicted ELUC (Emissions from Land Use Change). It is a simple Dash app.
4+
5+
## Dependencies:
6+
7+
The demo relies on the `unileaf_util` package. This has to be manually installed from the `.whl` file.
8+
The requirements.txt is set up for M1 macs. Your installations of tensorflow and pytorch may differ.
9+
10+
## Running the app:
11+
12+
To run the app use: ``python app.py``
13+
14+
## Predictors:
15+
16+
Saved predictors can be found in `predictors/`. The XGBoost predictor's weights are stored in a `.json` file whereas the LSTM predictor's weights are stored in a `.pt` file and its configuration is saved in the corresponding `.json` file.
17+
18+
## Prescriptors:
19+
20+
Prescriptors are stored in `prescriptors/` as well as the pareto front image and a CSV of pareto info from training the prescriptors.

0 commit comments

Comments
 (0)