planetmodel 1.0.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.
Files changed (50) hide show
  1. planetmodel/__init__.py +208 -0
  2. planetmodel/behaviours.py +286 -0
  3. planetmodel/catalogue.py +274 -0
  4. planetmodel/character.py +146 -0
  5. planetmodel/deck.py +447 -0
  6. planetmodel/displacement.py +312 -0
  7. planetmodel/fields.py +599 -0
  8. planetmodel/frames.py +232 -0
  9. planetmodel/geometry.py +420 -0
  10. planetmodel/harmonics.py +184 -0
  11. planetmodel/layerfunction.py +503 -0
  12. planetmodel/loading/__init__.py +41 -0
  13. planetmodel/loading/assembly.py +391 -0
  14. planetmodel/loading/love.py +342 -0
  15. planetmodel/loading/material.py +184 -0
  16. planetmodel/mapping.py +512 -0
  17. planetmodel/materials.py +465 -0
  18. planetmodel/mesh1d/__init__.py +8 -0
  19. planetmodel/mesh1d/gll.py +81 -0
  20. planetmodel/mesh1d/gravity.py +152 -0
  21. planetmodel/mesh1d/mesh.py +266 -0
  22. planetmodel/mesh3d/__init__.py +41 -0
  23. planetmodel/mesh3d/_geometry.py +138 -0
  24. planetmodel/mesh3d/_orient.py +228 -0
  25. planetmodel/mesh3d/_session.py +76 -0
  26. planetmodel/mesh3d/_sizing.py +144 -0
  27. planetmodel/mesh3d/_tagging.py +201 -0
  28. planetmodel/mesh3d/_validate.py +134 -0
  29. planetmodel/mesh3d/_writer.py +90 -0
  30. planetmodel/mesh3d/export.py +514 -0
  31. planetmodel/mesh3d/layered.py +202 -0
  32. planetmodel/mesh3d/manifest.py +547 -0
  33. planetmodel/mesh3d/offset.py +246 -0
  34. planetmodel/mesh3d/spec.py +400 -0
  35. planetmodel/model.py +471 -0
  36. planetmodel/plotting.py +99 -0
  37. planetmodel/pushforward.py +322 -0
  38. planetmodel/randomfield/__init__.py +28 -0
  39. planetmodel/randomfield/fields.py +514 -0
  40. planetmodel/randomfield/operator.py +385 -0
  41. planetmodel/rheology.py +173 -0
  42. planetmodel/sampling.py +434 -0
  43. planetmodel/skeleton.py +285 -0
  44. planetmodel/testing.py +634 -0
  45. planetmodel/units.py +204 -0
  46. planetmodel/vocabulary.py +117 -0
  47. planetmodel-1.0.0.dist-info/METADATA +161 -0
  48. planetmodel-1.0.0.dist-info/RECORD +50 -0
  49. planetmodel-1.0.0.dist-info/WHEEL +4 -0
  50. planetmodel-1.0.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,208 @@
1
+ """planetmodel: spherically layered planetary models, and their meshes.
2
+
3
+ A model is a skeleton of boundary radii, a geometry that places the
4
+ skeleton in the physical world through one continuous mapping, and a
5
+ bag of fields on each layer. This package holds the skeleton and the
6
+ geometry, the mappings that place them, the fields and the model, the
7
+ catalogue of named models, and the meshers that hand a model to a
8
+ solver. Every internal is numbers: the model alone carries units.
9
+
10
+ Modules reached by name, one level down:
11
+
12
+ frames the local spherical frame, tensors moved between frames, Voigt
13
+ materials velocity and moduli conversions, the elastic field, what a
14
+ layer's fields imply (fluidity, moduli)
15
+ rheology what a layer's rheology fields imply, and a model frozen at
16
+ a frequency with complex moduli
17
+ units Dimensions, Scales and the dimension constants
18
+ vocabulary the shipped field names, their characters and dimensions
19
+ behaviours the free functions as methods: `layer_method` and the mixins
20
+ catalogue PREM, LayeredIsotropicElastic and MineosModel, model types
21
+ deck tabulated models: decks, their formats, fields by interpolation
22
+ sampling a model on a radial mesh times an angular grid
23
+ harmonics real spherical harmonics; grid transforms via pyshtools (optional)
24
+ plotting radial profiles drawn one way, radius upward (matplotlib, optional)
25
+ mesh1d radial spectral-element meshes (GLL), nodal values, gravity
26
+ mesh3d 2D and 3D meshes via gmsh, and MFEM export
27
+ loading the loading and tidal problem on a radial mesh: Love numbers
28
+ randomfield Matern random fields on balls, annuli and layers
29
+ testing the executable contracts, `check_field` and its kin
30
+ """
31
+
32
+ from .behaviours import (
33
+ ConstantQ,
34
+ Elastic,
35
+ SelfGravitating,
36
+ Viscoelastic,
37
+ layer_method,
38
+ with_moduli,
39
+ with_velocities,
40
+ )
41
+ from .catalogue import PREM, LayeredIsotropicElastic, MineosModel
42
+ from .character import DENSITY, ELASTIC, SCALAR, STRESS, VECTOR, Character, Symmetry
43
+ from .deck import (
44
+ MINEOS,
45
+ Deck,
46
+ DeckFormat,
47
+ Tabulated,
48
+ deck_layers,
49
+ read_deck,
50
+ write_deck,
51
+ )
52
+ from .displacement import (
53
+ CallableDisplacement,
54
+ RadialDisplacement,
55
+ ZeroDisplacement,
56
+ as_displacement,
57
+ flattening,
58
+ layer_linear,
59
+ )
60
+ from .fields import (
61
+ AnalyticField,
62
+ ComposedField,
63
+ Field,
64
+ FieldBase,
65
+ RadialField,
66
+ constant_field,
67
+ )
68
+ from .geometry import Geometry, InterfaceInfo, LayerInfo
69
+ from .harmonics import analyse_grid, real_harmonics, synthesise, synthesise_grid
70
+ from .layerfunction import (
71
+ LayerFunction,
72
+ NumericLayer,
73
+ PolynomialLayer,
74
+ as_layer_function,
75
+ constant_layer,
76
+ polynomial_fit,
77
+ polynomial_layer,
78
+ )
79
+ from .mapping import (
80
+ IdentityMapping,
81
+ Mapping,
82
+ MappingBase,
83
+ MappingPerturbation,
84
+ RadialStretch,
85
+ ScaledMapping,
86
+ ValidityReport,
87
+ outer_radius_of,
88
+ validity_lattice,
89
+ )
90
+ from .materials import ElasticField, elastic_moduli, is_fluid, kappa_mu, moduli
91
+ from .mesh1d import RadialMesh
92
+ from .mesh1d.gravity import gravity, gravity_fields, mass
93
+ from .model import Layer, Model
94
+ from .pushforward import PulledBackField, PushedForwardField, pull_back, push_forward
95
+ from .rheology import (
96
+ dispersive_moduli,
97
+ frozen,
98
+ frozen_moduli,
99
+ is_viscoelastic,
100
+ reference_omega,
101
+ )
102
+ from .sampling import AngularGrid, Sample, equiangular, gauss_legendre, sample
103
+ from .skeleton import CoarseningMap, Location, Skeleton
104
+ from .units import EARTH_MEAN_DENSITY, G_SI, Dimensions, Scales
105
+ from .vocabulary import CONSTANTS, VOCABULARY, Constant, FieldSpec
106
+ from . import testing # noqa: F401
107
+
108
+ __version__ = "1.0.0"
109
+
110
+ __all__ = [
111
+ "Skeleton",
112
+ "Location",
113
+ "CoarseningMap",
114
+ "Geometry",
115
+ "LayerInfo",
116
+ "InterfaceInfo",
117
+ "Mapping",
118
+ "MappingBase",
119
+ "IdentityMapping",
120
+ "RadialStretch",
121
+ "ScaledMapping",
122
+ "ValidityReport",
123
+ "MappingPerturbation",
124
+ "validity_lattice",
125
+ "outer_radius_of",
126
+ "RadialDisplacement",
127
+ "ZeroDisplacement",
128
+ "CallableDisplacement",
129
+ "as_displacement",
130
+ "RadialMesh",
131
+ "Character",
132
+ "SCALAR",
133
+ "DENSITY",
134
+ "VECTOR",
135
+ "STRESS",
136
+ "ELASTIC",
137
+ "Symmetry",
138
+ "LayerFunction",
139
+ "PolynomialLayer",
140
+ "NumericLayer",
141
+ "as_layer_function",
142
+ "polynomial_layer",
143
+ "constant_layer",
144
+ "polynomial_fit",
145
+ "Field",
146
+ "FieldBase",
147
+ "RadialField",
148
+ "AnalyticField",
149
+ "ComposedField",
150
+ "constant_field",
151
+ "ElasticField",
152
+ "is_fluid",
153
+ "moduli",
154
+ "elastic_moduli",
155
+ "kappa_mu",
156
+ "is_viscoelastic",
157
+ "frozen_moduli",
158
+ "dispersive_moduli",
159
+ "frozen",
160
+ "reference_omega",
161
+ "push_forward",
162
+ "pull_back",
163
+ "PushedForwardField",
164
+ "PulledBackField",
165
+ "Dimensions",
166
+ "Scales",
167
+ "G_SI",
168
+ "EARTH_MEAN_DENSITY",
169
+ "FieldSpec",
170
+ "Constant",
171
+ "VOCABULARY",
172
+ "CONSTANTS",
173
+ "Layer",
174
+ "Model",
175
+ "layer_method",
176
+ "with_moduli",
177
+ "with_velocities",
178
+ "Elastic",
179
+ "ConstantQ",
180
+ "SelfGravitating",
181
+ "Viscoelastic",
182
+ "PREM",
183
+ "LayeredIsotropicElastic",
184
+ "MineosModel",
185
+ "Deck",
186
+ "DeckFormat",
187
+ "MINEOS",
188
+ "read_deck",
189
+ "write_deck",
190
+ "deck_layers",
191
+ "Tabulated",
192
+ "flattening",
193
+ "layer_linear",
194
+ "AngularGrid",
195
+ "Sample",
196
+ "sample",
197
+ "gauss_legendre",
198
+ "equiangular",
199
+ "real_harmonics",
200
+ "synthesise",
201
+ "synthesise_grid",
202
+ "analyse_grid",
203
+ "gravity",
204
+ "mass",
205
+ "gravity_fields",
206
+ "testing",
207
+ "__version__",
208
+ ]
@@ -0,0 +1,286 @@
1
+ """The free functions as methods: an adaptor, and stateless mixins.
2
+
3
+ A model type is a class derived from `Model` alone. What its
4
+ constructor is given is its base information, PREM's polynomial
5
+ coefficients say; the model is that information and everything derived
6
+ from it, fields and behaviour alike, and the mixins here are the common
7
+ derivations written once: the elastic description completed in both
8
+ directions (moduli from velocities and velocities from moduli), the
9
+ elastic tensor and its averages, gravity, and the linear rheologies
10
+ built from their static parts. A model type gets them by mixing in,
11
+ never by reimplementing them and never through a hierarchy of model
12
+ types. Two forms of wrapping are here.
13
+
14
+ `layer_method(fn)` turns a function of a layer, such as `moduli(layer)`,
15
+ into a method `model.moduli(which)` that resolves `which` (an index or a
16
+ name) through `model.layer`. A function of a model, such as
17
+ `gravity(model, radii)` or `frozen(model, omega)`, needs no adaptor:
18
+ assigned in a class body it is already a method.
19
+
20
+ The mixins bundle the wrapped methods a kind of model exposes. Each is
21
+ a class body of such assignments and nothing else, with one exception:
22
+ `Elastic` has a constructor hook that attaches the five Love moduli to
23
+ every layer as first-class fields, since a spherically symmetric
24
+ elastic medium is those five and there is no reason to hide them behind
25
+ a call. No mixin holds state of its own, so `class PREM(Elastic,
26
+ ConstantQ, SelfGravitating, Viscoelastic, Model)` is still one model
27
+ type derived from `Model` alone. The transformations `isotropic`,
28
+ `elastic` and `with_gravity` return copies of the same class through
29
+ `Model.replaced`.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import functools
34
+ from collections.abc import Callable, Mapping
35
+ from typing import TYPE_CHECKING, Any
36
+
37
+ import numpy as np
38
+
39
+ from . import materials, rheology
40
+ from .character import SCALAR
41
+ from .fields import ComposedField, Field
42
+ from .mesh1d.gravity import gravity, gravity_fields, mass
43
+
44
+ if TYPE_CHECKING:
45
+ from .model import Model
46
+
47
+ __all__ = ["layer_method", "with_moduli", "with_velocities", "Elastic", "ConstantQ",
48
+ "SelfGravitating", "Viscoelastic", "ELASTIC_FAMILIES", "elastic_family"]
49
+
50
+ #: The velocity names an elastic description may be given in.
51
+ VELOCITY_NAMES = ("vp", "vs", "vpv", "vph", "vsv", "vsh", "eta")
52
+
53
+ #: The ways a layer describes its elastic medium, each a family of names
54
+ #: that is complete on its own: the velocities beside rho, the isotropic
55
+ #: moduli, the five Love moduli, and the tensor stored under
56
+ #: `elastic_moduli`.
57
+ ELASTIC_FAMILIES = (VELOCITY_NAMES,
58
+ materials.MODULI_NAMES[materials.Symmetry.ISOTROPIC],
59
+ materials.MODULI_NAMES[materials.Symmetry.VTI],
60
+ ("elastic_moduli",))
61
+
62
+
63
+ def elastic_family(name: str) -> tuple[str, ...]:
64
+ """The family of `ELASTIC_FAMILIES` an elastic name belongs to."""
65
+ for family in ELASTIC_FAMILIES:
66
+ if name in family:
67
+ return family
68
+ raise KeyError(f"{name!r} is not an elastic name; those are "
69
+ f"{list(materials.ELASTIC_NAMES)}")
70
+
71
+
72
+ def layer_method[T](fn: Callable[..., T]) -> Callable[..., T]:
73
+ """`fn(layer, *args, **kwargs)` as a method `model.fn(which, *args, **kwargs)`.
74
+
75
+ `which` is a layer index (negatives counting back) or a layer name,
76
+ resolved by `model.layer`; the docstring and name are the function's.
77
+ """
78
+ @functools.wraps(fn)
79
+ def method(self: Model, which: int | str, *args: object, **kwargs: object) -> T:
80
+ return fn(self.layer(which), *args, **kwargs)
81
+ return method
82
+
83
+
84
+ def with_moduli(fields: Mapping[str, Field]) -> dict[str, Field]:
85
+ """A layer's fields with the five Love moduli A, C, F, L, N added,
86
+ read from whatever elastic description it holds; unchanged where it
87
+ already holds the five, holds its tensor directly, or holds no
88
+ elastic description at all."""
89
+ out = dict(fields)
90
+ five = materials.MODULI_NAMES[materials.Symmetry.VTI]
91
+ if (all(n in out for n in five) or "elastic_moduli" in out
92
+ or not any(n in out for n in materials.ELASTIC_NAMES)):
93
+ return out
94
+ out.update(materials.moduli(out))
95
+ return out
96
+
97
+
98
+ def with_velocities(fields: Mapping[str, Field]) -> dict[str, Field]:
99
+ """A layer's fields with the velocities added from rho and the five
100
+ Love moduli: vp and vs where the five are isotropic, else vpv, vph,
101
+ vsv, vsh and eta. Square roots are taken pointwise, so these are
102
+ composed fields, not polynomials. Unchanged where the layer already
103
+ holds any velocity, lacks rho or the five, or holds its tensor
104
+ directly."""
105
+ out = dict(fields)
106
+ five = materials.MODULI_NAMES[materials.Symmetry.VTI]
107
+ if (any(n in out for n in VELOCITY_NAMES) or "rho" not in out
108
+ or not all(n in out for n in five) or "elastic_moduli" in out):
109
+ return out
110
+ rho = out["rho"]
111
+ A, C, F, L, N = (out[n] for n in five)
112
+
113
+ def speed(modulus: Field, name: str) -> Field:
114
+ return ComposedField(lambda m, d: np.sqrt(m / d), (modulus, rho),
115
+ character=SCALAR, name=name)
116
+
117
+ symmetry, _ = materials.independent_moduli(out)
118
+ if symmetry is materials.Symmetry.ISOTROPIC:
119
+ out["vp"] = speed(C, "vp")
120
+ out["vs"] = speed(L, "vs")
121
+ return out
122
+ out["vpv"], out["vph"] = speed(C, "vpv"), speed(A, "vph")
123
+ out["vsv"], out["vsh"] = speed(L, "vsv"), speed(N, "vsh")
124
+ out["eta"] = ComposedField(lambda f, a, l: f / (a - 2.0 * l), (F, A, L),
125
+ character=SCALAR, name="eta")
126
+ return out
127
+
128
+
129
+ class Elastic:
130
+ """The elastic behaviour of a spherically symmetric model.
131
+
132
+ On construction the elastic description of every layer is completed
133
+ in both directions: the five Love moduli A, C, F, L, N are attached
134
+ where a layer was given rho with velocities or kappa and mu, exact
135
+ where those are polynomial, and the velocities where a layer was
136
+ given rho with the five (`with_moduli`, `with_velocities`); a layer
137
+ already holding both, or its tensor under `elastic_moduli`, is left
138
+ as it is. The methods then read the fields: `moduli(which)` the
139
+ five, `elastic_moduli` the tensor (the layer's own of any symmetry
140
+ where it holds one), `kappa_mu` the Voigt average, `is_fluid` the
141
+ vanishing of shear, and `isotropic()` the model re-described by its
142
+ Voigt average, kappa and mu with the five and the velocities
143
+ recomputed from them. A layer holding a general anisotropic tensor
144
+ is refused by the three that read the five, until the general Voigt
145
+ average is written.
146
+
147
+ The derived fields are kept in step by `with_field`: replacing an
148
+ elastic field on a layer makes the family it belongs to (rho with
149
+ the velocities, kappa and mu, or the five) the description, drops
150
+ the other elastic names from that layer, and completes the
151
+ description again, so the five follow a new `vs` and the velocities
152
+ a new `L`; a field added beside the existing ones is attached as it
153
+ is. A change that goes through `replaced` directly, as the
154
+ library's own transformations do, is the caller's to keep
155
+ consistent.
156
+ """
157
+
158
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
159
+ super().__init__(*args, **kwargs)
160
+ from .model import Layer
161
+ self._layers = tuple(
162
+ Layer(layer.info, with_velocities(with_moduli(layer.fields)))
163
+ for layer in self._layers)
164
+
165
+ is_fluid = layer_method(materials.is_fluid)
166
+ moduli = layer_method(materials.moduli)
167
+ elastic_moduli = layer_method(materials.elastic_moduli)
168
+ kappa_mu = layer_method(materials.kappa_mu)
169
+
170
+ def with_field(self: Model, which: int | str, name: str, field: Field, *,
171
+ replace: bool = False) -> Model:
172
+ """A copy with `field` attached to one layer under `name`.
173
+
174
+ Replacing an elastic field makes its family the layer's
175
+ description and derives the other elastic fields from it again;
176
+ a field added beside the existing ones, or a family too
177
+ incomplete to describe the medium, is attached as it is.
178
+ """
179
+ from .model import Model
180
+ out = Model.with_field(self, which, name, field, replace=replace)
181
+ if not replace or name not in materials.ELASTIC_NAMES:
182
+ return out
183
+ i = self.layer(which).index
184
+ layers = [dict(layer.fields) for layer in out.layers]
185
+ keep = elastic_family(name)
186
+ fields = {k: f for k, f in layers[i].items()
187
+ if k not in materials.ELASTIC_NAMES or k in keep}
188
+ try:
189
+ layers[i] = with_velocities(with_moduli(fields))
190
+ except KeyError:
191
+ return out
192
+ return out.replaced(layers=layers)
193
+
194
+ def isotropic(self: Model) -> Model:
195
+ """The model with every elastic description replaced by its Voigt
196
+ average: each layer keeps rho and everything that is not an
197
+ elastic name, and holds kappa and mu, and the five recomputed
198
+ from them, in place of whatever it described its medium by.
199
+ Exact on polynomial layers. A layer holding no elastic
200
+ description is left alone.
201
+ """
202
+ layers = []
203
+ for layer in self.layers:
204
+ fields = {k: f for k, f in layer.fields.items()
205
+ if k not in materials.ELASTIC_NAMES}
206
+ if any(n in layer for n in materials.ELASTIC_NAMES):
207
+ kappa, mu = materials.kappa_mu(layer)
208
+ fields["kappa"], fields["mu"] = kappa, mu
209
+ fields = with_velocities(with_moduli(fields))
210
+ layers.append(fields)
211
+ return self.replaced(layers=layers)
212
+
213
+
214
+ class ConstantQ:
215
+ """The Love moduli at a frequency under the constant-Q absorption
216
+ band: kappa dispersed by `qkappa` and mu by `qmu` about a reference
217
+ frequency, following the logarithmic dispersion relation, so that a
218
+ layer's five at angular frequency omega are complex fields. The
219
+ model is not changed; `Viscoelastic.frozen` is the model that is.
220
+ The reference frequency is the model's constant `omega_ref` where
221
+ it declares one, else 2 pi rad/s, a period of one second, in the
222
+ model's units (`reference_omega`).
223
+ """
224
+
225
+ reference_omega = rheology.reference_omega
226
+
227
+ def moduli_at(self: Model, which: int | str, omega: float, *,
228
+ reference_omega: float | None = None) -> dict[str, Field]:
229
+ """The five moduli of a layer at `omega`, complex where the layer
230
+ holds a Q, else its static five."""
231
+ ref = self.reference_omega() if reference_omega is None else reference_omega
232
+ return rheology.dispersive_moduli(self.layer(which), omega,
233
+ reference_omega=ref)
234
+
235
+ def elastic_moduli_at(self: Model, which: int | str, omega: float, *,
236
+ reference_omega: float | None = None
237
+ ) -> materials.ElasticField:
238
+ """The transversely isotropic tensor of a layer at `omega`."""
239
+ five = self.moduli_at(which, omega, reference_omega=reference_omega)
240
+ return materials.ElasticField(materials.Symmetry.VTI, five,
241
+ name="elastic_moduli")
242
+
243
+
244
+ class SelfGravitating:
245
+ """The gravity and mass of a spherically symmetric model with a radial
246
+ density on every layer: the reference body's, computed on each call
247
+ and never stored, as numbers by `gravity` and `mass`, as one radial
248
+ field per layer by `gravity_fields`, and as a copy of the model
249
+ holding that field under the vocabulary name `g` by `with_gravity`.
250
+ A density that depends on direction is refused, and the geometry's
251
+ mapping does not enter (see `mesh1d.gravity`)."""
252
+
253
+ gravity = gravity
254
+ mass = mass
255
+ gravity_fields = gravity_fields
256
+
257
+ def with_gravity(self: Model, *, name: str = "g", replace: bool = False) -> Model:
258
+ """The model with its gravity attached to every layer as a radial
259
+ field under `name`, exact where the density is polynomial; a
260
+ layer already holding `name` is refused unless `replace`."""
261
+ fields = gravity_fields(self)
262
+ layers = []
263
+ for layer, field in zip(self.layers, fields):
264
+ if name in layer and not replace:
265
+ raise ValueError(
266
+ f"layer {layer.index} ({layer.name!r}) already holds {name!r}; "
267
+ "pass replace=True to replace it")
268
+ layers.append({**layer.fields, name: field})
269
+ return self.replaced(layers=layers)
270
+
271
+
272
+ class Viscoelastic:
273
+ """The rheology read from the fields: a layer's viscoelasticity, the
274
+ model frozen at a frequency, and the elastic model that drops the
275
+ rheology fields."""
276
+
277
+ is_viscoelastic = layer_method(rheology.is_viscoelastic)
278
+ frozen = rheology.frozen
279
+
280
+ def elastic(self: Model) -> Model:
281
+ """The model without its rheology fields (`RHEOLOGY_NAMES`), so
282
+ that every layer is elastic; a frozen model keeps its complex
283
+ moduli, which are what its rheology became."""
284
+ layers = [{k: f for k, f in layer.fields.items()
285
+ if k not in rheology.RHEOLOGY_NAMES} for layer in self.layers]
286
+ return self.replaced(layers=layers, check=False)