gmpas 0.4.2__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.
- gmpas/__init__.py +49 -0
- gmpas/accessor.py +139 -0
- gmpas/cli.py +811 -0
- gmpas/config.py +333 -0
- gmpas/dashboard.py +262 -0
- gmpas/data.py +101 -0
- gmpas/mesh.py +697 -0
- gmpas/paths.py +54 -0
- gmpas/plot.py +279 -0
- gmpas/prep/__init__.py +26 -0
- gmpas/prep/generate.py +510 -0
- gmpas/prep/hfun.py +279 -0
- gmpas/prep/hfunview.py +263 -0
- gmpas/prep/layout.py +428 -0
- gmpas/prep/meshview.py +224 -0
- gmpas/py.typed +0 -0
- gmpas/raster.py +103 -0
- gmpas/remap.py +496 -0
- gmpas/scrip.py +160 -0
- gmpas/series.py +260 -0
- gmpas/style.py +88 -0
- gmpas/viewer.py +1167 -0
- gmpas-0.4.2.dist-info/METADATA +121 -0
- gmpas-0.4.2.dist-info/RECORD +27 -0
- gmpas-0.4.2.dist-info/WHEEL +4 -0
- gmpas-0.4.2.dist-info/entry_points.txt +2 -0
- gmpas-0.4.2.dist-info/licenses/LICENSE +21 -0
gmpas/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""gmpas — fast native-mesh plotting for MPAS output.
|
|
2
|
+
|
|
3
|
+
MPAS is unstructured: a variable-resolution spherical Voronoi tessellation with
|
|
4
|
+
scalars at cell centres and normal velocity on cell edges. gmpas draws it on
|
|
5
|
+
that mesh directly, with no regridding, so refinement survives into the figure
|
|
6
|
+
exactly as the model carries it.
|
|
7
|
+
|
|
8
|
+
import gmpas
|
|
9
|
+
|
|
10
|
+
ds = gmpas.open_mpas("diag.2019-09-01_00.00.00.nc", mesh="maritime.region.nc")
|
|
11
|
+
ds.mpas.plot("mslp") # cell field on the native Voronoi mesh
|
|
12
|
+
ds.mpas.plot("u") # edge field, drawn on the cell faces
|
|
13
|
+
ds.mpas.plot_mesh() # where the mesh refines
|
|
14
|
+
|
|
15
|
+
Depends on numpy, scipy, xarray and netCDF4; matplotlib and cartopy are only
|
|
16
|
+
needed to actually draw, and are imported lazily. It does not use uxarray
|
|
17
|
+
(which rebuilds grid topology on every call) or geopandas (the .npz geometry
|
|
18
|
+
cache replaces a .gpkg and also carries edge geometry).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from .accessor import MpasAccessor, open_mpas
|
|
24
|
+
from .data import field_label, open_data, plottable, select, spatial_dim
|
|
25
|
+
from .mesh import MpasMesh, has_mesh, reconstruct_cell_winds
|
|
26
|
+
from .paths import cache_dir, data_dir, resolve_path
|
|
27
|
+
from .plot import cell_field, edge_field, mesh_structure, vectors
|
|
28
|
+
from .raster import RASTER_THRESHOLD, rasterize, should_raster, target_grid
|
|
29
|
+
from .style import CMAPS, EXTENTS, Style, resolve_extent, save_figure
|
|
30
|
+
|
|
31
|
+
# kept in step with pyproject.toml by test_version.py -- `gmpas --version` and
|
|
32
|
+
# the built wheel disagreeing is the kind of thing only noticed after a release
|
|
33
|
+
__version__ = "0.4.2"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
# entry points
|
|
37
|
+
"open_mpas", "open_data", "MpasMesh", "MpasAccessor",
|
|
38
|
+
# rendering
|
|
39
|
+
"cell_field", "edge_field", "vectors", "mesh_structure", "save_figure",
|
|
40
|
+
# geometry / sampling
|
|
41
|
+
"rasterize", "target_grid", "should_raster", "RASTER_THRESHOLD",
|
|
42
|
+
"reconstruct_cell_winds",
|
|
43
|
+
# data access
|
|
44
|
+
"select", "spatial_dim", "field_label", "plottable", "has_mesh",
|
|
45
|
+
# configuration
|
|
46
|
+
"Style", "EXTENTS", "CMAPS", "resolve_extent",
|
|
47
|
+
"cache_dir", "data_dir", "resolve_path",
|
|
48
|
+
"__version__",
|
|
49
|
+
]
|
gmpas/accessor.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""An xarray accessor for MPAS output, so plotting is one line.
|
|
2
|
+
|
|
3
|
+
xarray gives you `ds.t2m.plot()` because it knows the field's coordinates. MPAS
|
|
4
|
+
output has no such luxury: a `diag.*.nc` is a bare (Time, nCells) array with the
|
|
5
|
+
geometry living in a separate mesh file. This accessor supplies the missing
|
|
6
|
+
half, then gets out of the way::
|
|
7
|
+
|
|
8
|
+
import gmpas
|
|
9
|
+
ds = gmpas.open_mpas("diag.2019-09-01_00.00.00.nc", mesh="maritime.region.nc")
|
|
10
|
+
|
|
11
|
+
ds.mpas.plot("mslp") # cell field, native Voronoi mesh
|
|
12
|
+
ds.mpas.plot("u") # edge field -- drawn on the faces
|
|
13
|
+
ds.mpas.plot("mslp", time=3)
|
|
14
|
+
ds.mpas.plot_mesh() # where the mesh refines
|
|
15
|
+
|
|
16
|
+
The mesh is loaded once per dataset and cached on disk between sessions, so the
|
|
17
|
+
second plot -- and every plot in every later session -- skips the build.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import xarray as xr
|
|
23
|
+
|
|
24
|
+
from . import data as _data
|
|
25
|
+
from . import plot as _plot
|
|
26
|
+
from .mesh import MpasMesh, has_mesh
|
|
27
|
+
from .paths import resolve_path
|
|
28
|
+
|
|
29
|
+
MESH_ATTR = "gmpas_mesh_path"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def open_mpas(data_path: str, mesh: str = "", **kwargs) -> xr.Dataset:
|
|
33
|
+
"""Open MPAS output with its mesh attached, ready for `.mpas.plot(...)`.
|
|
34
|
+
|
|
35
|
+
`mesh` may be omitted when the file carries its own mesh information, or
|
|
36
|
+
when a mesh file with a matching cell count sits in the same directory.
|
|
37
|
+
"""
|
|
38
|
+
kwargs.setdefault("decode_timedelta", False)
|
|
39
|
+
kwargs.setdefault("engine", "netcdf4")
|
|
40
|
+
path = resolve_path(data_path)
|
|
41
|
+
ds = xr.open_dataset(path, **kwargs)
|
|
42
|
+
|
|
43
|
+
if mesh:
|
|
44
|
+
ds.attrs[MESH_ATTR] = str(resolve_path(mesh))
|
|
45
|
+
elif has_mesh(ds):
|
|
46
|
+
ds.attrs[MESH_ATTR] = str(path)
|
|
47
|
+
else:
|
|
48
|
+
found = _data.find_mesh_beside(path, int(ds.sizes.get("nCells", -1)))
|
|
49
|
+
if found is not None:
|
|
50
|
+
ds.attrs[MESH_ATTR] = str(found)
|
|
51
|
+
return ds
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@xr.register_dataset_accessor("mpas")
|
|
55
|
+
class MpasAccessor:
|
|
56
|
+
"""`ds.mpas` -- native-mesh plotting for an MPAS dataset."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, ds: xr.Dataset):
|
|
59
|
+
self._ds = ds
|
|
60
|
+
self._mesh: MpasMesh | None = None
|
|
61
|
+
|
|
62
|
+
# -- mesh ------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def mesh(self) -> MpasMesh:
|
|
66
|
+
"""The mesh backing this dataset, loaded once and cached."""
|
|
67
|
+
if self._mesh is None:
|
|
68
|
+
path = self._ds.attrs.get(MESH_ATTR)
|
|
69
|
+
if not path:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"No mesh attached to this dataset. Open it with "
|
|
72
|
+
"gmpas.open_mpas(path, mesh=...), or call "
|
|
73
|
+
"ds.mpas.use_mesh('mesh.nc')."
|
|
74
|
+
)
|
|
75
|
+
self._mesh = MpasMesh.load(path)
|
|
76
|
+
return self._mesh
|
|
77
|
+
|
|
78
|
+
def use_mesh(self, mesh_path: str) -> "MpasAccessor":
|
|
79
|
+
"""Attach (or replace) the mesh for this dataset."""
|
|
80
|
+
self._ds.attrs[MESH_ATTR] = str(resolve_path(mesh_path))
|
|
81
|
+
self._mesh = None
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
# -- introspection ---------------------------------------------------
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def variables(self) -> dict[str, list[str]]:
|
|
88
|
+
"""Plottable variables grouped by mesh element."""
|
|
89
|
+
return _data.plottable(self._ds)
|
|
90
|
+
|
|
91
|
+
def __repr__(self) -> str:
|
|
92
|
+
try:
|
|
93
|
+
m = self.mesh
|
|
94
|
+
head = f"<MPAS {m.n_cells:,} cells / {m.n_edges:,} edges — {m.path.name}>"
|
|
95
|
+
except ValueError:
|
|
96
|
+
head = "<MPAS dataset (no mesh attached)>"
|
|
97
|
+
groups = self.variables
|
|
98
|
+
counts = ", ".join(f"{len(v)} on {k}" for k, v in groups.items() if v)
|
|
99
|
+
return f"{head}\n {counts}"
|
|
100
|
+
|
|
101
|
+
# -- plotting --------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def plot(self, variable: str, time: int = 0, level: int = 0, **kwargs):
|
|
104
|
+
"""Plot a variable on the native mesh, dispatching on where it lives.
|
|
105
|
+
|
|
106
|
+
Cell fields fill the Voronoi polygons; edge fields are drawn on the cell
|
|
107
|
+
faces they belong to. Returns (fig, ax) like a matplotlib call.
|
|
108
|
+
"""
|
|
109
|
+
if variable not in self._ds:
|
|
110
|
+
raise KeyError(
|
|
111
|
+
f"{variable!r} not in dataset. Available: {self.variables}"
|
|
112
|
+
)
|
|
113
|
+
da = self._ds[variable]
|
|
114
|
+
dim = _data.spatial_dim(da)
|
|
115
|
+
values = _data.select(da, time=time, level=level)
|
|
116
|
+
kwargs.setdefault("label", _data.field_label(da))
|
|
117
|
+
kwargs.setdefault("title", variable)
|
|
118
|
+
|
|
119
|
+
if dim == "nCells":
|
|
120
|
+
return _plot.cell_field(self.mesh, values, **kwargs)
|
|
121
|
+
if dim == "nEdges":
|
|
122
|
+
return _plot.edge_field(self.mesh, values, **kwargs)
|
|
123
|
+
raise NotImplementedError(
|
|
124
|
+
f"{variable!r} lives on {dim}; only cell and edge fields are "
|
|
125
|
+
f"plotted directly. Vertex fields need the dual triangle mesh."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def quiver(self, u: str, v: str, time: int = 0, level: int = 0,
|
|
129
|
+
background: str = "", **kwargs):
|
|
130
|
+
"""Quiver cell-centred vector components, optionally over a filled field."""
|
|
131
|
+
uu = _data.select(self._ds[u], time=time, level=level)
|
|
132
|
+
vv = _data.select(self._ds[v], time=time, level=level)
|
|
133
|
+
bg = _data.select(self._ds[background], time=time, level=level) \
|
|
134
|
+
if background else None
|
|
135
|
+
return _plot.vectors(self.mesh, uu, vv, background=bg, **kwargs)
|
|
136
|
+
|
|
137
|
+
def plot_mesh(self, **kwargs):
|
|
138
|
+
"""Show the mesh itself, cells coloured by width in km."""
|
|
139
|
+
return _plot.mesh_structure(self.mesh, **kwargs)
|