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,181 @@
|
|
|
1
|
+
"""Spatial and attribute subsetting.
|
|
2
|
+
|
|
3
|
+
A domain narrows a dataset to the part a plot is about. Applying one is a
|
|
4
|
+
two-step template: an optional reprojection onto another dataset's geometry,
|
|
5
|
+
then the subclass's filter.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from ClimateGraph.utils.registry import RegistryMixin
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ClimateGraph.data.data import Data
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Domain(RegistryMixin, ABC):
|
|
20
|
+
"""The Domain abstract class.
|
|
21
|
+
|
|
22
|
+
A class that abstracts the domain definition and interface.
|
|
23
|
+
This class contains and implements attributes and methods common to all the domain subclasses.
|
|
24
|
+
|
|
25
|
+
A domain application is a two-step template: an optional spatial *resample*
|
|
26
|
+
onto another dataset's geometry (``resample_to`` in the config), followed by
|
|
27
|
+
the subclass's own *filter* step (``_filter``). This lets a single domain
|
|
28
|
+
express "this data, on that geometry, for this subset".
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
registry: dict[str, type["Domain"]] = dict()
|
|
32
|
+
aliases: list[str] = list()
|
|
33
|
+
config: type["BaseModel"] | None = None
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def create(
|
|
37
|
+
cls,
|
|
38
|
+
name: str,
|
|
39
|
+
type: str,
|
|
40
|
+
domain_config: BaseModel,
|
|
41
|
+
target_data: "Data | None" = None,
|
|
42
|
+
) -> "Domain":
|
|
43
|
+
"""Build a Domain of the subclass matching ``type``.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
name : str
|
|
48
|
+
Name of the object, used for reference inside ClimateGraph execution.
|
|
49
|
+
type : str
|
|
50
|
+
Type of Domain, used for lookup in the Domain registry.
|
|
51
|
+
domain_config : BaseModel
|
|
52
|
+
BaseModel object of the corresponding config as a Pydantic model. Used as arguments for the actual domain handling.
|
|
53
|
+
target_data : Data | None, optional
|
|
54
|
+
Resolved target dataset for the optional resample pre-step. The parser
|
|
55
|
+
resolves ``domain_config.resample_to`` (a dataset name) into the actual
|
|
56
|
+
``Data`` object and passes it here. ``None`` when the domain does no
|
|
57
|
+
resampling. By default None.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
Domain
|
|
62
|
+
Domain object with the config, ready to be applied.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
domain_class = cls.get_domain_class(type)
|
|
66
|
+
kwargs = domain_config.model_extra if domain_config.model_extra else {}
|
|
67
|
+
|
|
68
|
+
return domain_class(
|
|
69
|
+
name,
|
|
70
|
+
domain_config=domain_config,
|
|
71
|
+
target_data=target_data,
|
|
72
|
+
**kwargs,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
name: str,
|
|
78
|
+
domain_config: BaseModel,
|
|
79
|
+
target_data: "Data | None" = None,
|
|
80
|
+
**kwargs,
|
|
81
|
+
):
|
|
82
|
+
"""Create a domain. ``target_data`` is the geometry to reproject onto, if any.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
name : str
|
|
87
|
+
Name of the object, used for reference inside ClimateGraph execution.
|
|
88
|
+
domain_config : BaseModel
|
|
89
|
+
BaseModel object of the corresponding config as a Pydantic model. Used as arguments for the actual domain handling.
|
|
90
|
+
target_data : Data | None, optional
|
|
91
|
+
Resolved target dataset for the optional resample pre-step, or None
|
|
92
|
+
when the domain does no resampling. By default None.
|
|
93
|
+
"""
|
|
94
|
+
self.domain_config = domain_config
|
|
95
|
+
|
|
96
|
+
self.name = name
|
|
97
|
+
self.domain_kwargs = kwargs
|
|
98
|
+
self._resample_target = target_data
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def check_domain_class(cls, type: str) -> bool:
|
|
102
|
+
"""Return True if ``type`` names a registered domain."""
|
|
103
|
+
return cls.check_class(type)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def get_domain_class(cls, name: str):
|
|
107
|
+
"""Look up a registered domain subclass by name."""
|
|
108
|
+
return cls.get_class(name)
|
|
109
|
+
|
|
110
|
+
def apply(self, data: "Data") -> "Data":
|
|
111
|
+
"""Apply the domain: optional resample pre-step, then the filter step.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
data : Data
|
|
116
|
+
Data object into which the domain will be applied.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
Data
|
|
121
|
+
Data object with the domain applied. When ``resample_to`` is set the
|
|
122
|
+
result carries the target's topology/geometry (and the source's identity);
|
|
123
|
+
otherwise the topology is unchanged.
|
|
124
|
+
"""
|
|
125
|
+
if self._resample_target is not None:
|
|
126
|
+
data = self._resample(data)
|
|
127
|
+
return self._filter(data)
|
|
128
|
+
|
|
129
|
+
def _resample(self, data: "Data") -> "Data":
|
|
130
|
+
"""Reproject ``data`` onto the resample target's geometry.
|
|
131
|
+
|
|
132
|
+
``resample_vars`` is purely spatial, so the reprojected result keeps
|
|
133
|
+
``data``'s own time axis and inherits the target's spatial coords (site /
|
|
134
|
+
region / lat / lon). It memoizes on the source, so several domains sharing a
|
|
135
|
+
``resample_to`` target (a ``one_for_each`` fan-out, or many hand-written
|
|
136
|
+
domains onto one obs network) pay the projection cost once — the 2nd..Nth
|
|
137
|
+
``_resample`` here hit that cache. The returned value is a cheap
|
|
138
|
+
target-topology wrapper wearing the SOURCE's identity (name + vars).
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
data : Data
|
|
143
|
+
Source data to reproject.
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
Data
|
|
148
|
+
The source data on the target's topology.
|
|
149
|
+
"""
|
|
150
|
+
if data is self._resample_target:
|
|
151
|
+
return data
|
|
152
|
+
vars_list = list(data.obj.data_vars)
|
|
153
|
+
resampled_ds = self._resample_target.resample_vars(
|
|
154
|
+
data,
|
|
155
|
+
vars_list,
|
|
156
|
+
radius_of_influence=self.domain_config.radius_of_influence,
|
|
157
|
+
engine=self.domain_config.engine,
|
|
158
|
+
engine_kwargs=self.domain_config.engine_kwargs,
|
|
159
|
+
)
|
|
160
|
+
clean_ds = resampled_ds.rename({f"{v}__{data.name}": v for v in vars_list})
|
|
161
|
+
result = self._resample_target.copy()
|
|
162
|
+
result.obj = clean_ds
|
|
163
|
+
result.name = data.name
|
|
164
|
+
result._vars = data.vars
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
@abstractmethod
|
|
168
|
+
def _filter(self, data: "Data") -> "Data":
|
|
169
|
+
"""Subclass hook: narrow ``data`` (already resampled if requested).
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
data : Data
|
|
174
|
+
Data object to filter.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
Data
|
|
179
|
+
Filtered Data object. If the filter doesn't apply, the original is returned.
|
|
180
|
+
"""
|
|
181
|
+
pass
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""The concrete domain types and their config models."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
5
|
+
|
|
6
|
+
import geopandas as gpd
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import regionmask
|
|
9
|
+
import shapely.geometry as gm
|
|
10
|
+
from pydantic import BaseModel, model_validator
|
|
11
|
+
|
|
12
|
+
from .domain import Domain
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ClimateGraph.data.data import Data
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseDomainConfig(BaseModel):
|
|
19
|
+
"""BaseDomainConfig Shared config fields for every domain.
|
|
20
|
+
|
|
21
|
+
Carries the optional spatial resample pre-step: when ``resample_to`` names
|
|
22
|
+
another dataset, the domain first reprojects the incoming data onto that
|
|
23
|
+
dataset's geometry, then runs its own filter over the resampled result.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
resample_to: str | None = None
|
|
27
|
+
"""Name of another dataset to reproject onto before filtering. None = no resample."""
|
|
28
|
+
radius_of_influence: int = 50000
|
|
29
|
+
engine: str = "pyresample"
|
|
30
|
+
engine_kwargs: dict | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AttributeConfig(BaseDomainConfig):
|
|
34
|
+
"""AttributeConfig Pydantic model for the Attribute domain definition in the config file."""
|
|
35
|
+
|
|
36
|
+
type: Literal["attribute", "attr"]
|
|
37
|
+
field_name: str
|
|
38
|
+
field_value: Any
|
|
39
|
+
one_for_each: bool = False
|
|
40
|
+
"""When field_value is a list and this is True, the parser expands this
|
|
41
|
+
single domain block into one Attribute domain per value. When False (default),
|
|
42
|
+
the set is analyzed together."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Attribute(Domain):
|
|
46
|
+
"""Attribute Domain specification by attribute in a xr.Dataset, uses Attribute==Value as a mask"""
|
|
47
|
+
|
|
48
|
+
config = AttributeConfig
|
|
49
|
+
aliases = ["attribute", "attr"]
|
|
50
|
+
|
|
51
|
+
def _filter(self, data: "Data") -> "Data":
|
|
52
|
+
"""Keep the data where the field matches the configured value.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
data : Data
|
|
57
|
+
Data to be filtered
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
Data
|
|
62
|
+
Data filtered containing the attribute defined.
|
|
63
|
+
"""
|
|
64
|
+
field_name = self.domain_config.field_name
|
|
65
|
+
field_value = self.domain_config.field_value
|
|
66
|
+
if isinstance(field_value, list):
|
|
67
|
+
mask = data.obj[field_name].isin(field_value)
|
|
68
|
+
else:
|
|
69
|
+
mask = data.obj[field_name] == field_value
|
|
70
|
+
result = data.copy()
|
|
71
|
+
result.obj = data.obj.where(mask.compute(), drop=True)
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PolygonConfig(BaseDomainConfig):
|
|
76
|
+
"""PolygonConfig Pydantic model for the Polygon domain definition in the config file. The only argument is vertex which is a List of tuples representing vertices, or List of vertices (List of List of tuples)"""
|
|
77
|
+
|
|
78
|
+
type: Literal["polygon", "poly"]
|
|
79
|
+
vertex: list[tuple[float, float]] | list[list[tuple[float, float]]]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Polygon(Domain):
|
|
83
|
+
"""Polygon Domain specification by polygons specified with a list of vertices, or list of list of vertices (multipolygon)"""
|
|
84
|
+
|
|
85
|
+
config = PolygonConfig
|
|
86
|
+
aliases = ["polygon", "poly"]
|
|
87
|
+
|
|
88
|
+
def _filter(self, data: "Data") -> "Data":
|
|
89
|
+
"""Build polygons from the configured vertices and mask the data with them.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
data : Data
|
|
94
|
+
Data to be filtered by the polygons
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
Data
|
|
99
|
+
Data filtered by the polygon(s)
|
|
100
|
+
"""
|
|
101
|
+
vertex = self.domain_config.vertex
|
|
102
|
+
if isinstance(vertex[0], tuple):
|
|
103
|
+
vertex = [vertex]
|
|
104
|
+
polygons = []
|
|
105
|
+
|
|
106
|
+
for p in vertex:
|
|
107
|
+
polygons.append(gm.Polygon(p))
|
|
108
|
+
|
|
109
|
+
polygons = gm.MultiPolygon(polygons)
|
|
110
|
+
regions = regionmask.Regions([polygons])
|
|
111
|
+
|
|
112
|
+
# Regionmask requires "lat" and "lon"
|
|
113
|
+
region_mask = regions.mask(
|
|
114
|
+
data.obj.rename({"latitude": "lat", "longitude": "lon"})
|
|
115
|
+
)
|
|
116
|
+
region_mask = region_mask.rename({"lat": "latitude", "lon": "longitude"})
|
|
117
|
+
|
|
118
|
+
result = data.copy()
|
|
119
|
+
result.obj = data.obj.where(region_mask.notnull())
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class AllConfig(BaseDomainConfig):
|
|
124
|
+
"""AllConfig Pydantic model for the All domain — no filtering applied."""
|
|
125
|
+
|
|
126
|
+
type: Literal["all"]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class All(Domain):
|
|
130
|
+
"""All domain that returns data unchanged — explicit 'use all data' marker.
|
|
131
|
+
|
|
132
|
+
With ``resample_to`` set it becomes a pure reprojection (resample, no filter).
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
config = AllConfig
|
|
136
|
+
aliases = ["all"]
|
|
137
|
+
|
|
138
|
+
def _filter(self, data: "Data") -> "Data":
|
|
139
|
+
"""Return the data unchanged."""
|
|
140
|
+
return data
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class PointItem(BaseModel):
|
|
144
|
+
"""A single named point for an inline ``Points`` domain."""
|
|
145
|
+
|
|
146
|
+
name: str
|
|
147
|
+
lat: float
|
|
148
|
+
lon: float
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# Tolerated CSV header spellings for each logical column.
|
|
152
|
+
_POINT_COL_ALIASES = {
|
|
153
|
+
"name": ("name", "site", "station", "id"),
|
|
154
|
+
"lat": ("latitude", "lat", "latitud"),
|
|
155
|
+
"lon": ("longitude", "lon", "longitud"),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class PointsConfig(BaseDomainConfig):
|
|
160
|
+
"""PointsConfig Resample onto a set of named points given inline or via CSV.
|
|
161
|
+
|
|
162
|
+
The domain carries its own target geometry (the points), so ``resample_to`` is
|
|
163
|
+
unused; ``radius_of_influence``/``engine`` from the base still drive the resample.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
type: Literal["points", "pts"]
|
|
167
|
+
path: Path | None = None
|
|
168
|
+
"""CSV of named points (columns: name + latitude/longitude, aliases tolerated)."""
|
|
169
|
+
points: list[PointItem] | None = None
|
|
170
|
+
"""Inline alternative to ``path``: a list of ``{name, lat, lon}``."""
|
|
171
|
+
name_col: str = "name"
|
|
172
|
+
lat_col: str = "latitude"
|
|
173
|
+
lon_col: str = "longitude"
|
|
174
|
+
one_for_each: bool = False
|
|
175
|
+
"""Expand into one domain per point (named by the point) instead of one grouped
|
|
176
|
+
domain over all points."""
|
|
177
|
+
select_name: str | None = None
|
|
178
|
+
"""Internal: set by the parser on each fan-out expansion to the point to keep."""
|
|
179
|
+
|
|
180
|
+
@model_validator(mode="after")
|
|
181
|
+
def _exactly_one_source(self) -> "PointsConfig":
|
|
182
|
+
"""Require exactly one source of points: an inline list or a file."""
|
|
183
|
+
if (self.path is None) == (self.points is None):
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"Points domain needs exactly one of 'path' (CSV) or 'points' (inline)."
|
|
186
|
+
)
|
|
187
|
+
return self
|
|
188
|
+
|
|
189
|
+
def _resolve_col(self, columns, logical: str, configured: str) -> str:
|
|
190
|
+
"""Find the CSV column for a logical field (configured name, else an alias)."""
|
|
191
|
+
lower = {c.lower(): c for c in columns}
|
|
192
|
+
for candidate in (configured, *_POINT_COL_ALIASES[logical]):
|
|
193
|
+
if candidate.lower() in lower:
|
|
194
|
+
return lower[candidate.lower()]
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"Points CSV is missing a '{logical}' column "
|
|
197
|
+
f"(looked for {configured!r} or {_POINT_COL_ALIASES[logical]}); "
|
|
198
|
+
f"has {list(columns)}."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def resolve_points(self) -> list[tuple[str, float, float]]:
|
|
202
|
+
"""Return the points as ``[(name, lat, lon), ...]`` from inline or CSV."""
|
|
203
|
+
if self.points is not None:
|
|
204
|
+
return [(p.name, p.lat, p.lon) for p in self.points]
|
|
205
|
+
df = pd.read_csv(self.path)
|
|
206
|
+
name_c = self._resolve_col(df.columns, "name", self.name_col)
|
|
207
|
+
lat_c = self._resolve_col(df.columns, "lat", self.lat_col)
|
|
208
|
+
lon_c = self._resolve_col(df.columns, "lon", self.lon_col)
|
|
209
|
+
return [
|
|
210
|
+
(str(r[name_c]), float(r[lat_c]), float(r[lon_c])) for _, r in df.iterrows()
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class Points(Domain):
|
|
215
|
+
"""Points domain: resample any dataset onto a fixed set of named points.
|
|
216
|
+
|
|
217
|
+
The parser builds a ``PointSurface`` target from the config's points and injects
|
|
218
|
+
it as the resample target, so the base ``apply`` template reprojects onto the
|
|
219
|
+
points and then ``_filter`` runs. Grouped (``select_name`` None) keeps every
|
|
220
|
+
point; a fan-out expansion selects its single named point.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
config = PointsConfig
|
|
224
|
+
aliases = ["points", "pts"]
|
|
225
|
+
|
|
226
|
+
def _filter(self, data: "Data") -> "Data":
|
|
227
|
+
"""Select a single site when this domain is one of a fan-out expansion.
|
|
228
|
+
|
|
229
|
+
The resampling onto the point set has already happened in ``apply``.
|
|
230
|
+
Without ``select_name`` the whole point set is kept.
|
|
231
|
+
"""
|
|
232
|
+
sel = self.domain_config.select_name
|
|
233
|
+
if sel is None:
|
|
234
|
+
return data
|
|
235
|
+
result = data.copy()
|
|
236
|
+
result.obj = data.obj.sel(site=[sel])
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class ShapefileConfig(BaseDomainConfig):
|
|
241
|
+
"""ShapefileConfig Pydantic model for the Polygon domain definition in the config file. Receives a local path to the shapefile, and a value for filtering."""
|
|
242
|
+
|
|
243
|
+
type: Literal["shapefile", "shp"]
|
|
244
|
+
path: Path
|
|
245
|
+
field_value: Any
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class Shapefile(Domain):
|
|
249
|
+
"""Shapefile domain defined by an imported shapefile and a field value inside of it."""
|
|
250
|
+
|
|
251
|
+
config = ShapefileConfig
|
|
252
|
+
aliases = ["shapefile", "shp"]
|
|
253
|
+
|
|
254
|
+
def _filter(self, data: "Data") -> "Data":
|
|
255
|
+
"""Build a region mask from the shapefile and apply it to the data.
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
data : Data
|
|
260
|
+
Data to be filtered by the shapefile.
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
Data
|
|
265
|
+
Data filtered by the shapefile
|
|
266
|
+
"""
|
|
267
|
+
path = self.domain_config.path
|
|
268
|
+
field_value = self.domain_config.field_value
|
|
269
|
+
|
|
270
|
+
regions = regionmask.from_geopandas(gpd.read_file(path))
|
|
271
|
+
|
|
272
|
+
region_mask = regions.mask(
|
|
273
|
+
data.obj.rename({"latitude": "lat", "longitude": "lon"})
|
|
274
|
+
)
|
|
275
|
+
region_mask = region_mask.rename({"lat": "latitude", "lon": "longitude"})
|
|
276
|
+
|
|
277
|
+
result = data.copy()
|
|
278
|
+
result.obj = data.obj.where(region_mask == field_value)
|
|
279
|
+
return result
|