ClimateGraph 0.4.0__py3-none-any.whl
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.
- ClimateGraph/__init__.py +9 -0
- ClimateGraph/appkernel.py +148 -0
- ClimateGraph/cli.py +63 -0
- ClimateGraph/data/__init__.py +8 -0
- ClimateGraph/data/data.py +508 -0
- ClimateGraph/data/point_surface.py +91 -0
- ClimateGraph/data/regular_grid.py +25 -0
- ClimateGraph/data/satellite_swath.py +21 -0
- ClimateGraph/domain/__init__.py +11 -0
- ClimateGraph/domain/domain.py +181 -0
- ClimateGraph/domain/domains.py +279 -0
- ClimateGraph/plot/__init__.py +11 -0
- ClimateGraph/plot/plot.py +402 -0
- ClimateGraph/plot/plots.py +1054 -0
- ClimateGraph/plot/primitive.py +99 -0
- ClimateGraph/plot/primitives.py +160 -0
- ClimateGraph/reader/__init__.py +15 -0
- ClimateGraph/reader/reader/__init__.py +14 -0
- ClimateGraph/reader/reader/point_surface/__init__.py +7 -0
- ClimateGraph/reader/reader/point_surface/csv_base.py +135 -0
- ClimateGraph/reader/reader/point_surface/default.py +36 -0
- ClimateGraph/reader/reader/point_surface/dmc.py +10 -0
- ClimateGraph/reader/reader/point_surface/sinca.py +10 -0
- ClimateGraph/reader/reader/point_surface/single_file.py +60 -0
- ClimateGraph/reader/reader/point_surface/station_per_file.py +55 -0
- ClimateGraph/reader/reader/point_surface/variable_per_file.py +91 -0
- ClimateGraph/reader/reader/reader.py +353 -0
- ClimateGraph/reader/reader/regular_grid/__init__.py +7 -0
- ClimateGraph/reader/reader/regular_grid/chimere.py +24 -0
- ClimateGraph/reader/reader/regular_grid/default.py +51 -0
- ClimateGraph/reader/reader/regular_grid/wrf.py +53 -0
- ClimateGraph/utils/__init__.py +1 -0
- ClimateGraph/utils/control_model.py +292 -0
- ClimateGraph/utils/dataset_utils.py +287 -0
- ClimateGraph/utils/general_utils.py +259 -0
- ClimateGraph/utils/parser.py +269 -0
- ClimateGraph/utils/registry.py +81 -0
- ClimateGraph/utils/resample_engine.py +156 -0
- climategraph-0.4.0.dist-info/METADATA +177 -0
- climategraph-0.4.0.dist-info/RECORD +44 -0
- climategraph-0.4.0.dist-info/WHEEL +5 -0
- climategraph-0.4.0.dist-info/entry_points.txt +2 -0
- climategraph-0.4.0.dist-info/licenses/LICENSE +21 -0
- climategraph-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""The dataset abstraction.
|
|
2
|
+
|
|
3
|
+
Brings datasets onto a common set of dimensions regardless of their format, file
|
|
4
|
+
layout or shape, so the rest of the framework can work with them interchangeably.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import cartopy.crs as ccrs
|
|
13
|
+
import numpy as np
|
|
14
|
+
import xarray as xr
|
|
15
|
+
|
|
16
|
+
from ClimateGraph.reader import Reader
|
|
17
|
+
from ClimateGraph.reader.reader.reader import ReadSpec
|
|
18
|
+
from ClimateGraph.utils.dataset_utils import (
|
|
19
|
+
_record,
|
|
20
|
+
change_unit,
|
|
21
|
+
dim_reduction,
|
|
22
|
+
normalize_vars,
|
|
23
|
+
)
|
|
24
|
+
from ClimateGraph.utils.general_utils import ReductionMethodEnum
|
|
25
|
+
from ClimateGraph.utils.registry import RegistryMixin
|
|
26
|
+
from ClimateGraph.utils.resample_engine import ResampleEngine, get_engine
|
|
27
|
+
|
|
28
|
+
log = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Data(RegistryMixin, ABC):
|
|
32
|
+
"""The Data abstract class.
|
|
33
|
+
|
|
34
|
+
A class that abstracts the core nature of environmental data, independent of the topology of the data.
|
|
35
|
+
This class contains and implements attributes and methods common to all the Data that ClimateGraph is supposed to handle.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
registry: dict[str, type["Data"]] = {}
|
|
39
|
+
aliases: list[str] = list()
|
|
40
|
+
geom_dims: tuple[str, ...] = ()
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def get_data_subclass(cls, name: str):
|
|
44
|
+
return cls.get_class(name)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def check_topology_type(cls, type: str) -> bool:
|
|
48
|
+
return cls.check_class(type)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def create(
|
|
52
|
+
cls,
|
|
53
|
+
name: str,
|
|
54
|
+
topology: str,
|
|
55
|
+
reader: str,
|
|
56
|
+
path: Path | list[Path],
|
|
57
|
+
vars: dict[str, dict[str, str]] | list[str] | None,
|
|
58
|
+
crs: ccrs,
|
|
59
|
+
reader_kwargs: dict,
|
|
60
|
+
):
|
|
61
|
+
"""Build a Data object of the subclass matching ``topology``.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
name : str
|
|
66
|
+
Name of the object, used for reference inside ClimateGraph execution.
|
|
67
|
+
topology : str
|
|
68
|
+
Type of topology, used for lookup in the Data registry.
|
|
69
|
+
reader : str
|
|
70
|
+
Specific reader name used for reading the files that compose the data object. A reader must exist for the corresponding topology and reader name.
|
|
71
|
+
path : Path | List[Path]
|
|
72
|
+
Path or list of paths that compose the data object.
|
|
73
|
+
vars : Dict[str, Dict[str, str]]
|
|
74
|
+
Dictionary where keys are Variable names, and the value is another Dictionary with "name" (with the name of the variable in the files) and "unit" (with the "pint" unit name for this variable) keys.
|
|
75
|
+
crs : Cartopy.CRS
|
|
76
|
+
Cartopy Coordinate Reference System object
|
|
77
|
+
reader_kwargs : Dict
|
|
78
|
+
Other keyword arguments in Dict format passed directly to the Reader, allows for further reader specialization.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
Data
|
|
83
|
+
Data object created with the arguments in the adequate Data subclass
|
|
84
|
+
"""
|
|
85
|
+
data_cls = cls.get_data_subclass(topology)
|
|
86
|
+
reader_cls = Reader.get_reader_subclass(topology, reader)
|
|
87
|
+
return data_cls(name, path, vars, reader_cls, crs, reader_kwargs=reader_kwargs)
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
name: str,
|
|
92
|
+
path: Path | list[Path],
|
|
93
|
+
vars: dict[str, dict[str, str]] | list[str] | None,
|
|
94
|
+
reader: Reader,
|
|
95
|
+
crs: ccrs.CRS,
|
|
96
|
+
reader_kwargs: dict[str, Any] | None = None,
|
|
97
|
+
):
|
|
98
|
+
"""Create a dataset handle. Nothing is read until ``obj`` is accessed.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
name : str
|
|
103
|
+
Name for reference inside the ClimateGraph execution.
|
|
104
|
+
path : Path | list[Path]
|
|
105
|
+
Path or list of paths that compose the data objects
|
|
106
|
+
vars : Dict[str, Dict[str, str]]
|
|
107
|
+
Dictionary where keys are Variable names, and the value is another Dictionary with "name" (with the name of the variable in the files) and "unit" (with the "pint" unit name for this variable) keys.
|
|
108
|
+
reader : Reader
|
|
109
|
+
Specific reader object used for reading the files that compose the data object. A reader must exist for the corresponding topology and reader name.
|
|
110
|
+
crs : ccrs.CRS
|
|
111
|
+
Cartopy Coordinate Reference System object
|
|
112
|
+
reader_kwargs : Dict[str, Any] | None, optional
|
|
113
|
+
Other keyword arguments in Dict format passed directly to the Reader, allows for further reader specialization, by default None
|
|
114
|
+
"""
|
|
115
|
+
self.reader = reader
|
|
116
|
+
self.reader_kwargs = {} if reader_kwargs is None else reader_kwargs
|
|
117
|
+
|
|
118
|
+
self._obj = None
|
|
119
|
+
self._geom = None
|
|
120
|
+
self._resample_cache: dict = {}
|
|
121
|
+
self._vars = normalize_vars(vars)
|
|
122
|
+
self._path = None
|
|
123
|
+
self._bbox = None
|
|
124
|
+
self._dims = None
|
|
125
|
+
|
|
126
|
+
self.name = name
|
|
127
|
+
self.path = path
|
|
128
|
+
self.crs = crs
|
|
129
|
+
|
|
130
|
+
def copy(self):
|
|
131
|
+
"""Return a copy of this object. Used when a domain produces a filtered dataset.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
Data
|
|
136
|
+
Exact replica of the Data object
|
|
137
|
+
"""
|
|
138
|
+
new = self.__class__(
|
|
139
|
+
self.name,
|
|
140
|
+
self.path,
|
|
141
|
+
self.vars,
|
|
142
|
+
self.reader,
|
|
143
|
+
self.crs,
|
|
144
|
+
reader_kwargs=self.reader_kwargs,
|
|
145
|
+
)
|
|
146
|
+
new.obj = self._obj
|
|
147
|
+
new._geom = self._geom
|
|
148
|
+
new._bbox = self._bbox
|
|
149
|
+
new._dims = self._dims
|
|
150
|
+
|
|
151
|
+
return new
|
|
152
|
+
|
|
153
|
+
@abstractmethod
|
|
154
|
+
def _set_geom(self):
|
|
155
|
+
"""Build the pyresample geometry used for resampling.
|
|
156
|
+
|
|
157
|
+
The core of the topology abstraction: each topology defines its own
|
|
158
|
+
spatial geometry here.
|
|
159
|
+
"""
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def obj(self) -> xr.Dataset:
|
|
164
|
+
"""The underlying dataset, loaded on first access.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
xr.Dataset
|
|
169
|
+
Xarray dataset of the Data object.
|
|
170
|
+
"""
|
|
171
|
+
if self._obj is None:
|
|
172
|
+
self.load_obj()
|
|
173
|
+
return self._obj
|
|
174
|
+
|
|
175
|
+
@obj.setter
|
|
176
|
+
def obj(self, _obj: xr.Dataset):
|
|
177
|
+
"""Replace the underlying dataset, clearing everything derived from the old one.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
_obj : xr.Dataset
|
|
182
|
+
Xarray Dataset object meant to be set in the Data object.
|
|
183
|
+
"""
|
|
184
|
+
self._obj = _obj
|
|
185
|
+
self._bbox = None
|
|
186
|
+
self._geom = None
|
|
187
|
+
self._dims = None
|
|
188
|
+
self._resample_cache = {}
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def geom(self):
|
|
192
|
+
if self._geom is None:
|
|
193
|
+
self._set_geom()
|
|
194
|
+
return self._geom
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def vars(self):
|
|
198
|
+
"""The variable names this dataset exposes.
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
Dict[str, Dict[str, str]] | None
|
|
203
|
+
Normalized dict mapping each user-facing variable name to a dict with
|
|
204
|
+
"name" (file-native name) and "unit" (pint unit, possibly None)
|
|
205
|
+
keys, or None when the data block declared no vars, and vars will be treated
|
|
206
|
+
by native file names.
|
|
207
|
+
"""
|
|
208
|
+
return self._vars
|
|
209
|
+
|
|
210
|
+
def var_unit(self, var_name: str) -> str | None:
|
|
211
|
+
"""Resolve the declared source unit for ``var_name``.
|
|
212
|
+
|
|
213
|
+
Single choke point for reading a variable's declared unit, so callers
|
|
214
|
+
never index self.vars[...]["unit"]
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
var_name : str
|
|
219
|
+
User-facing variable name.
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
str | None
|
|
224
|
+
The declared pint unit, or ``None`` when vars wasn't declared.
|
|
225
|
+
"""
|
|
226
|
+
if isinstance(self._vars, dict):
|
|
227
|
+
entry = self._vars.get(var_name)
|
|
228
|
+
if entry is not None:
|
|
229
|
+
return entry.get("unit")
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def dims(self):
|
|
234
|
+
"""Mapping of dimension name to dimension size.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
Dict[str, int]
|
|
239
|
+
Dictionary mapping dimension names to dimension size.
|
|
240
|
+
"""
|
|
241
|
+
if self._dims is None:
|
|
242
|
+
self._dims = dict(self.obj.sizes)
|
|
243
|
+
return self._dims
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def bbox(self):
|
|
247
|
+
"""Bounding box around the spatial extent of the dataset.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
Tuple[float, float, float, float]
|
|
252
|
+
Tuple representing (Minimum Longitude, Minimum Latitude, Maximum Longitude, Maximum Latitude)
|
|
253
|
+
"""
|
|
254
|
+
if self._bbox is None:
|
|
255
|
+
lons, lats = self.get_coordinates(["longitude", "latitude"])
|
|
256
|
+
self._bbox = (
|
|
257
|
+
float(lons.min()),
|
|
258
|
+
float(lats.min()),
|
|
259
|
+
float(lons.max()),
|
|
260
|
+
float(lats.max()),
|
|
261
|
+
)
|
|
262
|
+
return self._bbox
|
|
263
|
+
|
|
264
|
+
def load_obj(self):
|
|
265
|
+
"""Read the dataset into ``obj`` by building
|
|
266
|
+
a ReadSpec and invoking ``reader.read(spec)``. ``load_mode``,
|
|
267
|
+
``cache_dir`` and ``engine`` are pulled out of ``reader_kwargs``
|
|
268
|
+
if present; the remainder lives on ``spec.extras`` for the
|
|
269
|
+
subclass to consume.
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
xr.Dataset
|
|
274
|
+
Xarray dataset of the Data object.
|
|
275
|
+
"""
|
|
276
|
+
extras = dict(self.reader_kwargs)
|
|
277
|
+
load_mode = extras.pop("load_mode", "safe")
|
|
278
|
+
cache_dir = extras.pop("cache_dir", None)
|
|
279
|
+
engine = extras.pop("engine", None)
|
|
280
|
+
spec = ReadSpec(
|
|
281
|
+
paths=self.path if isinstance(self.path, list) else [self.path],
|
|
282
|
+
vars=self.vars,
|
|
283
|
+
load_mode=load_mode,
|
|
284
|
+
cache_dir=cache_dir,
|
|
285
|
+
engine=engine,
|
|
286
|
+
extras=extras,
|
|
287
|
+
)
|
|
288
|
+
self._obj = self.reader.read(spec)
|
|
289
|
+
return self._obj
|
|
290
|
+
|
|
291
|
+
def get_var(
|
|
292
|
+
self,
|
|
293
|
+
var_name: str,
|
|
294
|
+
in_unit: str | None = None,
|
|
295
|
+
reduction_func: str | None = None,
|
|
296
|
+
keep_dims: str | list[str] | None = None,
|
|
297
|
+
reduction_dims: str | list[str] | None = None,
|
|
298
|
+
as_array: bool = False,
|
|
299
|
+
dim_reduce: dict[str, str | dict] | None = None,
|
|
300
|
+
) -> xr.DataArray | np.ndarray:
|
|
301
|
+
"""Get a variable, reduced and unit-converted, ready to draw.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
var_name : str
|
|
306
|
+
Variable name.
|
|
307
|
+
in_unit : str | None, optional
|
|
308
|
+
Measure of unit in which to convert the variable, by default None
|
|
309
|
+
reduction_func : str | None, optional
|
|
310
|
+
name of function with which to reduce the variable dimensions, dimensions in reduction_dims or, that aren't in keep_dims arg. Works only if keep_dims or reduction_dims is also specified. By default None
|
|
311
|
+
keep_dims : str | list[str] | None, optional
|
|
312
|
+
Dimensions to keep after reducing the others with the reduction_func. Only works if reduction_func is not None. By default None
|
|
313
|
+
reduction_dims : str | list[str] | None, optional
|
|
314
|
+
Dimensions to reduce with the reduction_func. Only works if reduction_func is not None. By default None
|
|
315
|
+
as_array : bool, optional
|
|
316
|
+
Boolean representing if the resulting data will be returned in numpy.ndarray format, by default False
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
xr.DataArray | numpy.ndarray
|
|
321
|
+
Variable obtained from the obj attribute. Processed if required.
|
|
322
|
+
|
|
323
|
+
Raises
|
|
324
|
+
------
|
|
325
|
+
KeyError
|
|
326
|
+
If the variable can't be found in the obj object.
|
|
327
|
+
"""
|
|
328
|
+
if var_name not in self.obj:
|
|
329
|
+
raise KeyError(
|
|
330
|
+
f"Variable {var_name!r} not found in dataset {self.name!r} "
|
|
331
|
+
f"(available: {list(self.obj.data_vars)})."
|
|
332
|
+
)
|
|
333
|
+
xa = self.obj.data_vars[var_name]
|
|
334
|
+
|
|
335
|
+
if dim_reduce is not None:
|
|
336
|
+
xa = dim_reduction(xa, dim_reduce, name=var_name)
|
|
337
|
+
|
|
338
|
+
if reduction_func is not None:
|
|
339
|
+
reduction_func = (
|
|
340
|
+
ReductionMethodEnum(reduction_func).func
|
|
341
|
+
if isinstance(reduction_func, str)
|
|
342
|
+
else reduction_func
|
|
343
|
+
)
|
|
344
|
+
if reduction_dims is None and keep_dims is not None:
|
|
345
|
+
reduction_dims = list(set(self.dims) - set(keep_dims))
|
|
346
|
+
xa = xa.reduce(reduction_func, reduction_dims)
|
|
347
|
+
_record(
|
|
348
|
+
xa,
|
|
349
|
+
f"reduced {var_name!r} over {reduction_dims} with {reduction_func.__name__}",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if in_unit is not None:
|
|
353
|
+
xa = change_unit(xa, self.var_unit(var_name), in_unit)
|
|
354
|
+
|
|
355
|
+
if as_array:
|
|
356
|
+
xa = xa.to_numpy()
|
|
357
|
+
|
|
358
|
+
return xa
|
|
359
|
+
|
|
360
|
+
def get_coordinates(
|
|
361
|
+
self,
|
|
362
|
+
coord_names: list[str] | str,
|
|
363
|
+
as_array: bool = False,
|
|
364
|
+
) -> list[xr.DataArray] | xr.DataArray | list[np.ndarray] | np.ndarray:
|
|
365
|
+
"""Get one or more coordinates from the dataset.
|
|
366
|
+
|
|
367
|
+
Parameters
|
|
368
|
+
----------
|
|
369
|
+
coord_names : list[str] | str
|
|
370
|
+
Coordinate name or names.
|
|
371
|
+
as_array : bool, optional
|
|
372
|
+
Boolean representing if the resulting data will be returned in numpy.ndarray format, by default False
|
|
373
|
+
|
|
374
|
+
Returns
|
|
375
|
+
-------
|
|
376
|
+
list[xr.DataArray] | xr.DataArray | list[np.ndarray] | np.ndarray
|
|
377
|
+
Coordinate(s) obtained from the obj attribute. Processed if required.
|
|
378
|
+
|
|
379
|
+
Raises
|
|
380
|
+
------
|
|
381
|
+
KeyError
|
|
382
|
+
If a coordinate can't be found in the obj object.
|
|
383
|
+
"""
|
|
384
|
+
obj = self.obj
|
|
385
|
+
if isinstance(coord_names, str):
|
|
386
|
+
coord_names = [coord_names]
|
|
387
|
+
missing_coords = [i for i in coord_names if i not in obj.coords]
|
|
388
|
+
if len(missing_coords) > 0:
|
|
389
|
+
raise KeyError(f"Coordinates {missing_coords} not in {self.name} dataset.")
|
|
390
|
+
|
|
391
|
+
coords = [obj.coords[coord] for coord in coord_names]
|
|
392
|
+
if as_array:
|
|
393
|
+
coords = [array.to_numpy() for array in coords]
|
|
394
|
+
|
|
395
|
+
if len(coords) == 1:
|
|
396
|
+
coords = coords[0]
|
|
397
|
+
return coords
|
|
398
|
+
|
|
399
|
+
def resample_vars(
|
|
400
|
+
self,
|
|
401
|
+
other: "Data",
|
|
402
|
+
vars: str | list[str],
|
|
403
|
+
radius_of_influence: int = 10000,
|
|
404
|
+
engine: str | ResampleEngine = "pyresample",
|
|
405
|
+
engine_kwargs: dict | None = None,
|
|
406
|
+
) -> xr.Dataset:
|
|
407
|
+
"""Project ``other``'s vars onto ``self``'s spatial geometry.
|
|
408
|
+
|
|
409
|
+
Purely spatial: each variable of ``other`` is reprojected onto ``self``'s
|
|
410
|
+
geometry with the requested engine, keeping ``other``'s own time axis and any
|
|
411
|
+
extra dims (z, member, …) untouched, and inheriting ``self``'s spatial
|
|
412
|
+
coordinates (site / latitude / longitude / region / …). It does NOT align time
|
|
413
|
+
or convert units — those are the caller's concern. Two sources with different
|
|
414
|
+
time extents resampled onto the same target never clobber each other.
|
|
415
|
+
|
|
416
|
+
The result is memoized on the source (``other._resample_cache``), keyed by
|
|
417
|
+
``(self.name, vars, radius, engine, engine_kwargs)``, so repeated
|
|
418
|
+
``resample_to`` domains sharing a target recompute the projection once and hit
|
|
419
|
+
the cache thereafter. The cache is invalidated whenever ``other.obj`` is
|
|
420
|
+
reassigned; the target's geometry is assumed stable across a run.
|
|
421
|
+
|
|
422
|
+
Parameters
|
|
423
|
+
----------
|
|
424
|
+
other : Data
|
|
425
|
+
Data whose vars are reprojected onto ``self``'s geometry.
|
|
426
|
+
vars : str | list[str]
|
|
427
|
+
Variable name or list of names to resample.
|
|
428
|
+
radius_of_influence : int, optional
|
|
429
|
+
Radius length in meters to use for resampling. by default 10000
|
|
430
|
+
engine : str | ResampleEngine, optional
|
|
431
|
+
Resample backend name or instance. Default ``"pyresample"``.
|
|
432
|
+
engine_kwargs : dict | None, optional
|
|
433
|
+
Extra kwargs forwarded to the engine constructor
|
|
434
|
+
(e.g. ``method``, ``sigmas``). Default None.
|
|
435
|
+
|
|
436
|
+
Returns
|
|
437
|
+
-------
|
|
438
|
+
xr.Dataset
|
|
439
|
+
One variable per input var, keyed ``"{var}__{other.name}"``, on
|
|
440
|
+
``self``'s geometry and carrying ``other``'s time / extra dims.
|
|
441
|
+
"""
|
|
442
|
+
if isinstance(vars, str):
|
|
443
|
+
vars = [vars]
|
|
444
|
+
|
|
445
|
+
engine_key = engine if isinstance(engine, str) else type(engine).__name__
|
|
446
|
+
cache_key = (
|
|
447
|
+
self.name,
|
|
448
|
+
tuple(sorted(vars)),
|
|
449
|
+
radius_of_influence,
|
|
450
|
+
engine_key,
|
|
451
|
+
tuple(sorted((engine_kwargs or {}).items())),
|
|
452
|
+
)
|
|
453
|
+
if cache_key in other._resample_cache:
|
|
454
|
+
log.debug("resample cache hit: %s -> %s (%s)", other.name, self.name, vars)
|
|
455
|
+
return other._resample_cache[cache_key]
|
|
456
|
+
|
|
457
|
+
resample_engine = get_engine(engine, **(engine_kwargs or {}))
|
|
458
|
+
info = resample_engine.prepare(
|
|
459
|
+
other.geom,
|
|
460
|
+
self.geom,
|
|
461
|
+
radius_of_influence=radius_of_influence,
|
|
462
|
+
)
|
|
463
|
+
_resample = resample_engine.make_resampler(
|
|
464
|
+
info, self.geom.shape, other.geom.shape
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
dst_geom_dims = [d for d in self.obj.dims if d in set(self.geom_dims)]
|
|
468
|
+
dst_sizes = {d: self.obj.sizes[d] for d in dst_geom_dims}
|
|
469
|
+
dst_coords = {
|
|
470
|
+
name: coord
|
|
471
|
+
for name, coord in self.obj.coords.items()
|
|
472
|
+
if set(coord.dims) <= set(self.geom_dims)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
resampled_vars = {}
|
|
476
|
+
for var in vars:
|
|
477
|
+
var_src = other.get_var(var)
|
|
478
|
+
src_geom_dims = [d for d in var_src.dims if d in set(other.geom_dims)]
|
|
479
|
+
|
|
480
|
+
resampled = xr.apply_ufunc(
|
|
481
|
+
_resample,
|
|
482
|
+
var_src,
|
|
483
|
+
input_core_dims=[src_geom_dims],
|
|
484
|
+
output_core_dims=[dst_geom_dims],
|
|
485
|
+
dask="parallelized",
|
|
486
|
+
output_dtypes=[var_src.dtype],
|
|
487
|
+
dask_gufunc_kwargs={"output_sizes": dst_sizes},
|
|
488
|
+
)
|
|
489
|
+
resampled_vars[f"{var}__{other.name}"] = resampled.assign_coords(dst_coords)
|
|
490
|
+
|
|
491
|
+
result = xr.Dataset(resampled_vars)
|
|
492
|
+
# load so anything downstream doesn't re compute all processed until here
|
|
493
|
+
result = result.load()
|
|
494
|
+
_record(
|
|
495
|
+
result,
|
|
496
|
+
f"spatially resampled {list(resampled_vars)} from {other.name} "
|
|
497
|
+
f"({type(other).__name__}) to {self.name} ({type(self).__name__})",
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
save_to = self.reader_kwargs.get("save_resampled_to")
|
|
501
|
+
if save_to:
|
|
502
|
+
target = Path(save_to)
|
|
503
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
504
|
+
result.to_netcdf(target)
|
|
505
|
+
_record(result, f"saved resampled data to {target}")
|
|
506
|
+
|
|
507
|
+
other._resample_cache[cache_key] = result
|
|
508
|
+
return result
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Station and point-observation topology."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import cartopy.crs as ccrs
|
|
7
|
+
import numpy as np
|
|
8
|
+
import xarray as xr
|
|
9
|
+
from pyresample.geometry import SwathDefinition
|
|
10
|
+
|
|
11
|
+
from .data import Data
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _PrebuiltReader:
|
|
15
|
+
"""No-op reader for in-memory PointSurface objects whose ``obj`` is set directly.
|
|
16
|
+
|
|
17
|
+
``from_points`` builds the dataset itself, so ``read`` is never reached; it
|
|
18
|
+
only exists to satisfy ``Data.__init__``/``copy`` which carry a reader around.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def read(spec):
|
|
23
|
+
raise RuntimeError(
|
|
24
|
+
"_PrebuiltReader has no files to read; its obj is set in-memory."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PointSurface(Data):
|
|
29
|
+
"""The PointSurface topology class.
|
|
30
|
+
|
|
31
|
+
A class that gives particular representation to Point Surface data. This data should be in ("time", "site") dimensions.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
aliases = ["pt_sfc", "point_surface", "point"]
|
|
35
|
+
geom_dims = ("site",)
|
|
36
|
+
|
|
37
|
+
def _set_geom(self):
|
|
38
|
+
"""Build the pyresample ``SwathDefinition`` over the one-dimensional site coordinates."""
|
|
39
|
+
lons, lats = self.get_coordinates(["longitude", "latitude"], as_array=True)
|
|
40
|
+
self._geom = SwathDefinition(lons=lons, lats=lats, crs=self.crs)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_points(
|
|
44
|
+
cls,
|
|
45
|
+
name: str,
|
|
46
|
+
names: Sequence[str],
|
|
47
|
+
lats: Sequence[float],
|
|
48
|
+
lons: Sequence[float],
|
|
49
|
+
crs: ccrs.CRS | None = None,
|
|
50
|
+
) -> "PointSurface":
|
|
51
|
+
"""Build an in-memory point target from named coordinates.
|
|
52
|
+
|
|
53
|
+
Used as the resample target of a ``Points`` domain: a coords-only
|
|
54
|
+
PointSurface whose ``site`` dimension holds the provided **names**, with
|
|
55
|
+
``latitude``/``longitude`` on ``site`` (so ``_set_geom`` can build the
|
|
56
|
+
SwathDefinition). No data variables — the resample only needs the geometry
|
|
57
|
+
and the site-scoped coords. ``obj`` is set directly, so no reader runs.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
name : str
|
|
62
|
+
Identity for the target (used in the resample cache key / history).
|
|
63
|
+
names : Sequence[str]
|
|
64
|
+
Point names — become the ``site`` index (labels and ``.sel`` keys).
|
|
65
|
+
lats, lons : Sequence[float]
|
|
66
|
+
Latitude / longitude per point, in the ``crs`` (default lat/lon).
|
|
67
|
+
crs : ccrs.CRS | None, optional
|
|
68
|
+
Coordinate reference system. Defaults to ``PlateCarree`` (lat/lon).
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
PointSurface
|
|
73
|
+
An in-memory point target ready to resample onto.
|
|
74
|
+
"""
|
|
75
|
+
site = np.asarray(list(names), dtype=object)
|
|
76
|
+
ds = xr.Dataset(
|
|
77
|
+
coords={
|
|
78
|
+
"site": site,
|
|
79
|
+
"latitude": ("site", np.asarray(lats, dtype=float)),
|
|
80
|
+
"longitude": ("site", np.asarray(lons, dtype=float)),
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
instance = cls(
|
|
84
|
+
name=name,
|
|
85
|
+
path=Path(f"memory://{name}"),
|
|
86
|
+
vars=None,
|
|
87
|
+
reader=_PrebuiltReader,
|
|
88
|
+
crs=crs or ccrs.PlateCarree(),
|
|
89
|
+
)
|
|
90
|
+
instance.obj = ds
|
|
91
|
+
return instance
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Structured latitude/longitude grid topology."""
|
|
2
|
+
|
|
3
|
+
from pyresample import SwathDefinition
|
|
4
|
+
|
|
5
|
+
from .data import Data
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RegularGrid(Data):
|
|
9
|
+
"""The RegularGrid topology class.
|
|
10
|
+
|
|
11
|
+
A class that implements Regular Grid specific logic. This data should be in ("time", "x", "y", "z") dimensions.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
aliases = ["regular_grid", "grid", "regulargrid"]
|
|
15
|
+
geom_dims = ("x", "y")
|
|
16
|
+
|
|
17
|
+
def _set_geom(self):
|
|
18
|
+
"""Build the pyresample geometry.
|
|
19
|
+
|
|
20
|
+
Uses a ``SwathDefinition`` rather than an ``AreaDefinition``, which does
|
|
21
|
+
not behave correctly here.
|
|
22
|
+
"""
|
|
23
|
+
# TODO: change to AreaDefinition
|
|
24
|
+
lons, lats = self.get_coordinates(["longitude", "latitude"], as_array=True)
|
|
25
|
+
self._geom = SwathDefinition(lons=lons, lats=lats, crs=self.crs)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Satellite swath topology.
|
|
2
|
+
|
|
3
|
+
not usable yet
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pyresample.geometry import SwathDefinition
|
|
7
|
+
|
|
8
|
+
from .data import Data
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SatelliteSwath(Data):
|
|
12
|
+
"""The Satellite Swath topology class. Not being used and not recommended.
|
|
13
|
+
|
|
14
|
+
A class that gives particular representation to Satellite Swath data.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def _set_geom(self):
|
|
18
|
+
lons, lats = self.get_coordinates(["longitude", "latitude"])
|
|
19
|
+
swath_def = SwathDefinition(lons=lons, lats=lats, crs=self.crs)
|
|
20
|
+
self.geom = swath_def.compute_optimal_bb_area(self.crs.proj4_params)
|
|
21
|
+
return self.geom
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Spatial and attribute subsetting applied to datasets."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import pkgutil
|
|
5
|
+
|
|
6
|
+
for module in pkgutil.iter_modules(__path__):
|
|
7
|
+
importlib.import_module(f"{__name__}.{module.name}")
|
|
8
|
+
|
|
9
|
+
from .domain import Domain
|
|
10
|
+
|
|
11
|
+
__all__ = ["Domain"]
|