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.
Files changed (41) hide show
  1. {deepsensor-0.1.11 → deepsensor-0.2.1}/PKG-INFO +19 -4
  2. {deepsensor-0.1.11 → deepsensor-0.2.1}/README.md +3 -3
  3. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/active_learning/algorithms.py +2 -2
  4. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/data/loader.py +197 -52
  5. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/data/processor.py +119 -73
  6. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/model/convnp.py +58 -3
  7. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/model/model.py +1 -1
  8. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/model/nps.py +8 -6
  9. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/PKG-INFO +19 -4
  10. {deepsensor-0.1.11 → deepsensor-0.2.1}/setup.cfg +1 -1
  11. {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_data_processor.py +22 -13
  12. {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_model.py +53 -2
  13. {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_plotting.py +1 -1
  14. {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_task_loader.py +6 -0
  15. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/__init__.py +0 -0
  16. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/active_learning/__init__.py +0 -0
  17. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/active_learning/acquisition_fns.py +0 -0
  18. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/config.py +0 -0
  19. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/data/__init__.py +0 -0
  20. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/data/task.py +0 -0
  21. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/data/utils.py +0 -0
  22. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/errors.py +0 -0
  23. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/model/__init__.py +0 -0
  24. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/model/defaults.py +0 -0
  25. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/plot.py +0 -0
  26. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/py.typed +0 -0
  27. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/tensorflow/__init__.py +0 -0
  28. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/torch/__init__.py +0 -0
  29. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/train/__init__.py +0 -0
  30. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor/train/train.py +0 -0
  31. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/SOURCES.txt +0 -0
  32. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/dependency_links.txt +0 -0
  33. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/not-zip-safe +0 -0
  34. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/requires.txt +0 -0
  35. {deepsensor-0.1.11 → deepsensor-0.2.1}/deepsensor.egg-info/top_level.txt +0 -0
  36. {deepsensor-0.1.11 → deepsensor-0.2.1}/pyproject.toml +0 -0
  37. {deepsensor-0.1.11 → deepsensor-0.2.1}/setup.py +0 -0
  38. {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/__init__.py +0 -0
  39. {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_active_learning.py +0 -0
  40. {deepsensor-0.1.11 → deepsensor-0.2.1}/tests/test_training.py +0 -0
  41. {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.11
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
  [//]: # (![](figs/DeepSensorLogo.png))
17
32
  <ul style="text-align: center;">
@@ -26,7 +41,7 @@ data with neural processes</p>
26
41
 
27
42
  -----------
28
43
 
29
- [![release](https://img.shields.io/badge/release-v0.1.11-green?logo=github)](https://github.com/tom-andersson/deepsensor/releases)
44
+ [![release](https://img.shields.io/badge/release-v0.2.1-green?logo=github)](https://github.com/tom-andersson/deepsensor/releases)
30
45
  ![Tests](https://github.com/tom-andersson/deepsensor/actions/workflows/tests.yml/badge.svg)
31
46
  [![Coverage Status](https://coveralls.io/repos/github/tom-andersson/deepsensor/badge.svg?branch=main)](https://coveralls.io/github/tom-andersson/deepsensor?branch=main)
32
47
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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", x1_map=(15, 75), x2_name="lon", x2_map=(200, 330))
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(model, train_tasks, progress_bar=True)
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
- [![release](https://img.shields.io/badge/release-v0.1.11-green?logo=github)](https://github.com/tom-andersson/deepsensor/releases)
14
+ [![release](https://img.shields.io/badge/release-v0.2.1-green?logo=github)](https://github.com/tom-andersson/deepsensor/releases)
15
15
  ![Tests](https://github.com/tom-andersson/deepsensor/actions/workflows/tests.yml/badge.svg)
16
16
  [![Coverage Status](https://coveralls.io/repos/github/tom-andersson/deepsensor/badge.svg?branch=main)](https://coveralls.io/github/tom-andersson/deepsensor?branch=main)
17
17
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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", x1_map=(15, 75), x2_name="lon", x2_map=(200, 330))
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(model, train_tasks, progress_bar=True)
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.norm_params["coords"]["x1"]["name"]
76
- self.x2_name = self.model.data_processor.norm_params["coords"]["x2"]["name"]
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
- List[Union[xr.DataArray, xr.Dataset, pd.DataFrame]],
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
- List[Union[xr.DataArray, xr.Dataset, pd.DataFrame]],
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
- self.time_freq = time_freq
72
- self.xarray_interp_method = xarray_interp_method
73
- self.discrete_xarray_sampling = discrete_xarray_sampling
74
- self.dtype = dtype
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
- if isinstance(context, (xr.DataArray, xr.Dataset, pd.DataFrame, pd.Series)):
82
- context = (context,)
83
- if isinstance(target, (xr.DataArray, xr.Dataset, pd.DataFrame, pd.Series)):
84
- target = (target,)
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 _cast_context_and_target_to_dtype(
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
- context: List,
127
- target: List,
128
- aux_at_contexts: Union[xr.DataArray, xr.Dataset] = None,
129
- aux_at_targets: Union[xr.DataArray, xr.Dataset] = None,
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
- # x1/x2 coord dtypes during task sampling
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
- context = tuple([cast_to_dtype(var) for var in context])
157
- target = tuple([cast_to_dtype(var) for var in target])
158
- if aux_at_contexts is not None:
159
- aux_at_contexts = aux_at_contexts.astype(self.dtype)
160
- if aux_at_targets is not None:
161
- aux_at_targets = aux_at_targets.astype(self.dtype)
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 context, target, aux_at_contexts, aux_at_targets
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
- norm_params: dict = None,
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 = (0, 1),
23
- x2_map: tuple = (0, 1),
27
+ x1_map: tuple | None = None,
28
+ x2_map: tuple | None = None,
24
29
  deepcopy: bool = True,
25
30
  verbose: bool = False,
26
- ) -> None:
31
+ ):
27
32
  """Initialise DataProcessor
28
33
 
29
34
  Args:
30
- norm_params (dict, optional): Normalisation params. Defaults to {}.
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
- Defaults to (0, 1) (i.e. no normalisation).
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
- Defaults to (0, 1) (i.e. no normalisation).
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
- x1_map, x2_map = self._validate_coord_mappings(x1_map, x2_map)
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
- if norm_params is None:
44
- norm_params = {}
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
- self.norm_params = norm_params
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
- if "coords" not in self.norm_params:
49
- # Add coordinate normalisation info to norm_params
50
- self.set_coord_params(time_name, x1_name, x1_map, x2_name, x2_map)
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.norm_params["coords"][coord]["name"] for coord in ["x1", "x2"]
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.norm_params["coords"][coord]["name"]
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.norm_params["coords"][coord]["name"] for coord in ["time", "x1", "x2"]
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.norm_params)
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.norm_params["coords"] = {}
140
- self.norm_params["coords"]["time"] = {"name": time_name}
141
- self.norm_params["coords"]["x1"] = {}
142
- self.norm_params["coords"]["x2"] = {}
143
- self.norm_params["coords"]["x1"]["name"] = x1_name
144
- self.norm_params["coords"]["x1"]["map"] = x1_map
145
- self.norm_params["coords"]["x2"]["name"] = x2_name
146
- self.norm_params["coords"]["x2"]["map"] = x2_map
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.norm_params
152
- and self.norm_params[var_ID]["method"] == method
153
- and "param1" in self.norm_params[var_ID]
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 add_to_norm_params(self, var_ID, **kwargs):
161
- """Add `kwargs` to `norm_params` dict for variable `var_ID`"""
162
- self.norm_params[var_ID] = kwargs
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 "param1" and "param2" in norm_params with `"method": method` - load them
173
- param1 = self.norm_params[var_ID]["param1"]
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
- param1 = float(data.mean())
186
- param2 = float(data.std())
215
+ params = {"mean": float(data.mean()), "std": float(data.std())}
187
216
  elif method == "min_max":
188
- param1 = float(data.min())
189
- param2 = float(data.max())
217
+ params = {"min": float(data.min()), "max": float(data.max())}
190
218
  if self.verbose:
191
- print(
192
- f"Done. {var_ID} {method} param1={param1:.3f}, param2={param2:.3f}"
193
- )
194
- self.add_to_norm_params(
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 param1, param2
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.norm_params["coords"]["x1"]["map"]
218
- x21, x22 = self.norm_params["coords"]["x2"]["map"]
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.norm_params["coords"][coord_ID]["name"]
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.norm_params["coords"][coord_ID]["name"]
277
+ self.config["coords"][coord_ID]["name"]
250
278
  for coord_ID in ["time", "x1", "x2"]
251
279
  ]
252
280
 
253
- new_x1, new_x2 = self.map_x1_and_x2(
254
- data[old_coord_IDs[1]], data[old_coord_IDs[2]], unnorm=unnorm
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.norm_params[var_ID]['method']}', "
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.norm_params[var_ID]['method']}'`"
360
+ f"`method='{self.config[var_ID]['method']}'`"
319
361
  )
320
362
  if method is None and unnorm:
321
- # Determine normalisation method from norm_params for unnormalising
322
- method = self.norm_params[var_ID]["method"]
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
- param1, param2 = self.get_norm_params(var_ID, data, method)
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 = param2
333
- offset = param1
376
+ scale = std
377
+ offset = mean
334
378
  else:
335
- scale = 1 / param2
336
- offset = -param1 / param2
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 = (param2 - param1) / 2
345
- offset = (param2 + param1) / 2
390
+ scale = (maximum - minimum) / 2
391
+ offset = (maximum + minimum) / 2
346
392
  else:
347
- scale = 2 / (param2 - param1)
348
- offset = -(param2 + param1) / (param2 - param1)
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 1.
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, dim_x=2, **kwargs)
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.norm_params[var_ID]["method"],
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
- # Use a stride of 1 for the first layer and 2 for all other layers
161
- unet_strides = (1, *(2,) * (len(unet_channels) - 1))
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
- unet_strides=unet_strides,
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
- return neural_process
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.11
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
  [//]: # (![]&#40;figs/DeepSensorLogo.png&#41;)
17
32
  <ul style="text-align: center;">
@@ -26,7 +41,7 @@ data with neural processes</p>
26
41
 
27
42
  -----------
28
43
 
29
- [![release](https://img.shields.io/badge/release-v0.1.11-green?logo=github)](https://github.com/tom-andersson/deepsensor/releases)
44
+ [![release](https://img.shields.io/badge/release-v0.2.1-green?logo=github)](https://github.com/tom-andersson/deepsensor/releases)
30
45
  ![Tests](https://github.com/tom-andersson/deepsensor/actions/workflows/tests.yml/badge.svg)
31
46
  [![Coverage Status](https://coveralls.io/repos/github/tom-andersson/deepsensor/badge.svg?branch=main)](https://coveralls.io/github/tom-andersson/deepsensor?branch=main)
32
47
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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", x1_map=(15, 75), x2_name="lon", x2_map=(200, 330))
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(model, train_tasks, progress_bar=True)
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)
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = deepsensor
3
- version = 0.1.11
3
+ version = 0.2.1
4
4
  author = Tom R. Andersson
5
5
  author_email = tomand@bas.ac.uk
6
6
  description = A Python package for modelling xarray and pandas data with neural processes.
@@ -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
- dp = DataProcessor(
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
- for method in dp.valid_methods:
81
- da_norm, df_norm = dp([da_raw, df_raw], method=method)
82
- da_unnorm, df_unnorm = dp.unnormalise([da_norm, df_norm])
83
- self.assertTrue(
84
- self.assert_allclose_xr(da_unnorm, da_raw),
85
- f"Original {type(da_raw).__name__} not restored for method {method}.",
86
- )
87
- self.assertTrue(
88
- self.assert_allclose_pd(df_unnorm, df_raw),
89
- f"Original {type(df_raw).__name__} not restored for method {method}.",
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.tensorflow as 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