deepsensor 0.1.11__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deepsensor-0.1.11 → deepsensor-0.2.1}/PKG-INFO +19 -4
- {deepsensor-0.1.11 → deepsensor-0.2.1}/README.md +3 -3
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/active_learning/algorithms.py +2 -2
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/data/loader.py +197 -52
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/data/processor.py +119 -73
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/model/convnp.py +58 -3
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/model/model.py +1 -1
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/model/nps.py +8 -6
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/PKG-INFO +19 -4
- {deepsensor-0.1.11 → deepsensor-0.2.1}/setup.cfg +1 -1
- {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_data_processor.py +22 -13
- {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_model.py +53 -2
- {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_plotting.py +1 -1
- {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_task_loader.py +6 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/__init__.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/active_learning/__init__.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/active_learning/acquisition_fns.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/config.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/data/__init__.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/data/task.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/data/utils.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/errors.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/model/__init__.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/model/defaults.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/plot.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/py.typed +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/tensorflow/__init__.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/torch/__init__.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/train/__init__.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/train/train.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/SOURCES.txt +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/dependency_links.txt +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/not-zip-safe +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/requires.txt +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/top_level.txt +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/pyproject.toml +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/setup.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/__init__.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_active_learning.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_training.py +0 -0
- {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: deepsensor
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A Python package for modelling xarray and pandas data with neural processes.
|
|
5
5
|
Home-page: https://github.com/tom-andersson/deepsensor
|
|
6
6
|
Author: Tom R. Andersson
|
|
@@ -11,7 +11,22 @@ Platform: linux
|
|
|
11
11
|
Platform: osx
|
|
12
12
|
Requires-Python: >=3.8
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: neuralprocesses
|
|
15
|
+
Requires-Dist: backends-matrix
|
|
16
|
+
Requires-Dist: backends
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: pandas
|
|
19
|
+
Requires-Dist: xarray
|
|
20
|
+
Requires-Dist: dask
|
|
21
|
+
Requires-Dist: distributed
|
|
22
|
+
Requires-Dist: pyshp
|
|
23
|
+
Requires-Dist: tqdm
|
|
24
|
+
Requires-Dist: pooch
|
|
14
25
|
Provides-Extra: testing
|
|
26
|
+
Requires-Dist: pytest; extra == "testing"
|
|
27
|
+
Requires-Dist: pytest-cov; extra == "testing"
|
|
28
|
+
Requires-Dist: mypy; extra == "testing"
|
|
29
|
+
Requires-Dist: tox; extra == "testing"
|
|
15
30
|
|
|
16
31
|
[//]: # ()
|
|
17
32
|
<ul style="text-align: center;">
|
|
@@ -26,7 +41,7 @@ data with neural processes</p>
|
|
|
26
41
|
|
|
27
42
|
-----------
|
|
28
43
|
|
|
29
|
-
[](https://github.com/tom-andersson/deepsensor/releases)
|
|
30
45
|

|
|
31
46
|
[](https://coveralls.io/github/tom-andersson/deepsensor?branch=main)
|
|
32
47
|
[](https://github.com/psf/black)
|
|
@@ -97,7 +112,7 @@ import numpy as np
|
|
|
97
112
|
ds_raw = xr.tutorial.open_dataset("air_temperature")
|
|
98
113
|
|
|
99
114
|
# Normalise data
|
|
100
|
-
data_processor = DataProcessor(x1_name="lat",
|
|
115
|
+
data_processor = DataProcessor(x1_name="lat", x2_name="lon")
|
|
101
116
|
ds = data_processor(ds_raw)
|
|
102
117
|
|
|
103
118
|
# Set up task loader
|
|
@@ -116,7 +131,7 @@ for date in pd.date_range("2013-01-01", "2014-11-30")[::7]:
|
|
|
116
131
|
# Train model
|
|
117
132
|
trainer = Trainer(model, lr=5e-5)
|
|
118
133
|
for epoch in range(10):
|
|
119
|
-
trainer(
|
|
134
|
+
trainer(train_tasks, progress_bar=True)
|
|
120
135
|
|
|
121
136
|
# Predict on new task with 10% of context data and a dense grid of target points
|
|
122
137
|
test_task = task_loader("2014-12-31", 0.1)
|
|
@@ -11,7 +11,7 @@ data with neural processes</p>
|
|
|
11
11
|
|
|
12
12
|
-----------
|
|
13
13
|
|
|
14
|
-
[](https://github.com/tom-andersson/deepsensor/releases)
|
|
15
15
|

|
|
16
16
|
[](https://coveralls.io/github/tom-andersson/deepsensor?branch=main)
|
|
17
17
|
[](https://github.com/psf/black)
|
|
@@ -82,7 +82,7 @@ import numpy as np
|
|
|
82
82
|
ds_raw = xr.tutorial.open_dataset("air_temperature")
|
|
83
83
|
|
|
84
84
|
# Normalise data
|
|
85
|
-
data_processor = DataProcessor(x1_name="lat",
|
|
85
|
+
data_processor = DataProcessor(x1_name="lat", x2_name="lon")
|
|
86
86
|
ds = data_processor(ds_raw)
|
|
87
87
|
|
|
88
88
|
# Set up task loader
|
|
@@ -101,7 +101,7 @@ for date in pd.date_range("2013-01-01", "2014-11-30")[::7]:
|
|
|
101
101
|
# Train model
|
|
102
102
|
trainer = Trainer(model, lr=5e-5)
|
|
103
103
|
for epoch in range(10):
|
|
104
|
-
trainer(
|
|
104
|
+
trainer(train_tasks, progress_bar=True)
|
|
105
105
|
|
|
106
106
|
# Predict on new task with 10% of context data and a dense grid of target points
|
|
107
107
|
test_task = task_loader("2014-12-31", 0.1)
|
|
@@ -72,8 +72,8 @@ class GreedyAlgorithm:
|
|
|
72
72
|
self.X_s_mask = X_s_mask
|
|
73
73
|
self.X_t_mask = X_t_mask
|
|
74
74
|
|
|
75
|
-
self.x1_name = self.model.data_processor.
|
|
76
|
-
self.x2_name = self.model.data_processor.
|
|
75
|
+
self.x1_name = self.model.data_processor.config["coords"]["x1"]["name"]
|
|
76
|
+
self.x2_name = self.model.data_processor.config["coords"]["x2"]["name"]
|
|
77
77
|
|
|
78
78
|
# Normalised search and target coordinates
|
|
79
79
|
if not X_normalised:
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
from deepsensor.data.task import Task, flatten_X
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import copy
|
|
6
|
+
|
|
3
7
|
import numpy as np
|
|
4
8
|
import xarray as xr
|
|
5
9
|
import pandas as pd
|
|
@@ -10,25 +14,27 @@ from deepsensor.errors import InvalidSamplingStrategyError
|
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
class TaskLoader:
|
|
17
|
+
config_fname = "task_loader_config.json"
|
|
18
|
+
|
|
13
19
|
def __init__(
|
|
14
20
|
self,
|
|
21
|
+
task_loader_ID: str | None = None,
|
|
15
22
|
context: Union[
|
|
16
23
|
xr.DataArray,
|
|
17
24
|
xr.Dataset,
|
|
18
25
|
pd.DataFrame,
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
str,
|
|
27
|
+
List[Union[xr.DataArray, xr.Dataset, pd.DataFrame, str]],
|
|
28
|
+
] = None,
|
|
21
29
|
target: Union[
|
|
22
30
|
xr.DataArray,
|
|
23
31
|
xr.Dataset,
|
|
24
32
|
pd.DataFrame,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
aux_at_contexts: Union[xr.DataArray, xr.Dataset] = None,
|
|
28
|
-
aux_at_targets: Union[
|
|
29
|
-
xr.DataArray,
|
|
30
|
-
xr.Dataset,
|
|
33
|
+
str,
|
|
34
|
+
List[Union[xr.DataArray, xr.Dataset, pd.DataFrame, str]],
|
|
31
35
|
] = None,
|
|
36
|
+
aux_at_contexts: Union[xr.DataArray, xr.Dataset, str] = None,
|
|
37
|
+
aux_at_targets: Union[xr.DataArray, xr.Dataset, str] = None,
|
|
32
38
|
links: Union[Tuple, List[Tuple[int, int]], None] = None,
|
|
33
39
|
context_delta_t: Union[int, List[int]] = 0,
|
|
34
40
|
target_delta_t: Union[int, List[int]] = 0,
|
|
@@ -39,7 +45,15 @@ class TaskLoader:
|
|
|
39
45
|
) -> None:
|
|
40
46
|
"""Initialise a TaskLoader object
|
|
41
47
|
|
|
48
|
+
The behaviour is the following:
|
|
49
|
+
- If all data passed as paths, load the data and overwrite the paths with the loaded data
|
|
50
|
+
- Either all data is passed as paths, or all data is passed as loaded data (else ValueError)
|
|
51
|
+
- If all data passed as paths, the TaskLoader can be saved with the `save` method (using config)
|
|
52
|
+
|
|
42
53
|
Args:
|
|
54
|
+
task_loader_ID: If loading a TaskLoader from a config file, this is the folder the
|
|
55
|
+
TaskLoader was saved in (using `.save`). If this argument is passed, all other
|
|
56
|
+
arguments are ignored.
|
|
43
57
|
context: Context data. Can be a single xr.DataArray, xr.Dataset or pd.DataFrame,
|
|
44
58
|
or a list/tuple of these.
|
|
45
59
|
target: Target data. Can be a single xr.DataArray, xr.Dataset or pd.DataFrame,
|
|
@@ -68,46 +82,64 @@ class TaskLoader:
|
|
|
68
82
|
dtype: Data type of the data. Used to cast the data to the specified dtype.
|
|
69
83
|
Default: 'float32'.
|
|
70
84
|
"""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
if task_loader_ID is not None:
|
|
86
|
+
self.task_loader_ID = task_loader_ID
|
|
87
|
+
# Load TaskLoader from config file
|
|
88
|
+
fpath = os.path.join(task_loader_ID, self.config_fname)
|
|
89
|
+
with open(fpath, "r") as f:
|
|
90
|
+
self.config = json.load(f)
|
|
91
|
+
|
|
92
|
+
self.context = self.config["context"]
|
|
93
|
+
self.target = self.config["target"]
|
|
94
|
+
self.aux_at_contexts = self.config["aux_at_contexts"]
|
|
95
|
+
self.aux_at_targets = self.config["aux_at_targets"]
|
|
96
|
+
self.time_freq = self.config["time_freq"]
|
|
97
|
+
self.xarray_interp_method = self.config["xarray_interp_method"]
|
|
98
|
+
self.discrete_xarray_sampling = self.config["discrete_xarray_sampling"]
|
|
99
|
+
self.dtype = self.config["dtype"]
|
|
100
|
+
else:
|
|
101
|
+
self.context = context
|
|
102
|
+
self.target = target
|
|
103
|
+
self.aux_at_contexts = aux_at_contexts
|
|
104
|
+
self.aux_at_targets = aux_at_targets
|
|
105
|
+
self.time_freq = time_freq
|
|
106
|
+
self.xarray_interp_method = xarray_interp_method
|
|
107
|
+
self.discrete_xarray_sampling = discrete_xarray_sampling
|
|
108
|
+
self.dtype = dtype
|
|
109
|
+
|
|
110
|
+
if not isinstance(self.context, (tuple, list)):
|
|
111
|
+
self.context = (self.context,)
|
|
112
|
+
if not isinstance(self.target, (tuple, list)):
|
|
113
|
+
self.target = (self.target,)
|
|
114
|
+
|
|
115
|
+
if isinstance(context_delta_t, int):
|
|
116
|
+
context_delta_t = (context_delta_t,) * len(self.context)
|
|
117
|
+
else:
|
|
118
|
+
assert len(context_delta_t) == len(self.context)
|
|
119
|
+
if isinstance(target_delta_t, int):
|
|
120
|
+
target_delta_t = (target_delta_t,) * len(self.target)
|
|
121
|
+
else:
|
|
122
|
+
assert len(target_delta_t) == len(self.target)
|
|
123
|
+
self.context_delta_t = context_delta_t
|
|
124
|
+
self.target_delta_t = target_delta_t
|
|
125
|
+
|
|
126
|
+
all_paths = self._check_if_all_data_passed_as_paths()
|
|
127
|
+
if all_paths:
|
|
128
|
+
self._set_config()
|
|
129
|
+
self._load_data_from_paths()
|
|
75
130
|
|
|
76
131
|
if aux_at_contexts is not None:
|
|
77
132
|
self._check_offgrid_aux(self._check_offgrid_aux(aux_at_contexts))
|
|
78
133
|
if aux_at_targets is not None:
|
|
79
134
|
self._check_offgrid_aux(aux_at_targets)
|
|
80
135
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
(
|
|
86
|
-
context,
|
|
87
|
-
target,
|
|
88
|
-
aux_at_contexts,
|
|
89
|
-
aux_at_targets,
|
|
90
|
-
) = self._cast_context_and_target_to_dtype(
|
|
91
|
-
context, target, aux_at_contexts, aux_at_targets
|
|
92
|
-
)
|
|
93
|
-
self.context = context
|
|
94
|
-
self.target = target
|
|
95
|
-
self.aux_at_contexts = aux_at_contexts
|
|
96
|
-
self.aux_at_targets = aux_at_targets
|
|
136
|
+
self.context = self._cast_to_dtype(self.context)
|
|
137
|
+
self.target = self._cast_to_dtype(self.target)
|
|
138
|
+
self.aux_at_contexts = self._cast_to_dtype(self.aux_at_contexts)
|
|
139
|
+
self.aux_at_targets = self._cast_to_dtype(self.aux_at_targets)
|
|
97
140
|
|
|
98
141
|
self.links = self.check_links(links)
|
|
99
142
|
|
|
100
|
-
if isinstance(context_delta_t, int):
|
|
101
|
-
context_delta_t = (context_delta_t,) * len(context)
|
|
102
|
-
else:
|
|
103
|
-
assert len(context_delta_t) == len(context)
|
|
104
|
-
if isinstance(target_delta_t, int):
|
|
105
|
-
target_delta_t = (target_delta_t,) * len(target)
|
|
106
|
-
else:
|
|
107
|
-
assert len(target_delta_t) == len(target)
|
|
108
|
-
self.context_delta_t = context_delta_t
|
|
109
|
-
self.target_delta_t = target_delta_t
|
|
110
|
-
|
|
111
143
|
(
|
|
112
144
|
self.context_dims,
|
|
113
145
|
self.target_dims,
|
|
@@ -121,15 +153,128 @@ class TaskLoader:
|
|
|
121
153
|
self.aux_at_target_var_IDs,
|
|
122
154
|
) = self.infer_context_and_target_var_IDs()
|
|
123
155
|
|
|
124
|
-
def
|
|
156
|
+
def _set_config(self):
|
|
157
|
+
"""Instantiate a config dictionary for the TaskLoader object"""
|
|
158
|
+
# Take deepcopy to avoid modifying the original config
|
|
159
|
+
self.config = copy.deepcopy(
|
|
160
|
+
dict(
|
|
161
|
+
context=self.context,
|
|
162
|
+
target=self.target,
|
|
163
|
+
aux_at_contexts=self.aux_at_contexts,
|
|
164
|
+
aux_at_targets=self.aux_at_targets,
|
|
165
|
+
context_delta_t=self.context_delta_t,
|
|
166
|
+
target_delta_t=self.target_delta_t,
|
|
167
|
+
time_freq=self.time_freq,
|
|
168
|
+
xarray_interp_method=self.xarray_interp_method,
|
|
169
|
+
discrete_xarray_sampling=self.discrete_xarray_sampling,
|
|
170
|
+
dtype=self.dtype,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def _check_if_all_data_passed_as_paths(self) -> bool:
|
|
175
|
+
"""If all data passed as paths, save paths to config and return True."""
|
|
176
|
+
|
|
177
|
+
def _check_if_strings(x, mode="all"):
|
|
178
|
+
if x is None:
|
|
179
|
+
return None
|
|
180
|
+
elif isinstance(x, (tuple, list)):
|
|
181
|
+
if mode == "all":
|
|
182
|
+
return all([isinstance(x_i, str) for x_i in x])
|
|
183
|
+
elif mode == "any":
|
|
184
|
+
return any([isinstance(x_i, str) for x_i in x])
|
|
185
|
+
else:
|
|
186
|
+
return isinstance(x, str)
|
|
187
|
+
|
|
188
|
+
all_paths = all(
|
|
189
|
+
filter(
|
|
190
|
+
lambda x: x is not None,
|
|
191
|
+
[
|
|
192
|
+
_check_if_strings(self.context),
|
|
193
|
+
_check_if_strings(self.target),
|
|
194
|
+
_check_if_strings(self.aux_at_contexts),
|
|
195
|
+
_check_if_strings(self.aux_at_targets),
|
|
196
|
+
],
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
self._is_saveable = all_paths
|
|
200
|
+
|
|
201
|
+
any_paths = any(
|
|
202
|
+
filter(
|
|
203
|
+
lambda x: x is not None,
|
|
204
|
+
[
|
|
205
|
+
_check_if_strings(self.context, mode="any"),
|
|
206
|
+
_check_if_strings(self.target, mode="any"),
|
|
207
|
+
_check_if_strings(self.aux_at_contexts, mode="any"),
|
|
208
|
+
_check_if_strings(self.aux_at_targets, mode="any"),
|
|
209
|
+
],
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
if any_paths and not all_paths:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
"Data must be passed either all as paths or all as xarray/pandas objects (not a mix)."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return all_paths
|
|
218
|
+
|
|
219
|
+
def _load_data_from_paths(self):
|
|
220
|
+
"""Load data from paths and overwrite paths with loaded data."""
|
|
221
|
+
|
|
222
|
+
def _load_pandas_or_xarray(path):
|
|
223
|
+
# Need to be careful about this. We need to ensure data gets into the right form
|
|
224
|
+
# for TaskLoader.
|
|
225
|
+
if path is None:
|
|
226
|
+
return None
|
|
227
|
+
elif path.endswith(".nc"):
|
|
228
|
+
return xr.open_dataset(path)
|
|
229
|
+
elif path.endswith(".csv"):
|
|
230
|
+
df = pd.read_csv(path)
|
|
231
|
+
if "time" in df.columns:
|
|
232
|
+
df["time"] = pd.to_datetime(df["time"])
|
|
233
|
+
df = df.set_index(["time", "x1", "x2"]).sort_index()
|
|
234
|
+
else:
|
|
235
|
+
df = df.set_index(["x1", "x2"]).sort_index()
|
|
236
|
+
return df
|
|
237
|
+
else:
|
|
238
|
+
raise ValueError(f"Unknown file extension for {path}")
|
|
239
|
+
|
|
240
|
+
def _load_data(data):
|
|
241
|
+
if isinstance(data, (tuple, list)):
|
|
242
|
+
data = tuple([_load_pandas_or_xarray(data_i) for data_i in data])
|
|
243
|
+
else:
|
|
244
|
+
data = _load_pandas_or_xarray(data)
|
|
245
|
+
return data
|
|
246
|
+
|
|
247
|
+
self.context = _load_data(self.context)
|
|
248
|
+
self.target = _load_data(self.target)
|
|
249
|
+
self.aux_at_contexts = _load_data(self.aux_at_contexts)
|
|
250
|
+
self.aux_at_targets = _load_data(self.aux_at_targets)
|
|
251
|
+
|
|
252
|
+
def save(self, folder: str):
|
|
253
|
+
"""Save TaskLoader config to JSON in `folder`"""
|
|
254
|
+
if not self._is_saveable:
|
|
255
|
+
raise ValueError(
|
|
256
|
+
"TaskLoader cannot be saved because not all data was passed as paths."
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
os.makedirs(folder, exist_ok=True)
|
|
260
|
+
fpath = os.path.join(folder, self.config_fname)
|
|
261
|
+
with open(fpath, "w") as f:
|
|
262
|
+
json.dump(self.config, f, indent=4, sort_keys=False)
|
|
263
|
+
|
|
264
|
+
def _cast_to_dtype(
|
|
125
265
|
self,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
266
|
+
var: Union[
|
|
267
|
+
xr.DataArray,
|
|
268
|
+
xr.Dataset,
|
|
269
|
+
pd.DataFrame,
|
|
270
|
+
List[Union[xr.DataArray, xr.Dataset, pd.DataFrame, str]],
|
|
271
|
+
],
|
|
130
272
|
) -> (List, List):
|
|
131
273
|
"""Cast context and target data to the default dtype.
|
|
132
274
|
|
|
275
|
+
TODO unit test this by passing in a variety of data types and checking that they are
|
|
276
|
+
cast correctly.
|
|
277
|
+
|
|
133
278
|
Returns
|
|
134
279
|
-------
|
|
135
280
|
context : tuple. Tuple of context data with specified dtype.
|
|
@@ -148,19 +293,19 @@ class TaskLoader:
|
|
|
148
293
|
elif isinstance(var, (pd.DataFrame, pd.Series)):
|
|
149
294
|
var = var.astype(self.dtype)
|
|
150
295
|
# Note: Numeric pandas indexes are always cast to float64, so we have to cast
|
|
151
|
-
#
|
|
296
|
+
# x1/x2 coord dtypes during task sampling
|
|
152
297
|
else:
|
|
153
298
|
raise ValueError(f"Unknown type {type(var)} for context set {var}")
|
|
154
299
|
return var
|
|
155
300
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
301
|
+
if var is None:
|
|
302
|
+
return var
|
|
303
|
+
elif isinstance(var, (tuple, list)):
|
|
304
|
+
var = tuple([cast_to_dtype(var_i) for var_i in var])
|
|
305
|
+
else:
|
|
306
|
+
var = cast_to_dtype(var)
|
|
162
307
|
|
|
163
|
-
return
|
|
308
|
+
return var
|
|
164
309
|
|
|
165
310
|
def _check_offgrid_aux(self, offgrid_aux):
|
|
166
311
|
if offgrid_aux is not None and "time" in offgrid_aux.dims:
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
2
4
|
|
|
3
5
|
import xarray as xr
|
|
4
6
|
import pandas as pd
|
|
@@ -7,50 +9,71 @@ import pprint
|
|
|
7
9
|
|
|
8
10
|
from copy import deepcopy
|
|
9
11
|
|
|
12
|
+
from plum import dispatch
|
|
10
13
|
from typing import Union
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class DataProcessor:
|
|
14
17
|
"""Normalise xarray and pandas data for use in deepsensor models"""
|
|
15
18
|
|
|
19
|
+
config_fname = "data_processor_config.json"
|
|
20
|
+
|
|
16
21
|
def __init__(
|
|
17
22
|
self,
|
|
18
|
-
|
|
23
|
+
folder: str | None = None,
|
|
19
24
|
time_name: str = "time",
|
|
20
25
|
x1_name: str = "x1",
|
|
21
26
|
x2_name: str = "x2",
|
|
22
|
-
x1_map: tuple =
|
|
23
|
-
x2_map: tuple =
|
|
27
|
+
x1_map: tuple | None = None,
|
|
28
|
+
x2_map: tuple | None = None,
|
|
24
29
|
deepcopy: bool = True,
|
|
25
30
|
verbose: bool = False,
|
|
26
|
-
)
|
|
31
|
+
):
|
|
27
32
|
"""Initialise DataProcessor
|
|
28
33
|
|
|
29
34
|
Args:
|
|
30
|
-
|
|
35
|
+
folder (str, optional): Folder to load normalisation params from. Defaults to None.
|
|
31
36
|
x1_name (str, optional): Name of first spatial coord (e.g. "lat"). Defaults to "x1".
|
|
32
37
|
x2_name (str, optional): Name of second spatial coord (e.g. "lon"). Defaults to "x2".
|
|
33
38
|
x1_map (tuple, optional): 2-tuple of raw x1 coords to linearly map to (0, 1), respectively.
|
|
34
|
-
|
|
39
|
+
If None, the mapping inferred from first data object passed to DataProcessor.
|
|
40
|
+
Defaults to None.
|
|
35
41
|
x2_map (tuple, optional): 2-tuple of raw x2 coords to linearly map to (0, 1), respectively.
|
|
36
|
-
|
|
42
|
+
If None, the mapping inferred from first data object passed to DataProcessor.
|
|
43
|
+
Defaults to None.
|
|
37
44
|
deepcopy (bool, optional): Whether to make a deepcopy of raw data to ensure it is
|
|
38
|
-
not changed by reference when normalising. Defaults to True.
|
|
45
|
+
not changed by reference when normalising. Defaults to True. TODO - test if necessary.
|
|
39
46
|
verbose (bool, optional): Whether to print verbose output. Defaults to False.
|
|
40
47
|
"""
|
|
41
|
-
|
|
48
|
+
if folder is not None:
|
|
49
|
+
fpath = os.path.join(folder, self.config_fname)
|
|
50
|
+
if not os.path.exists(fpath):
|
|
51
|
+
raise FileNotFoundError(
|
|
52
|
+
f"Could not find DataProcessor config file {fpath}"
|
|
53
|
+
)
|
|
54
|
+
with open(fpath, "r") as f:
|
|
55
|
+
self.config = json.load(f)
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
self.x1_none = self.config["coords"]["x1"]["map"] is None
|
|
58
|
+
self.x2_none = self.config["coords"]["x2"]["map"] is None
|
|
59
|
+
else:
|
|
60
|
+
self.config = {}
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
self.x1_none = x1_map is None
|
|
63
|
+
self.x2_none = x2_map is None
|
|
64
|
+
if (self.x1_none and not self.x2_none) or (
|
|
65
|
+
not self.x1_none and self.x2_none
|
|
66
|
+
):
|
|
67
|
+
raise ValueError("Must provide both x1_map and x2_map, or neither.")
|
|
68
|
+
elif not self.x1_none and not self.x2_none:
|
|
69
|
+
x1_map, x2_map = self._validate_coord_mappings(x1_map, x2_map)
|
|
47
70
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
if "coords" not in self.config:
|
|
72
|
+
# Add coordinate normalisation info to config
|
|
73
|
+
self.set_coord_params(time_name, x1_name, x1_map, x2_name, x2_map)
|
|
51
74
|
|
|
52
75
|
self.raw_spatial_coord_names = [
|
|
53
|
-
self.
|
|
76
|
+
self.config["coords"][coord]["name"] for coord in ["x1", "x2"]
|
|
54
77
|
]
|
|
55
78
|
|
|
56
79
|
self.deepcopy = deepcopy
|
|
@@ -59,6 +82,13 @@ class DataProcessor:
|
|
|
59
82
|
# List of valid normalisation method names
|
|
60
83
|
self.valid_methods = ["mean_std", "min_max"]
|
|
61
84
|
|
|
85
|
+
def save(self, folder: str):
|
|
86
|
+
"""Save DataProcessor config to JSON in `folder`"""
|
|
87
|
+
os.makedirs(folder, exist_ok=True)
|
|
88
|
+
fpath = os.path.join(folder, self.config_fname)
|
|
89
|
+
with open(fpath, "w") as f:
|
|
90
|
+
json.dump(self.config, f, indent=4, sort_keys=False)
|
|
91
|
+
|
|
62
92
|
def _validate_coord_mappings(self, x1_map, x2_map):
|
|
63
93
|
"""Ensure the maps are valid and of appropriate types."""
|
|
64
94
|
try:
|
|
@@ -84,8 +114,7 @@ class DataProcessor:
|
|
|
84
114
|
def _validate_xr(self, data: Union[xr.DataArray, xr.Dataset]):
|
|
85
115
|
def _validate_da(da: xr.DataArray):
|
|
86
116
|
coord_names = [
|
|
87
|
-
self.
|
|
88
|
-
for coord in ["time", "x1", "x2"]
|
|
117
|
+
self.config["coords"][coord]["name"] for coord in ["time", "x1", "x2"]
|
|
89
118
|
]
|
|
90
119
|
if coord_names[0] not in da.dims:
|
|
91
120
|
# We don't have a time dimension.
|
|
@@ -104,7 +133,7 @@ class DataProcessor:
|
|
|
104
133
|
|
|
105
134
|
def _validate_pandas(self, df: Union[pd.DataFrame, pd.Series]):
|
|
106
135
|
coord_names = [
|
|
107
|
-
self.
|
|
136
|
+
self.config["coords"][coord]["name"] for coord in ["time", "x1", "x2"]
|
|
108
137
|
]
|
|
109
138
|
|
|
110
139
|
if coord_names[0] not in df.index.names:
|
|
@@ -122,7 +151,7 @@ class DataProcessor:
|
|
|
122
151
|
|
|
123
152
|
def __str__(self):
|
|
124
153
|
s = "DataProcessor with normalisation params:\n"
|
|
125
|
-
s += pprint.pformat(self.
|
|
154
|
+
s += pprint.pformat(self.config)
|
|
126
155
|
return s
|
|
127
156
|
|
|
128
157
|
@classmethod
|
|
@@ -136,42 +165,43 @@ class DataProcessor:
|
|
|
136
165
|
|
|
137
166
|
def set_coord_params(self, time_name, x1_name, x1_map, x2_name, x2_map):
|
|
138
167
|
"""Set coordinate normalisation params"""
|
|
139
|
-
self.
|
|
140
|
-
self.
|
|
141
|
-
self.
|
|
142
|
-
self.
|
|
143
|
-
self.
|
|
144
|
-
self.
|
|
145
|
-
self.
|
|
146
|
-
self.
|
|
168
|
+
self.config["coords"] = {}
|
|
169
|
+
self.config["coords"]["time"] = {"name": time_name}
|
|
170
|
+
self.config["coords"]["x1"] = {}
|
|
171
|
+
self.config["coords"]["x2"] = {}
|
|
172
|
+
self.config["coords"]["x1"]["name"] = x1_name
|
|
173
|
+
self.config["coords"]["x1"]["map"] = x1_map
|
|
174
|
+
self.config["coords"]["x2"]["name"] = x2_name
|
|
175
|
+
self.config["coords"]["x2"]["map"] = x2_map
|
|
147
176
|
|
|
148
177
|
def check_params_computed(self, var_ID, method):
|
|
149
178
|
"""Check if normalisation params computed for a given variable"""
|
|
150
179
|
if (
|
|
151
|
-
var_ID in self.
|
|
152
|
-
and self.
|
|
153
|
-
and "
|
|
154
|
-
and "param2" in self.norm_params[var_ID]
|
|
180
|
+
var_ID in self.config
|
|
181
|
+
and self.config[var_ID]["method"] == method
|
|
182
|
+
and "params" in self.config[var_ID]
|
|
155
183
|
):
|
|
156
184
|
return True
|
|
157
185
|
else:
|
|
158
186
|
return False
|
|
159
187
|
|
|
160
|
-
def
|
|
161
|
-
"""Add `kwargs` to `
|
|
162
|
-
self.
|
|
188
|
+
def add_to_config(self, var_ID, **kwargs):
|
|
189
|
+
"""Add `kwargs` to `config` dict for variable `var_ID`"""
|
|
190
|
+
self.config[var_ID] = kwargs
|
|
163
191
|
|
|
164
192
|
def get_norm_params(self, var_ID, data, method=None):
|
|
165
|
-
"""Get pre-computed normalisation params or compute them for variable `var_ID`
|
|
193
|
+
"""Get pre-computed normalisation params or compute them for variable `var_ID`
|
|
194
|
+
|
|
195
|
+
TODO do we need to pass var_ID? Can we just use name of data?
|
|
196
|
+
"""
|
|
166
197
|
if method not in self.valid_methods:
|
|
167
198
|
raise ValueError(
|
|
168
199
|
f"Method {method} not recognised. Must be one of {self.valid_methods}"
|
|
169
200
|
)
|
|
170
201
|
|
|
171
202
|
if self.check_params_computed(var_ID, method):
|
|
172
|
-
# Already have "
|
|
173
|
-
|
|
174
|
-
param2 = self.norm_params[var_ID]["param2"]
|
|
203
|
+
# Already have "params" in config with `"method": method` - load them
|
|
204
|
+
params = self.config[var_ID]["params"]
|
|
175
205
|
else:
|
|
176
206
|
# Params not computed - compute them now
|
|
177
207
|
if self.verbose:
|
|
@@ -182,19 +212,16 @@ class DataProcessor:
|
|
|
182
212
|
)
|
|
183
213
|
DataProcessor.load_dask(data)
|
|
184
214
|
if method == "mean_std":
|
|
185
|
-
|
|
186
|
-
param2 = float(data.std())
|
|
215
|
+
params = {"mean": float(data.mean()), "std": float(data.std())}
|
|
187
216
|
elif method == "min_max":
|
|
188
|
-
|
|
189
|
-
param2 = float(data.max())
|
|
217
|
+
params = {"min": float(data.min()), "max": float(data.max())}
|
|
190
218
|
if self.verbose:
|
|
191
|
-
print(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
var_ID, **{"method": method, "param1": param1, "param2": param2}
|
|
219
|
+
print(f"Done. {var_ID} {method} params={params}")
|
|
220
|
+
self.add_to_config(
|
|
221
|
+
var_ID,
|
|
222
|
+
**{"method": method, "params": params},
|
|
196
223
|
)
|
|
197
|
-
return
|
|
224
|
+
return params
|
|
198
225
|
|
|
199
226
|
def map_coord_array(self, coord_array: np.ndarray, unnorm: bool = False):
|
|
200
227
|
"""Normalise or unnormalise a coordinate array
|
|
@@ -214,8 +241,8 @@ class DataProcessor:
|
|
|
214
241
|
x1 (np.ndarray): Array of shape (N_x1,) containing spatial coords of x1
|
|
215
242
|
unnorm (bool, optional): Whether to unnormalise. Defaults to False.
|
|
216
243
|
"""
|
|
217
|
-
x11, x12 = self.
|
|
218
|
-
x21, x22 = self.
|
|
244
|
+
x11, x12 = self.config["coords"]["x1"]["map"]
|
|
245
|
+
x21, x22 = self.config["coords"]["x2"]["map"]
|
|
219
246
|
|
|
220
247
|
if not unnorm:
|
|
221
248
|
new_coords_x1 = (x1 - x11) / (x12 - x11)
|
|
@@ -232,6 +259,7 @@ class DataProcessor:
|
|
|
232
259
|
unnorm=False,
|
|
233
260
|
):
|
|
234
261
|
"""Normalise spatial coords in a pandas or xarray object"""
|
|
262
|
+
|
|
235
263
|
if isinstance(data, (pd.DataFrame, pd.Series)):
|
|
236
264
|
# Reset index to get coords as columns
|
|
237
265
|
indexes = list(data.index.names)
|
|
@@ -239,21 +267,39 @@ class DataProcessor:
|
|
|
239
267
|
|
|
240
268
|
if unnorm:
|
|
241
269
|
new_coord_IDs = [
|
|
242
|
-
self.
|
|
270
|
+
self.config["coords"][coord_ID]["name"]
|
|
243
271
|
for coord_ID in ["time", "x1", "x2"]
|
|
244
272
|
]
|
|
245
273
|
old_coord_IDs = ["time", "x1", "x2"]
|
|
246
274
|
else:
|
|
247
275
|
new_coord_IDs = ["time", "x1", "x2"]
|
|
248
276
|
old_coord_IDs = [
|
|
249
|
-
self.
|
|
277
|
+
self.config["coords"][coord_ID]["name"]
|
|
250
278
|
for coord_ID in ["time", "x1", "x2"]
|
|
251
279
|
]
|
|
252
280
|
|
|
253
|
-
|
|
254
|
-
data[old_coord_IDs[1]],
|
|
281
|
+
x1, x2 = (
|
|
282
|
+
data[old_coord_IDs[1]],
|
|
283
|
+
data[old_coord_IDs[2]],
|
|
255
284
|
)
|
|
256
285
|
|
|
286
|
+
# Infer x1 and x2 mappings from min/max of data coords if not provided by user
|
|
287
|
+
if self.x1_none and self.x2_none:
|
|
288
|
+
x1_map = (x1.min(), x1.max())
|
|
289
|
+
x2_map = (x2.min(), x2.max())
|
|
290
|
+
x1_map, x2_map = self._validate_coord_mappings(x1_map, x2_map)
|
|
291
|
+
self.config["coords"]["x1"]["map"] = x1_map
|
|
292
|
+
self.config["coords"]["x2"]["map"] = x2_map
|
|
293
|
+
if self.verbose:
|
|
294
|
+
print(
|
|
295
|
+
f"Inferring x1_map={x1_map} and x2_map={x2_map} from data min/max"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
self.x2_none = False
|
|
299
|
+
self.x1_none = False
|
|
300
|
+
|
|
301
|
+
new_x1, new_x2 = self.map_x1_and_x2(x1, x2, unnorm=unnorm)
|
|
302
|
+
|
|
257
303
|
if isinstance(data, (pd.DataFrame, pd.Series)):
|
|
258
304
|
# Drop old spatial coord columns *before* adding new ones, in case
|
|
259
305
|
# the old name is already x1.
|
|
@@ -306,46 +352,46 @@ class DataProcessor:
|
|
|
306
352
|
"""Normalise or unnormalise the data values in an xarray, pandas, or numpy object"""
|
|
307
353
|
if not unnorm and method is None:
|
|
308
354
|
raise ValueError("Must provide `method` if normalising data.")
|
|
309
|
-
elif
|
|
310
|
-
unnorm
|
|
311
|
-
and method is not None
|
|
312
|
-
and self.norm_params[var_ID]["method"] != method
|
|
313
|
-
):
|
|
355
|
+
elif unnorm and method is not None and self.config[var_ID]["method"] != method:
|
|
314
356
|
# User has provided a different method to the one used for normalising
|
|
315
357
|
raise ValueError(
|
|
316
|
-
f"Variable '{var_ID}' has been normalised with method '{self.
|
|
358
|
+
f"Variable '{var_ID}' has been normalised with method '{self.config[var_ID]['method']}', "
|
|
317
359
|
f"cannot unnormalise with method '{method}'. Pass `method=None` or"
|
|
318
|
-
f"`method='{self.
|
|
360
|
+
f"`method='{self.config[var_ID]['method']}'`"
|
|
319
361
|
)
|
|
320
362
|
if method is None and unnorm:
|
|
321
|
-
# Determine normalisation method from
|
|
322
|
-
method = self.
|
|
363
|
+
# Determine normalisation method from config for unnormalising
|
|
364
|
+
method = self.config[var_ID]["method"]
|
|
323
365
|
elif method not in self.valid_methods:
|
|
324
366
|
raise ValueError(
|
|
325
367
|
f"Method {method} not recognised. Use one of {self.valid_methods}"
|
|
326
368
|
)
|
|
327
369
|
|
|
328
|
-
|
|
370
|
+
params = self.get_norm_params(var_ID, data, method)
|
|
329
371
|
|
|
330
372
|
if method == "mean_std":
|
|
373
|
+
std = params["std"]
|
|
374
|
+
mean = params["mean"]
|
|
331
375
|
if unnorm:
|
|
332
|
-
scale =
|
|
333
|
-
offset =
|
|
376
|
+
scale = std
|
|
377
|
+
offset = mean
|
|
334
378
|
else:
|
|
335
|
-
scale = 1 /
|
|
336
|
-
offset = -
|
|
379
|
+
scale = 1 / std
|
|
380
|
+
offset = -mean / std
|
|
337
381
|
data = data * scale
|
|
338
382
|
if add_offset:
|
|
339
383
|
data = data + offset
|
|
340
384
|
return data
|
|
341
385
|
|
|
342
386
|
elif method == "min_max":
|
|
387
|
+
minimum = params["min"]
|
|
388
|
+
maximum = params["max"]
|
|
343
389
|
if unnorm:
|
|
344
|
-
scale = (
|
|
345
|
-
offset = (
|
|
390
|
+
scale = (maximum - minimum) / 2
|
|
391
|
+
offset = (maximum + minimum) / 2
|
|
346
392
|
else:
|
|
347
|
-
scale = 2 / (
|
|
348
|
-
offset = -(
|
|
393
|
+
scale = 2 / (maximum - minimum)
|
|
394
|
+
offset = -(maximum + minimum) / (maximum - minimum)
|
|
349
395
|
data = data * scale
|
|
350
396
|
if add_offset:
|
|
351
397
|
data = data + offset
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import copy
|
|
2
|
+
import os.path
|
|
2
3
|
import warnings
|
|
4
|
+
import json
|
|
3
5
|
from typing import Union, List
|
|
4
6
|
|
|
5
7
|
import lab as B
|
|
@@ -52,7 +54,7 @@ class ConvNP(DeepSensorModel):
|
|
|
52
54
|
likelihood (str, optional): Likelihood. Must be one of `"cnp"` (equivalently `"het"`),
|
|
53
55
|
`"gnp"` (equivalently `"lowrank"`), or `"cnp-spikes-beta"` (equivalently `"spikes-beta"`).
|
|
54
56
|
Defaults to `"cnp"`.
|
|
55
|
-
dim_x (int, optional): Dimensionality of the inputs. Defaults to
|
|
57
|
+
dim_x (int, optional): Dimensionality of the inputs. Defaults to 2.
|
|
56
58
|
dim_y (int, optional): Dimensionality of the outputs. Defaults to 1.
|
|
57
59
|
dim_yc (int or tuple[int], optional): Dimensionality of the outputs of the
|
|
58
60
|
context set. You should set this if the dimensionality of the outputs
|
|
@@ -110,7 +112,7 @@ class ConvNP(DeepSensorModel):
|
|
|
110
112
|
# so unnormalisation will not be performed at inference time.
|
|
111
113
|
super().__init__()
|
|
112
114
|
|
|
113
|
-
self.model = construct_neural_process(*args, **kwargs)
|
|
115
|
+
self.model, self.config = construct_neural_process(*args, **kwargs)
|
|
114
116
|
|
|
115
117
|
@dispatch
|
|
116
118
|
def __init__(
|
|
@@ -164,7 +166,7 @@ class ConvNP(DeepSensorModel):
|
|
|
164
166
|
print(f"decoder_scale inferred from TaskLoader: {decoder_scale}")
|
|
165
167
|
kwargs["decoder_scale"] = decoder_scale
|
|
166
168
|
|
|
167
|
-
self.model = construct_neural_process(*args,
|
|
169
|
+
self.model, self.config = construct_neural_process(*args, **kwargs)
|
|
168
170
|
|
|
169
171
|
@dispatch
|
|
170
172
|
def __init__(
|
|
@@ -178,6 +180,59 @@ class ConvNP(DeepSensorModel):
|
|
|
178
180
|
|
|
179
181
|
self.model = neural_process
|
|
180
182
|
|
|
183
|
+
@dispatch
|
|
184
|
+
def __init__(self, model_ID: str):
|
|
185
|
+
"""Instantiate a model from a folder containing model weights and config."""
|
|
186
|
+
super().__init__()
|
|
187
|
+
|
|
188
|
+
self.load(model_ID)
|
|
189
|
+
|
|
190
|
+
@dispatch
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
data_processor: DataProcessor,
|
|
194
|
+
task_loader: TaskLoader,
|
|
195
|
+
model_ID: str,
|
|
196
|
+
):
|
|
197
|
+
"""Instantiate a model from a folder containing model weights and config."""
|
|
198
|
+
super().__init__(data_processor, task_loader)
|
|
199
|
+
|
|
200
|
+
self.load(model_ID)
|
|
201
|
+
|
|
202
|
+
def save(self, model_ID: str):
|
|
203
|
+
"""Save the model weights and config to a folder."""
|
|
204
|
+
os.makedirs(model_ID, exist_ok=True)
|
|
205
|
+
|
|
206
|
+
if backend.str == "torch":
|
|
207
|
+
import torch
|
|
208
|
+
|
|
209
|
+
torch.save(self.model.state_dict(), os.path.join(model_ID, "model.pt"))
|
|
210
|
+
elif backend.str == "tf":
|
|
211
|
+
self.model.save_weights(os.path.join(model_ID, "model"))
|
|
212
|
+
else:
|
|
213
|
+
raise NotImplementedError(f"Backend {backend.str} not supported.")
|
|
214
|
+
|
|
215
|
+
config_fpath = os.path.join(model_ID, "model_config.json")
|
|
216
|
+
with open(config_fpath, "w") as f:
|
|
217
|
+
json.dump(self.config, f, indent=4, sort_keys=False)
|
|
218
|
+
|
|
219
|
+
def load(self, model_ID: str):
|
|
220
|
+
"""Load a model from a folder containing model weights and config."""
|
|
221
|
+
config_fpath = os.path.join(model_ID, "model_config.json")
|
|
222
|
+
with open(config_fpath, "r") as f:
|
|
223
|
+
self.config = json.load(f)
|
|
224
|
+
|
|
225
|
+
self.model, _ = construct_neural_process(**self.config)
|
|
226
|
+
|
|
227
|
+
if backend.str == "torch":
|
|
228
|
+
import torch
|
|
229
|
+
|
|
230
|
+
self.model.load_state_dict(torch.load(os.path.join(model_ID, "model.pt")))
|
|
231
|
+
elif backend.str == "tf":
|
|
232
|
+
self.model.load_weights(os.path.join(model_ID, "model"))
|
|
233
|
+
else:
|
|
234
|
+
raise NotImplementedError(f"Backend {backend.str} not supported.")
|
|
235
|
+
|
|
181
236
|
@classmethod
|
|
182
237
|
def modify_task(cls, task):
|
|
183
238
|
"""Cast numpy arrays to TensorFlow or PyTorch tensors, add batch dim, and mask NaNs"""
|
|
@@ -307,7 +307,7 @@ class DeepSensorModel(ProbabilisticModel):
|
|
|
307
307
|
arr[i] = self.data_processor.map_array(
|
|
308
308
|
arr[i],
|
|
309
309
|
var_ID,
|
|
310
|
-
method=self.data_processor.
|
|
310
|
+
method=self.data_processor.config[var_ID]["method"],
|
|
311
311
|
unnorm=True,
|
|
312
312
|
**kwargs,
|
|
313
313
|
)
|
|
@@ -157,8 +157,8 @@ def construct_neural_process(
|
|
|
157
157
|
elif likelihood == "cnp-spikes-beta":
|
|
158
158
|
likelihood = "spikes-beta"
|
|
159
159
|
|
|
160
|
-
#
|
|
161
|
-
|
|
160
|
+
# Log the call signature for `construct_convgnp`
|
|
161
|
+
config = dict(locals())
|
|
162
162
|
|
|
163
163
|
if backend.str == "torch":
|
|
164
164
|
import torch
|
|
@@ -177,14 +177,15 @@ def construct_neural_process(
|
|
|
177
177
|
dim_yt=dim_yt,
|
|
178
178
|
dim_aux_t=dim_aux_t,
|
|
179
179
|
dim_lv=dim_lv,
|
|
180
|
+
likelihood=likelihood,
|
|
180
181
|
conv_arch=conv_arch,
|
|
181
|
-
unet_channels=unet_channels,
|
|
182
|
+
unet_channels=tuple(unet_channels),
|
|
182
183
|
unet_resize_convs=unet_resize_convs,
|
|
183
184
|
unet_resize_conv_interp_method=unet_resize_conv_interp_method,
|
|
184
185
|
aux_t_mlp_layers=aux_t_mlp_layers,
|
|
185
|
-
likelihood=likelihood,
|
|
186
186
|
unet_kernels=unet_kernels,
|
|
187
|
-
|
|
187
|
+
# Use a stride of 1 for the first layer and 2 for all other layers
|
|
188
|
+
unet_strides=(1, *(2,) * (len(unet_channels) - 1)),
|
|
188
189
|
points_per_unit=points_per_unit,
|
|
189
190
|
encoder_scales=encoder_scales,
|
|
190
191
|
encoder_scales_learnable=encoder_scales_learnable,
|
|
@@ -194,7 +195,8 @@ def construct_neural_process(
|
|
|
194
195
|
epsilon=epsilon,
|
|
195
196
|
dtype=dtype,
|
|
196
197
|
)
|
|
197
|
-
|
|
198
|
+
|
|
199
|
+
return neural_process, config
|
|
198
200
|
|
|
199
201
|
|
|
200
202
|
def compute_encoding_tensor(model, task: Task):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: deepsensor
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A Python package for modelling xarray and pandas data with neural processes.
|
|
5
5
|
Home-page: https://github.com/tom-andersson/deepsensor
|
|
6
6
|
Author: Tom R. Andersson
|
|
@@ -11,7 +11,22 @@ Platform: linux
|
|
|
11
11
|
Platform: osx
|
|
12
12
|
Requires-Python: >=3.8
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: neuralprocesses
|
|
15
|
+
Requires-Dist: backends-matrix
|
|
16
|
+
Requires-Dist: backends
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: pandas
|
|
19
|
+
Requires-Dist: xarray
|
|
20
|
+
Requires-Dist: dask
|
|
21
|
+
Requires-Dist: distributed
|
|
22
|
+
Requires-Dist: pyshp
|
|
23
|
+
Requires-Dist: tqdm
|
|
24
|
+
Requires-Dist: pooch
|
|
14
25
|
Provides-Extra: testing
|
|
26
|
+
Requires-Dist: pytest; extra == "testing"
|
|
27
|
+
Requires-Dist: pytest-cov; extra == "testing"
|
|
28
|
+
Requires-Dist: mypy; extra == "testing"
|
|
29
|
+
Requires-Dist: tox; extra == "testing"
|
|
15
30
|
|
|
16
31
|
[//]: # ()
|
|
17
32
|
<ul style="text-align: center;">
|
|
@@ -26,7 +41,7 @@ data with neural processes</p>
|
|
|
26
41
|
|
|
27
42
|
-----------
|
|
28
43
|
|
|
29
|
-
[](https://github.com/tom-andersson/deepsensor/releases)
|
|
30
45
|

|
|
31
46
|
[](https://coveralls.io/github/tom-andersson/deepsensor?branch=main)
|
|
32
47
|
[](https://github.com/psf/black)
|
|
@@ -97,7 +112,7 @@ import numpy as np
|
|
|
97
112
|
ds_raw = xr.tutorial.open_dataset("air_temperature")
|
|
98
113
|
|
|
99
114
|
# Normalise data
|
|
100
|
-
data_processor = DataProcessor(x1_name="lat",
|
|
115
|
+
data_processor = DataProcessor(x1_name="lat", x2_name="lon")
|
|
101
116
|
ds = data_processor(ds_raw)
|
|
102
117
|
|
|
103
118
|
# Set up task loader
|
|
@@ -116,7 +131,7 @@ for date in pd.date_range("2013-01-01", "2014-11-30")[::7]:
|
|
|
116
131
|
# Train model
|
|
117
132
|
trainer = Trainer(model, lr=5e-5)
|
|
118
133
|
for epoch in range(10):
|
|
119
|
-
trainer(
|
|
134
|
+
trainer(train_tasks, progress_bar=True)
|
|
120
135
|
|
|
121
136
|
# Predict on new task with 10% of context data and a dense grid of target points
|
|
122
137
|
test_task = task_loader("2014-12-31", 0.1)
|
|
@@ -64,30 +64,39 @@ class TestDataProcessor(unittest.TestCase):
|
|
|
64
64
|
return False
|
|
65
65
|
return True
|
|
66
66
|
|
|
67
|
+
def test_only_passing_one_x_mapping_raises_valueerror(self):
|
|
68
|
+
with self.assertRaises(ValueError):
|
|
69
|
+
DataProcessor(x1_map=(20, 40), x2_map=None)
|
|
70
|
+
|
|
67
71
|
def test_unnorm_restores_data_for_each_method(self):
|
|
68
72
|
"""Check that the unnormalisation restores the original data for each normalisation method."""
|
|
69
73
|
da_raw = _gen_data_xr()
|
|
70
74
|
df_raw = _gen_data_pandas()
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
dp_with_x_mappings = DataProcessor(
|
|
73
77
|
x1_map=(20, 40),
|
|
74
78
|
x2_map=(40, 60),
|
|
75
79
|
time_name="time",
|
|
76
80
|
x1_name="lat",
|
|
77
81
|
x2_name="lon",
|
|
78
82
|
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
self.
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
dp_inferred_x_mappings = DataProcessor(
|
|
84
|
+
time_name="time", x1_name="lat", x2_name="lon"
|
|
85
|
+
)
|
|
86
|
+
dps = [dp_with_x_mappings, dp_inferred_x_mappings]
|
|
87
|
+
|
|
88
|
+
for dp in dps:
|
|
89
|
+
for method in dp.valid_methods:
|
|
90
|
+
da_norm, df_norm = dp([da_raw, df_raw], method=method)
|
|
91
|
+
da_unnorm, df_unnorm = dp.unnormalise([da_norm, df_norm])
|
|
92
|
+
self.assertTrue(
|
|
93
|
+
self.assert_allclose_xr(da_unnorm, da_raw),
|
|
94
|
+
f"Original {type(da_raw).__name__} not restored for method {method}.",
|
|
95
|
+
)
|
|
96
|
+
self.assertTrue(
|
|
97
|
+
self.assert_allclose_pd(df_unnorm, df_raw),
|
|
98
|
+
f"Original {type(df_raw).__name__} not restored for method {method}.",
|
|
99
|
+
)
|
|
91
100
|
|
|
92
101
|
def test_different_names_xr(self):
|
|
93
102
|
"""
|
|
@@ -3,7 +3,7 @@ import itertools
|
|
|
3
3
|
|
|
4
4
|
from parameterized import parameterized
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import os
|
|
7
7
|
import xarray as xr
|
|
8
8
|
import numpy as np
|
|
9
9
|
import pandas as pd
|
|
@@ -11,11 +11,12 @@ import unittest
|
|
|
11
11
|
|
|
12
12
|
import lab as B
|
|
13
13
|
|
|
14
|
-
import deepsensor.
|
|
14
|
+
import deepsensor.torch as deepsensor
|
|
15
15
|
|
|
16
16
|
from deepsensor.data.processor import DataProcessor
|
|
17
17
|
from deepsensor.data.loader import TaskLoader
|
|
18
18
|
from deepsensor.model.convnp import ConvNP
|
|
19
|
+
from deepsensor.train.train import Trainer
|
|
19
20
|
|
|
20
21
|
from tests.utils import gen_random_data_xr, gen_random_data_pandas
|
|
21
22
|
|
|
@@ -390,6 +391,56 @@ class TestModel(unittest.TestCase):
|
|
|
390
391
|
mean_df.reset_index()["longitude"], df_raw.reset_index()["longitude"]
|
|
391
392
|
)
|
|
392
393
|
|
|
394
|
+
def test_saving_and_loading(self):
|
|
395
|
+
"""Test saving and loading of model"""
|
|
396
|
+
folder = f"tmp_{pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')}"
|
|
397
|
+
os.makedirs(folder, exist_ok=True)
|
|
398
|
+
|
|
399
|
+
ds_raw = xr.tutorial.open_dataset("air_temperature")
|
|
400
|
+
|
|
401
|
+
data_processor = DataProcessor(x1_name="lat", x2_name="lon")
|
|
402
|
+
ds = data_processor(ds_raw)
|
|
403
|
+
|
|
404
|
+
t2m_fpath = f"{folder}/air_temperature_normalised.nc"
|
|
405
|
+
ds.to_netcdf(t2m_fpath)
|
|
406
|
+
|
|
407
|
+
task_loader = TaskLoader(context=t2m_fpath, target=t2m_fpath)
|
|
408
|
+
|
|
409
|
+
model = ConvNP(
|
|
410
|
+
data_processor, task_loader, unet_channels=(32,) * 3, verbose=False
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Train the model for a few iterations to test the trained model is restored correctly later.
|
|
414
|
+
task = task_loader("2014-12-31", 40, datewise_deterministic=True)
|
|
415
|
+
trainer = Trainer(model)
|
|
416
|
+
for _ in range(10):
|
|
417
|
+
trainer([task])
|
|
418
|
+
mean_ds_before, std_ds_before = model.predict(task, X_t=ds_raw)
|
|
419
|
+
mean_ds_before["air"].plot()
|
|
420
|
+
|
|
421
|
+
data_processor.save(folder)
|
|
422
|
+
task_loader.save(folder)
|
|
423
|
+
model.save(folder)
|
|
424
|
+
|
|
425
|
+
data_processor_loaded = DataProcessor(folder)
|
|
426
|
+
task_loader_loaded = TaskLoader(folder)
|
|
427
|
+
model_loaded = ConvNP(data_processor_loaded, task_loader_loaded, folder)
|
|
428
|
+
|
|
429
|
+
task = task_loader_loaded("2014-12-31", 40, datewise_deterministic=True)
|
|
430
|
+
mean_ds_loaded, std_ds_loaded = model_loaded.predict(task, X_t=ds_raw)
|
|
431
|
+
mean_ds_loaded["air"].plot()
|
|
432
|
+
|
|
433
|
+
xr.testing.assert_allclose(mean_ds_before, mean_ds_loaded)
|
|
434
|
+
print("Means match")
|
|
435
|
+
|
|
436
|
+
xr.testing.assert_allclose(std_ds_before, std_ds_loaded)
|
|
437
|
+
print("Standard deviations match")
|
|
438
|
+
|
|
439
|
+
# Delete temporary folder
|
|
440
|
+
import shutil
|
|
441
|
+
|
|
442
|
+
shutil.rmtree(folder)
|
|
443
|
+
|
|
393
444
|
|
|
394
445
|
def assert_shape(x, shape: tuple):
|
|
395
446
|
"""ex: assert_shape(conv_input_array, [8, 3, None, None])"""
|
|
@@ -23,7 +23,7 @@ class TestPlotting(unittest.TestCase):
|
|
|
23
23
|
x2_map=(ds_raw["lon"].min(), ds_raw["lon"].max()),
|
|
24
24
|
)
|
|
25
25
|
ds = self.data_processor(ds_raw)
|
|
26
|
-
self.task_loader = TaskLoader(ds, ds)
|
|
26
|
+
self.task_loader = TaskLoader(context=ds, target=ds)
|
|
27
27
|
self.model = ConvNP(
|
|
28
28
|
self.data_processor,
|
|
29
29
|
self.task_loader,
|
|
@@ -133,6 +133,12 @@ class TestTaskLoader(unittest.TestCase):
|
|
|
133
133
|
|
|
134
134
|
return None
|
|
135
135
|
|
|
136
|
+
def test_saving_taskloader_without_paths_fails(self):
|
|
137
|
+
"""Saving a TaskLoader is only possible if it is instantiated with file paths"""
|
|
138
|
+
tl = TaskLoader(context=self.da, target=self.da)
|
|
139
|
+
with self.assertRaises(ValueError):
|
|
140
|
+
tl.save("foo")
|
|
141
|
+
|
|
136
142
|
def test_aux_at_contexts_and_aux_at_targets(self):
|
|
137
143
|
"""Test the `aux_at_contexts` and `aux_at_targets` arguments"""
|
|
138
144
|
context = [self.da, self.df]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|