planetmodel 1.3.0__tar.gz → 1.3.2__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.
- {planetmodel-1.3.0 → planetmodel-1.3.2}/PKG-INFO +1 -1
- {planetmodel-1.3.0 → planetmodel-1.3.2}/pyproject.toml +1 -1
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/__init__.py +3 -1
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/modelfile.py +76 -49
- planetmodel-1.3.2/src/planetmodel/surfacefile.py +631 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/testing.py +110 -3
- {planetmodel-1.3.0 → planetmodel-1.3.2}/LICENSE +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/README.md +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/behaviours.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/catalogue.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/character.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/conversion/__init__.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/conversion/building.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/conversion/laws.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/conversion/lithosphere.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/conversion/parameters.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/conversion/profiles.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/conversion/prony.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/conversion/tables.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/__init__.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/building.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/coefficients.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/decks.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/entries.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/fetch.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/grids.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/models.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/processing.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/projections.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/registry.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/sph.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/strata.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/datasets/surfaces.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/deck.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/displacement.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/expansions.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/fields.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/frames.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/geometry.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/gridded.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/harmonics.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/lateral.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/layerfunction.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mapping.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/materials.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/means.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh1d/__init__.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh1d/gll.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh1d/gravity.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh1d/mesh.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/__init__.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/_geometry.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/_orient.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/_session.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/_sizing.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/_tagging.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/_validate.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/_writer.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/export.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/layered.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/manifest.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/offset.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/mesh3d/spec.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/model.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/perturbation.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/plotting/__init__.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/plotting/maps.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/plotting/profiles.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/plotting/sections.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/plotting/spectra.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/plotting/style.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/plotting/vtk.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/pushforward.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/randomfield/__init__.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/randomfield/basis.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/randomfield/fields.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/randomfield/mesh.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/randomfield/operator.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/rheology.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/sampling.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/skeleton.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/slices.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/units.py +0 -0
- {planetmodel-1.3.0 → planetmodel-1.3.2}/src/planetmodel/vocabulary.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "planetmodel"
|
|
3
|
-
version = "1.3.
|
|
3
|
+
version = "1.3.2"
|
|
4
4
|
description = "Spherically layered planetary models: reference bodies, fields, mappings and meshes"
|
|
5
5
|
keywords = ["geophysics", "seismology", "planetary models", "PREM", "spectral elements", "meshing"]
|
|
6
6
|
classifiers = [
|
|
@@ -22,6 +22,8 @@ Modules reached by name, one level down:
|
|
|
22
22
|
sampling a model on a radial mesh times an angular grid
|
|
23
23
|
lateral values between the nodes of an angular grid: the lateral rules
|
|
24
24
|
modelfile the model file: a model in one netCDF file (netCDF4, optional)
|
|
25
|
+
surfacefile the surface file: fields on one spherical surface, static or
|
|
26
|
+
in time (netCDF4, optional)
|
|
25
27
|
harmonics real spherical harmonics, through ducc0
|
|
26
28
|
slices a field on a sphere, a section or a profile; spectra and
|
|
27
29
|
correlations by degree
|
|
@@ -127,7 +129,7 @@ from .units import EARTH_MEAN_DENSITY, G_SI, Dimensions, Scales
|
|
|
127
129
|
from .vocabulary import CONSTANTS, VOCABULARY, Constant, FieldSpec
|
|
128
130
|
from . import testing # noqa: F401
|
|
129
131
|
|
|
130
|
-
__version__ = "1.3.
|
|
132
|
+
__version__ = "1.3.2"
|
|
131
133
|
|
|
132
134
|
__all__ = [
|
|
133
135
|
"Skeleton",
|
|
@@ -8,7 +8,9 @@ definition of the format and the reference the C++ readers are held to;
|
|
|
8
8
|
`docs/source/formats/model_file.rst` describes it for consumers.
|
|
9
9
|
|
|
10
10
|
**Global attributes**: `schema` (`SCHEMA`, checked by every reader),
|
|
11
|
-
`Conventions`, the
|
|
11
|
+
`Conventions`, `source` (the writing code and version) and `created`
|
|
12
|
+
(when, ISO 8601) — both stamped by the writers, and empty on reading a
|
|
13
|
+
file from before they were — the model's `name`, `scale_length`, `scale_mass`,
|
|
12
14
|
`scale_time` and `scale_temperature` (what one stored unit is in SI),
|
|
13
15
|
`rtol` (the geometry's tolerance, the default where a file has none),
|
|
14
16
|
`constant_<name>` for every constant in the model's units, `citation`
|
|
@@ -130,7 +132,9 @@ __all__ = ["SCHEMA", "CONVENTIONS", "DEFAULT_INTERPOLATION", "Grids",
|
|
|
130
132
|
"Interpolation", "Surfaces", "FileVariable",
|
|
131
133
|
"FilePart", "FileGeometry", "FileSurfaces", "ModelFile", "write_model",
|
|
132
134
|
"read_model_file", "file_layers", "file_geometry", "file_surfaces",
|
|
133
|
-
"read_model", "read_surfaces", "validate_structure"
|
|
135
|
+
"read_model", "read_surfaces", "validate_structure",
|
|
136
|
+
"netcdf_module", "put_variable", "variable_attributes",
|
|
137
|
+
"variable_values", "grid_degrees", "grid_of_group"]
|
|
134
138
|
|
|
135
139
|
#: The schema string every reader checks; bumped only for an incompatible change.
|
|
136
140
|
SCHEMA = "planetmodel.model/1"
|
|
@@ -171,7 +175,7 @@ type Grids = AngularGrid | Mapping[int | str, AngularGrid]
|
|
|
171
175
|
type Surfaces = Mapping[str, Mapping[int | str, Relief]]
|
|
172
176
|
|
|
173
177
|
|
|
174
|
-
def
|
|
178
|
+
def netcdf_module() -> ModuleType:
|
|
175
179
|
"""netCDF4, imported here and nowhere else in planetmodel."""
|
|
176
180
|
try:
|
|
177
181
|
import netCDF4
|
|
@@ -254,6 +258,12 @@ class ModelFile:
|
|
|
254
258
|
surfaces: FileSurfaces | None = None
|
|
255
259
|
#: The works the model cites, one a line of the attribute `citation`.
|
|
256
260
|
citation: tuple[str, ...] = ()
|
|
261
|
+
#: The code that wrote the file (the attribute `source`); empty in
|
|
262
|
+
#: files written before the writers stamped it.
|
|
263
|
+
source: str = ""
|
|
264
|
+
#: When the file was written, ISO 8601 (the attribute `created`);
|
|
265
|
+
#: empty in files written before the writers stamped it.
|
|
266
|
+
created: str = ""
|
|
257
267
|
|
|
258
268
|
def part_of(self, layer: int) -> FilePart:
|
|
259
269
|
"""The part holding `layer`."""
|
|
@@ -463,7 +473,7 @@ def _radial_layers(attributes: Mapping[str, Any]) -> set[int]:
|
|
|
463
473
|
return {int(i) for i in np.atleast_1d(attributes.get("radial_layers", []))}
|
|
464
474
|
|
|
465
475
|
|
|
466
|
-
def
|
|
476
|
+
def put_variable(group: Any, name: str, dims: tuple[str, ...], values: np.ndarray,
|
|
467
477
|
attributes: Mapping[str, Any]) -> None:
|
|
468
478
|
"""A float64 variable with NaN as its fill, and its attributes."""
|
|
469
479
|
var = group.createVariable(name, "f8", dims, fill_value=np.nan, **_ZLIB)
|
|
@@ -484,7 +494,7 @@ def _axes(group: Any, character: Character) -> tuple[str, ...]:
|
|
|
484
494
|
return _AXES[shape]
|
|
485
495
|
|
|
486
496
|
|
|
487
|
-
def
|
|
497
|
+
def grid_degrees(grid: AngularGrid) -> tuple[np.ndarray, np.ndarray]:
|
|
488
498
|
"""The grid's latitudes and longitudes in degrees, each on its own axis."""
|
|
489
499
|
lat, _ = geographic(grid.colatitudes, np.zeros_like(grid.colatitudes))
|
|
490
500
|
_, lon = geographic(np.full_like(grid.longitudes, 0.5 * np.pi), grid.longitudes)
|
|
@@ -508,16 +518,16 @@ def _write_part(group: Any, model: Model, index: int, layers: list[int],
|
|
|
508
518
|
length = scaled_unit(Dimensions(length=1), model.scales)
|
|
509
519
|
r = mesh.r.ravel()
|
|
510
520
|
node_layer = np.repeat(mesh.layer, mesh.ngll)
|
|
511
|
-
|
|
521
|
+
put_variable(group, "radius", ("node",), r,
|
|
512
522
|
{"long_name": "radius of the node; an element boundary is two nodes",
|
|
513
523
|
"units": length})
|
|
514
524
|
var = group.createVariable("element_layer", "i4", ("element",))
|
|
515
525
|
var[...] = mesh.layer
|
|
516
526
|
var.long_name = "skeleton layer of the element"
|
|
517
|
-
|
|
527
|
+
put_variable(group, "element_bounds", ("element", "bound"),
|
|
518
528
|
np.stack([mesh.left, mesh.right], axis=-1),
|
|
519
529
|
{"long_name": "radii of the element's ends", "units": length})
|
|
520
|
-
lat, lon =
|
|
530
|
+
lat, lon = grid_degrees(grid)
|
|
521
531
|
var = group.createVariable("latitude", "f8", ("latitude",))
|
|
522
532
|
var[...] = lat
|
|
523
533
|
var.units, var.standard_name = "degrees_north", "latitude"
|
|
@@ -527,7 +537,7 @@ def _write_part(group: Any, model: Model, index: int, layers: list[int],
|
|
|
527
537
|
var.units, var.standard_name = "degrees_east", "longitude"
|
|
528
538
|
var.long_name = "longitude"
|
|
529
539
|
if grid.weights is not None:
|
|
530
|
-
|
|
540
|
+
put_variable(group, "colatitude_weights", ("latitude",), grid.weights,
|
|
531
541
|
{"long_name": "colatitude quadrature weights against sin(theta) dtheta",
|
|
532
542
|
"units": "1"})
|
|
533
543
|
lacks_pole = not all(grid.pole_nodes)
|
|
@@ -568,9 +578,9 @@ def _write_part(group: Any, model: Model, index: int, layers: list[int],
|
|
|
568
578
|
attrs["coordinates"] = "radius"
|
|
569
579
|
if not radial:
|
|
570
580
|
_mark_radial_layers(attrs, fields)
|
|
571
|
-
|
|
581
|
+
put_variable(group, name, dims + axes, values, attrs)
|
|
572
582
|
if poles is not None:
|
|
573
|
-
|
|
583
|
+
put_variable(group, f"{name}_poles", ("node", "pole") + axes, poles,
|
|
574
584
|
{"long_name": f"{name} at the north and south poles, Cartesian "
|
|
575
585
|
"components", "units": attrs["units"]})
|
|
576
586
|
if form == "field":
|
|
@@ -588,12 +598,12 @@ def _write_part(group: Any, model: Model, index: int, layers: list[int],
|
|
|
588
598
|
for b in BASE], dtype=np.int32),
|
|
589
599
|
"radial_interpolation": rules[0],
|
|
590
600
|
"lateral_interpolation": rules[1], "coordinates": "radius"}
|
|
591
|
-
|
|
601
|
+
put_variable(group, "displacement", ("node", "latitude", "longitude") + axes,
|
|
592
602
|
values, attrs)
|
|
593
603
|
if lacks_pole:
|
|
594
604
|
from .frames import cartesian_points
|
|
595
605
|
P = cartesian_points(r[:, None], np.array([0.0, np.pi]), np.zeros(2))
|
|
596
|
-
|
|
606
|
+
put_variable(group, "displacement_poles", ("node", "pole") + axes,
|
|
597
607
|
np.asarray(mapping(P), dtype=float) - P,
|
|
598
608
|
{"long_name": "displacement at the north and south poles, "
|
|
599
609
|
"Cartesian components", "units": length})
|
|
@@ -636,7 +646,7 @@ def _write_perturbed(group: Any, model: Model, name: str,
|
|
|
636
646
|
attrs = _attributes(name, model, char, holders, ref_rules, lateral=False)
|
|
637
647
|
attrs.update({"perturbation": f"{name}_perturbation", "perturbation_kind": kind,
|
|
638
648
|
"coordinates": "radius"})
|
|
639
|
-
|
|
649
|
+
put_variable(group, name, ("node",), ref, attrs)
|
|
640
650
|
perturbed = [i for i, f in fields.items()
|
|
641
651
|
if getattr(f, "reference", None) is not None]
|
|
642
652
|
pattrs = _attributes(name, model, pert_char, perturbed, rules,
|
|
@@ -650,9 +660,9 @@ def _write_perturbed(group: Any, model: Model, name: str,
|
|
|
650
660
|
_mark_radial_layers(pattrs, {i: f.perturbation for i, f in fields.items()
|
|
651
661
|
if getattr(f, "reference", None) is not None})
|
|
652
662
|
dims = ("node",) if pert_radial else ("node", "latitude", "longitude")
|
|
653
|
-
|
|
663
|
+
put_variable(group, f"{name}_perturbation", dims, dp, pattrs)
|
|
654
664
|
if poles is not None:
|
|
655
|
-
|
|
665
|
+
put_variable(group, f"{name}_perturbation_poles", ("node", "pole"), poles,
|
|
656
666
|
{"long_name": f"{kind} perturbation of {name} at the poles",
|
|
657
667
|
"units": pattrs["units"]})
|
|
658
668
|
|
|
@@ -685,10 +695,10 @@ def _write_stack(root: Any, model: Model, parts: list[tuple[list[int], AngularGr
|
|
|
685
695
|
if grid.lmax is not None:
|
|
686
696
|
g.setncattr("grid_lmax", np.int32(grid.lmax))
|
|
687
697
|
length = scaled_unit(Dimensions(length=1), model.scales)
|
|
688
|
-
|
|
698
|
+
put_variable(g, "boundary_radius", ("boundary",), radii,
|
|
689
699
|
{"long_name": "radius of the surface the displacement belongs to",
|
|
690
700
|
"units": length})
|
|
691
|
-
lat, lon =
|
|
701
|
+
lat, lon = grid_degrees(grid)
|
|
692
702
|
var = g.createVariable("latitude", "f8", ("latitude",))
|
|
693
703
|
var[...] = lat
|
|
694
704
|
var.units, var.standard_name = "degrees_north", "latitude"
|
|
@@ -696,7 +706,8 @@ def _write_stack(root: Any, model: Model, parts: list[tuple[list[int], AngularGr
|
|
|
696
706
|
var[...] = lon
|
|
697
707
|
var.units, var.standard_name = "degrees_east", "longitude"
|
|
698
708
|
if grid.weights is not None:
|
|
699
|
-
|
|
709
|
+
put_variable(g, "colatitude_weights", ("latitude",), grid.weights,
|
|
710
|
+
{"units": "1"})
|
|
700
711
|
values = np.zeros((radii.size, grid.ntheta, grid.nphi))
|
|
701
712
|
poles = np.zeros((radii.size, 2))
|
|
702
713
|
for k, rel in enumerate(reliefs):
|
|
@@ -706,12 +717,13 @@ def _write_stack(root: Any, model: Model, parts: list[tuple[list[int], AngularGr
|
|
|
706
717
|
np.asarray(rel(grid.colatitudes[:, None], grid.longitudes[None, :]),
|
|
707
718
|
dtype=float), (grid.ntheta, grid.nphi))
|
|
708
719
|
poles[k] = np.asarray(rel(np.array([0.0, np.pi]), np.zeros(2)), dtype=float)
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
720
|
+
put_variable(g, "boundary_displacement",
|
|
721
|
+
("boundary", "latitude", "longitude"), values,
|
|
722
|
+
{"long_name": "radial displacement of the surface", "units": length,
|
|
723
|
+
"lateral_interpolation": lateral, "between": "linear"})
|
|
712
724
|
if not all(grid.pole_nodes):
|
|
713
725
|
g.createDimension("pole", 2)
|
|
714
|
-
|
|
726
|
+
put_variable(g, "boundary_displacement_poles", ("boundary", "pole"), poles,
|
|
715
727
|
{"long_name": "radial displacement at the north and south poles",
|
|
716
728
|
"units": length})
|
|
717
729
|
|
|
@@ -788,7 +800,7 @@ def _write_surfaces(root: Any, model: Model, held: Mapping[str, Mapping[int, Rel
|
|
|
788
800
|
var = g.createVariable("surface_interface", "i4", ("surface",))
|
|
789
801
|
var[...] = np.asarray(faces, dtype=np.int32)
|
|
790
802
|
var.long_name = "the interface the surface is, by its index at the root"
|
|
791
|
-
lat, lon =
|
|
803
|
+
lat, lon = grid_degrees(grid)
|
|
792
804
|
var = g.createVariable("latitude", "f8", ("latitude",))
|
|
793
805
|
var[...] = lat
|
|
794
806
|
var.units, var.standard_name = "degrees_north", "latitude"
|
|
@@ -798,7 +810,7 @@ def _write_surfaces(root: Any, model: Model, held: Mapping[str, Mapping[int, Rel
|
|
|
798
810
|
var.units, var.standard_name = "degrees_east", "longitude"
|
|
799
811
|
var.long_name = "longitude"
|
|
800
812
|
if grid.weights is not None:
|
|
801
|
-
|
|
813
|
+
put_variable(g, "colatitude_weights", ("latitude",), grid.weights,
|
|
802
814
|
{"long_name": "colatitude quadrature weights against sin(theta) dtheta",
|
|
803
815
|
"units": "1"})
|
|
804
816
|
lacks_pole = not all(grid.pole_nodes)
|
|
@@ -828,9 +840,9 @@ def _write_surfaces(root: Any, model: Model, held: Mapping[str, Mapping[int, Rel
|
|
|
828
840
|
if dims is not None:
|
|
829
841
|
attrs["dimensions"] = np.asarray([getattr(dims, b) for b in BASE],
|
|
830
842
|
dtype=np.int32)
|
|
831
|
-
|
|
843
|
+
put_variable(g, name, ("surface", "latitude", "longitude"), values, attrs)
|
|
832
844
|
if lacks_pole:
|
|
833
|
-
|
|
845
|
+
put_variable(g, f"{name}_poles", ("surface", "pole"), poles,
|
|
834
846
|
{"long_name": f"{name} at the north and south poles",
|
|
835
847
|
"units": attrs["units"]})
|
|
836
848
|
|
|
@@ -841,7 +853,8 @@ def write_model(path: str | os.PathLike[str], model: Model, *, grid: Grids,
|
|
|
841
853
|
interpolation: Interpolation | None = None,
|
|
842
854
|
meta: Mapping[str, Any] | None = None,
|
|
843
855
|
surfaces: Surfaces | None = None,
|
|
844
|
-
surface_grid: AngularGrid | None = None
|
|
856
|
+
surface_grid: AngularGrid | None = None,
|
|
857
|
+
source: str | None = None) -> Path:
|
|
845
858
|
"""Write `model` to a model file at `path`, which is returned.
|
|
846
859
|
|
|
847
860
|
`grid` is one `AngularGrid` for every layer, or a mapping from layers
|
|
@@ -858,13 +871,15 @@ def write_model(path: str | os.PathLike[str], model: Model, *, grid: Grids,
|
|
|
858
871
|
`surfaces` on `surface_grid`: by default the grid the functions are
|
|
859
872
|
held on where they share one, else the outermost part's. A surface
|
|
860
873
|
field's dimensions and meaning are the model's spec of its name, and
|
|
861
|
-
`interpolation` names its lateral rule as for a field.
|
|
874
|
+
`interpolation` names its lateral rule as for a field. `source`
|
|
875
|
+
names the writing code and version in the attribute of that name,
|
|
876
|
+
this library and its version unless given. The model is
|
|
862
877
|
written in its own units.
|
|
863
878
|
"""
|
|
864
879
|
from .model import Model
|
|
865
880
|
if not isinstance(model, Model):
|
|
866
881
|
raise TypeError(f"expected a Model, got {type(model).__name__}")
|
|
867
|
-
nc =
|
|
882
|
+
nc = netcdf_module()
|
|
868
883
|
interpolation = dict(interpolation or {})
|
|
869
884
|
names = model.field_names() if fields is None else tuple(fields)
|
|
870
885
|
if isinstance(names, str):
|
|
@@ -901,7 +916,7 @@ def write_model(path: str | os.PathLike[str], model: Model, *, grid: Grids,
|
|
|
901
916
|
os.close(fd)
|
|
902
917
|
try:
|
|
903
918
|
_write_file(nc, Path(partial), model, parts, form, names, radial,
|
|
904
|
-
interpolation, meta, held, on)
|
|
919
|
+
interpolation, meta, held, on, source)
|
|
905
920
|
os.replace(partial, path)
|
|
906
921
|
finally:
|
|
907
922
|
if os.path.exists(partial):
|
|
@@ -914,12 +929,19 @@ def _write_file(nc: ModuleType, path: Path, model: Model,
|
|
|
914
929
|
names: Sequence[str], radial: RadialMesh | None,
|
|
915
930
|
interpolation: Interpolation, meta: Mapping[str, Any],
|
|
916
931
|
held: Mapping[str, Mapping[int, Relief]],
|
|
917
|
-
surface_grid: AngularGrid | None
|
|
932
|
+
surface_grid: AngularGrid | None,
|
|
933
|
+
source: str | None) -> None:
|
|
918
934
|
"""The file of `write_model`, at a path of its own until it is whole."""
|
|
935
|
+
from datetime import datetime, timezone
|
|
936
|
+
|
|
937
|
+
from . import __version__
|
|
919
938
|
with nc.Dataset(path, "w", format="NETCDF4") as root:
|
|
920
939
|
root.setncattr("schema", SCHEMA)
|
|
921
940
|
root.setncattr("Conventions", CONVENTIONS)
|
|
922
941
|
root.setncattr("title", f"planetmodel model {model.name or ''}".strip())
|
|
942
|
+
root.setncattr("source", source or f"planetmodel {__version__}")
|
|
943
|
+
root.setncattr("created",
|
|
944
|
+
datetime.now(timezone.utc).isoformat(timespec="seconds"))
|
|
923
945
|
if model.name:
|
|
924
946
|
root.setncattr("name", model.name)
|
|
925
947
|
for base in ("length", "mass", "time", "temperature"):
|
|
@@ -941,14 +963,15 @@ def _write_file(nc: ModuleType, path: Path, model: Model,
|
|
|
941
963
|
root.createDimension("bound", 2)
|
|
942
964
|
b = model.skeleton.boundaries
|
|
943
965
|
length = scaled_unit(Dimensions(length=1), model.scales)
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
966
|
+
put_variable(root, "layer_bounds", ("layer", "bound"),
|
|
967
|
+
np.stack([b[:-1], b[1:]], -1),
|
|
968
|
+
{"long_name": "radii of the layer's inner and outer boundaries",
|
|
969
|
+
"units": length})
|
|
947
970
|
var = root.createVariable("layer_name", str, ("layer",))
|
|
948
971
|
for i, layer in enumerate(model.geometry.layers):
|
|
949
972
|
var[i] = layer.name or ""
|
|
950
973
|
faces = model.geometry.interfaces
|
|
951
|
-
|
|
974
|
+
put_variable(root, "interface_radius", ("interface",),
|
|
952
975
|
np.array([f.radius for f in faces], dtype=float),
|
|
953
976
|
{"long_name": "radius of the interface", "units": length})
|
|
954
977
|
var = root.createVariable("interface_name", str, ("interface",))
|
|
@@ -973,7 +996,7 @@ def _write_file(nc: ModuleType, path: Path, model: Model,
|
|
|
973
996
|
|
|
974
997
|
# -- reading --------------------------------------------------------------------
|
|
975
998
|
|
|
976
|
-
def
|
|
999
|
+
def variable_attributes(var: Any) -> dict[str, Any]:
|
|
977
1000
|
"""A netCDF variable's attributes, numpy scalars made plain."""
|
|
978
1001
|
out = {}
|
|
979
1002
|
for key in var.ncattrs():
|
|
@@ -986,14 +1009,14 @@ def _attrs(var: Any) -> dict[str, Any]:
|
|
|
986
1009
|
return out
|
|
987
1010
|
|
|
988
1011
|
|
|
989
|
-
def
|
|
1012
|
+
def variable_values(var: Any) -> np.ndarray:
|
|
990
1013
|
"""A variable's numbers as float64, NaN where it holds its fill."""
|
|
991
1014
|
v = var[:]
|
|
992
1015
|
return (v.filled(np.nan) if np.ma.isMaskedArray(v)
|
|
993
1016
|
else np.asarray(v, dtype=float)).astype(float)
|
|
994
1017
|
|
|
995
1018
|
|
|
996
|
-
def
|
|
1019
|
+
def grid_of_group(group: Any) -> AngularGrid:
|
|
997
1020
|
"""The angular grid a group's coordinates and attributes hold."""
|
|
998
1021
|
from .sampling import AngularGrid
|
|
999
1022
|
lat = np.asarray(group.variables["latitude"][:], dtype=float)
|
|
@@ -1017,7 +1040,7 @@ def validate_structure(source: str | os.PathLike[str] | Any) -> None:
|
|
|
1017
1040
|
structure the module docstring defines, raising ValueError naming
|
|
1018
1041
|
the first thing wrong."""
|
|
1019
1042
|
if isinstance(source, (str, os.PathLike)):
|
|
1020
|
-
nc =
|
|
1043
|
+
nc = netcdf_module()
|
|
1021
1044
|
with nc.Dataset(source, "r") as ds:
|
|
1022
1045
|
_validate(ds, source)
|
|
1023
1046
|
else:
|
|
@@ -1134,7 +1157,7 @@ def _validate_part(g: Any, where: str, layers: np.ndarray, bounds: np.ndarray,
|
|
|
1134
1157
|
want: tuple[int, ...] = (3,)
|
|
1135
1158
|
else:
|
|
1136
1159
|
try:
|
|
1137
|
-
want = stored_shape(_character(
|
|
1160
|
+
want = stored_shape(_character(variable_attributes(var)))
|
|
1138
1161
|
except (ValueError, TypeError) as error:
|
|
1139
1162
|
_fail(path, f"{where}/{name} states no character: {error}")
|
|
1140
1163
|
tail = var.shape[3 if lateral else 1:]
|
|
@@ -1145,7 +1168,7 @@ def _validate_part(g: Any, where: str, layers: np.ndarray, bounds: np.ndarray,
|
|
|
1145
1168
|
"radial_interpolation") not in RADIAL_METHODS:
|
|
1146
1169
|
_fail(path, f"{where}/{name} states an unknown radial rule")
|
|
1147
1170
|
if "radial_layers" in a:
|
|
1148
|
-
listed = _radial_layers(
|
|
1171
|
+
listed = _radial_layers(variable_attributes(var))
|
|
1149
1172
|
held = (set(int(i) for i in np.atleast_1d(var.getncattr("layers")))
|
|
1150
1173
|
if "layers" in a else set(int(i) for i in layers))
|
|
1151
1174
|
if not lateral or not listed <= held:
|
|
@@ -1235,7 +1258,7 @@ def _validate_surfaces(g: Any, path: object, ninterface: int, taken: set[str]) -
|
|
|
1235
1258
|
|
|
1236
1259
|
def read_model_file(path: str | os.PathLike[str]) -> ModelFile:
|
|
1237
1260
|
"""A model file's numbers and metadata, its structure validated first."""
|
|
1238
|
-
nc =
|
|
1261
|
+
nc = netcdf_module()
|
|
1239
1262
|
with nc.Dataset(path, "r") as ds:
|
|
1240
1263
|
_validate(ds, path)
|
|
1241
1264
|
attrs = {k: ds.getncattr(k) for k in ds.ncattrs()}
|
|
@@ -1274,11 +1297,11 @@ def read_model_file(path: str | os.PathLike[str]) -> ModelFile:
|
|
|
1274
1297
|
if not np.allclose(stored, mesh.r.ravel(), rtol=1e-12, atol=0):
|
|
1275
1298
|
_fail(path, f"part_{k}: radius is not the GLL nodes of its elements")
|
|
1276
1299
|
variables = {
|
|
1277
|
-
name: FileVariable(
|
|
1300
|
+
name: FileVariable(variable_values(var), variable_attributes(var))
|
|
1278
1301
|
for name, var in g.variables.items()
|
|
1279
1302
|
if name not in ("radius", "element_layer", "element_bounds",
|
|
1280
1303
|
"latitude", "longitude", "colatitude_weights")}
|
|
1281
|
-
parts.append(FilePart(layers, mesh,
|
|
1304
|
+
parts.append(FilePart(layers, mesh, grid_of_group(g),
|
|
1282
1305
|
MappingProxyType(variables)))
|
|
1283
1306
|
geometry = None
|
|
1284
1307
|
if "geometry" in ds.groups:
|
|
@@ -1289,7 +1312,7 @@ def read_model_file(path: str | os.PathLike[str]) -> ModelFile:
|
|
|
1289
1312
|
if "boundary_displacement_poles" in g.variables else None)
|
|
1290
1313
|
geometry = FileGeometry(
|
|
1291
1314
|
np.asarray(g.variables["boundary_radius"][:], dtype=float),
|
|
1292
|
-
|
|
1315
|
+
grid_of_group(g), np.asarray(var[:], dtype=float), poles,
|
|
1293
1316
|
str(var.getncattr("lateral_interpolation")),
|
|
1294
1317
|
str(var.getncattr("between")))
|
|
1295
1318
|
surfaces = None
|
|
@@ -1297,10 +1320,12 @@ def read_model_file(path: str | os.PathLike[str]) -> ModelFile:
|
|
|
1297
1320
|
g = ds.groups["surfaces"]
|
|
1298
1321
|
faces = tuple(int(k) for k in
|
|
1299
1322
|
np.asarray(g.variables["surface_interface"][:]))
|
|
1300
|
-
variables = {name: FileVariable(
|
|
1323
|
+
variables = {name: FileVariable(variable_values(var),
|
|
1324
|
+
variable_attributes(var))
|
|
1301
1325
|
for name, var in g.variables.items()
|
|
1302
1326
|
if name not in _SURFACE_OWN}
|
|
1303
|
-
surfaces = FileSurfaces(faces,
|
|
1327
|
+
surfaces = FileSurfaces(faces, grid_of_group(g),
|
|
1328
|
+
MappingProxyType(variables))
|
|
1304
1329
|
return ModelFile(schema=str(attrs["schema"]), name=attrs.get("name"),
|
|
1305
1330
|
scales=scales, constants=MappingProxyType(constants),
|
|
1306
1331
|
meta=MappingProxyType(meta), skeleton=skeleton,
|
|
@@ -1308,7 +1333,9 @@ def read_model_file(path: str | os.PathLike[str]) -> ModelFile:
|
|
|
1308
1333
|
parts=tuple(parts), geometry=geometry,
|
|
1309
1334
|
declared=MappingProxyType(declared),
|
|
1310
1335
|
rtol=float(attrs.get("rtol", DEFAULT_RTOL)), surfaces=surfaces,
|
|
1311
|
-
citation=tuple(str(attrs.get("citation", "")).splitlines())
|
|
1336
|
+
citation=tuple(str(attrs.get("citation", "")).splitlines()),
|
|
1337
|
+
source=str(attrs.get("source", "")),
|
|
1338
|
+
created=str(attrs.get("created", "")))
|
|
1312
1339
|
|
|
1313
1340
|
|
|
1314
1341
|
def _character(attrs: Mapping[str, Any]) -> Character:
|