planetmodel 1.2.3__tar.gz → 1.2.4__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.
Files changed (50) hide show
  1. planetmodel-1.2.4/PKG-INFO +166 -0
  2. planetmodel-1.2.4/README.md +130 -0
  3. {planetmodel-1.2.3 → planetmodel-1.2.4}/pyproject.toml +1 -1
  4. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/__init__.py +1 -1
  5. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/deck.py +3 -0
  6. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/displacement.py +11 -1
  7. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/fields.py +17 -5
  8. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/geometry.py +7 -0
  9. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/layerfunction.py +24 -5
  10. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mapping.py +26 -3
  11. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/materials.py +2 -0
  12. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh1d/__init__.py +8 -1
  13. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/__init__.py +5 -3
  14. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_geometry.py +1 -0
  15. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_sizing.py +48 -13
  16. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/layered.py +9 -4
  17. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/manifest.py +79 -3
  18. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/spec.py +206 -12
  19. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/model.py +18 -1
  20. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/pushforward.py +1 -0
  21. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/randomfield/__init__.py +2 -2
  22. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/sampling.py +2 -0
  23. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/testing.py +42 -1
  24. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/units.py +2 -0
  25. planetmodel-1.2.3/PKG-INFO +0 -161
  26. planetmodel-1.2.3/README.md +0 -125
  27. {planetmodel-1.2.3 → planetmodel-1.2.4}/LICENSE +0 -0
  28. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/behaviours.py +0 -0
  29. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/catalogue.py +0 -0
  30. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/character.py +0 -0
  31. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/frames.py +0 -0
  32. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/harmonics.py +0 -0
  33. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh1d/gll.py +0 -0
  34. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh1d/gravity.py +0 -0
  35. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh1d/mesh.py +0 -0
  36. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_orient.py +0 -0
  37. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_session.py +0 -0
  38. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_tagging.py +0 -0
  39. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_validate.py +0 -0
  40. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_writer.py +0 -0
  41. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/export.py +0 -0
  42. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/offset.py +0 -0
  43. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/plotting.py +0 -0
  44. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/randomfield/basis.py +0 -0
  45. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/randomfield/fields.py +0 -0
  46. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/randomfield/mesh.py +0 -0
  47. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/randomfield/operator.py +0 -0
  48. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/rheology.py +0 -0
  49. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/skeleton.py +0 -0
  50. {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/vocabulary.py +0 -0
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: planetmodel
3
+ Version: 1.2.4
4
+ Summary: Spherically layered planetary models: reference bodies, fields, mappings and meshes
5
+ License-Expression: BSD-3-Clause
6
+ License-File: LICENSE
7
+ Keywords: geophysics,seismology,planetary models,PREM,spectral elements,meshing
8
+ Author: David Al-Attar
9
+ Author-email: da380@cam.ac.uk
10
+ Requires-Python: >=3.12
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Physics
17
+ Provides-Extra: harmonics
18
+ Provides-Extra: meshing
19
+ Provides-Extra: mfem
20
+ Provides-Extra: netcdf
21
+ Provides-Extra: notebook
22
+ Provides-Extra: plot
23
+ Requires-Dist: ducc0 (>=0.41) ; extra == "harmonics"
24
+ Requires-Dist: gmsh (>=4.13) ; extra == "meshing"
25
+ Requires-Dist: ipykernel (>=6.29) ; extra == "notebook"
26
+ Requires-Dist: matplotlib (>=3.11,<4) ; extra == "plot"
27
+ Requires-Dist: mfem (>=4.8) ; extra == "mfem"
28
+ Requires-Dist: netCDF4 (>=1.7) ; extra == "netcdf"
29
+ Requires-Dist: numpy (>=2.0,<3)
30
+ Requires-Dist: pyshtools (>=4.14) ; extra == "harmonics"
31
+ Requires-Dist: scipy (>=1.15,<2)
32
+ Project-URL: Homepage, https://github.com/da380/planetmodel
33
+ Project-URL: Repository, https://github.com/da380/planetmodel
34
+ Description-Content-Type: text/markdown
35
+
36
+ # planetmodel
37
+
38
+ Spherically layered planetary models — a planet described as concentric
39
+ layers with physical properties on each — and the meshes that hand such
40
+ a model to a numerical solver.
41
+
42
+ With this library you can build a model (PREM out of the box, simple
43
+ layered models, any mineos deck file, or a model type of your own), ask
44
+ it for its properties with the discontinuities treated honestly, deform
45
+ it with ellipticity or surface topography, and mesh it: a
46
+ spectral-element mesh along the radius, or a 2D or 3D finite-element
47
+ mesh for [MFEM](https://mfem.org), refined where you need it. A
48
+ sub-package, `planetmodel.randomfield`, draws Matérn random fields on
49
+ balls, annuli and layers.
50
+
51
+ ## Design
52
+
53
+ **A skeleton.** The list of boundary radii, increasing, possibly
54
+ starting above zero for a hollow shell. It answers geometric questions
55
+ (which layer a radius lies in, the intervals between boundaries) and
56
+ supports surgery: refine, truncate, hollow, extend, coarsen.
57
+
58
+ **A geometry.** A skeleton placed in the physical world by one
59
+ continuous mapping from the reference (spherical) body to the physical
60
+ one, plus names for the layers and interfaces. Ellipticity or surface
61
+ topography is therefore a property of the whole model, stated once,
62
+ rather than something each field has to know about. The shipped mapping
63
+ is the radial stretch `m(X) = (r + h) e_r` driven by any callable
64
+ `h(r, theta, phi)`; any object with `__call__`,
65
+ `deformation_gradient` and `jacobian` serves as a mapping, and a
66
+ geometry checks the invariants (orientation preserved, continuous,
67
+ kinked only on boundaries) when it is built.
68
+
69
+ **A field on one layer.** An interval, a name, a character (the tensor
70
+ rank and weight that say how the values transform), and
71
+ `evaluate(r, theta, phi, *, frame)` giving components in the local
72
+ spherical frame or in Cartesian ones. A discontinuity is simply two
73
+ layers: ask each side and get each side's answer. Radial fields sit on
74
+ an algebra that is exact on polynomials — PREM's moduli `rho v^2` are
75
+ exact polynomials, not fits — and fields may be complex-valued, which
76
+ is what a model frozen at a frequency holds.
77
+
78
+ **One model class.** A model is a geometry with named fields on every
79
+ layer; what a name like `"rho"` means comes from the shipped vocabulary
80
+ or from the specs the model is given. A model type (`PREM`,
81
+ `LayeredIsotropicElastic`, `MineosModel`, or your own) derives from
82
+ `Model` alone — there is no hierarchy of model types — and the mixins
83
+ of `planetmodel.behaviours` add the standard derivations: the Love
84
+ moduli beside the velocities, the elastic tensor and its Voigt average,
85
+ gravity, and the constant-Q and Maxwell rheologies frozen at a
86
+ frequency.
87
+
88
+ **Units in one place.** The model's `Scales` say what one stored unit
89
+ of length, mass and time is in SI, and `converted` re-expresses the
90
+ whole model, exactly for polynomials. Nothing else names a unit: radii
91
+ are numbers, tolerances are relative, and the meshers hand the
92
+ geometry's numbers to gmsh unchanged.
93
+
94
+ **Two meshers.** `planetmodel.mesh1d` lays Gauss–Lobatto–Legendre
95
+ elements along the radius with every boundary an element boundary,
96
+ evaluates a model's fields and gravity on the nodes, and samples a
97
+ model on an angular grid.
98
+ `planetmodel.mesh3d` meshes a geometry, full or hollow, in 2D or 3D,
99
+ with buffer shells outside it when a far-field condition needs room;
100
+ element sizes are controlled per interface, and refinements
101
+ (`NearPoints`, `near_curve`, or any size function of your own) make the
102
+ mesh finer towards a coastline, a source region or a station network
103
+ without coarsening anything elsewhere. Every mesh comes with a JSON
104
+ manifest saying what each attribute means, and exports to MFEM together
105
+ with the mapping's displacement and the model's fields.
106
+
107
+ **Executable contracts.** `planetmodel.testing` holds one `check_*`
108
+ function per protocol, and the shipped implementations and your own are
109
+ held to the same call.
110
+
111
+ ## Installing
112
+
113
+ ```
114
+ pip install planetmodel # numpy and scipy only
115
+ pip install 'planetmodel[meshing]' # 2D and 3D meshes via gmsh
116
+ pip install 'planetmodel[mfem]' # export to MFEM (PyMFEM)
117
+ pip install 'planetmodel[harmonics]' # grid transforms via pyshtools and ducc0
118
+ pip install 'planetmodel[plot]' # matplotlib, for the figures
119
+ pip install 'planetmodel[notebook]' # ipykernel, to run tutorials cell by cell
120
+ ```
121
+
122
+ Python 3.12 or later. Nothing optional is imported by `import
123
+ planetmodel`.
124
+
125
+ ## A first look
126
+
127
+ ```python
128
+ from planetmodel import PREM, RadialMesh, flattening, gravity
129
+
130
+ m = PREM() # exact polynomials, SI units
131
+
132
+ # Fields live on layers, so a discontinuity gives two honest answers,
133
+ # one from each side.
134
+ r_cmb = m.geometry.interface("cmb").radius
135
+ print(m.layer("outer_core")["rho"](r_cmb)) # density, core side
136
+ print(m.layer("lowermost_mantle")["rho"](r_cmb)) # density, mantle side
137
+ print(gravity(m, [r_cmb, 6371e3])) # gravity at the CMB and surface
138
+
139
+ # Reshape the model: non-dimensionalise it, flatten it by 1/300, and
140
+ # lay a radial spectral-element mesh over it.
141
+ nd = m.nondimensionalised().stretched(flattening(1 / 300, rmax=1.0))
142
+ mesh = RadialMesh(nd, ngll=5, drmax=0.05)
143
+ print(mesh.nodal(nd, "rho").shape) # density at every mesh node
144
+ ```
145
+
146
+ ## Where to go next
147
+
148
+ - `examples/tutorials/`: eleven walkthroughs, from a skeleton to random
149
+ fields and deck files, each a `# %%` script that runs headless.
150
+ - `src/planetmodel/mesh3d/manifest.py`: the manifest beside every mesh,
151
+ its schema described from the consumer's side.
152
+ - `CONTRIBUTING.md`: the development setup, the hooks, the test
153
+ selections and how a release is made.
154
+
155
+ ## Tests
156
+
157
+ ```
158
+ poetry run pytest # the fast suite
159
+ poetry run pytest -m "not slow" # with gmsh and MFEM
160
+ poetry run ruff check .
161
+ ```
162
+
163
+ ## Licence
164
+
165
+ BSD-3
166
+
@@ -0,0 +1,130 @@
1
+ # planetmodel
2
+
3
+ Spherically layered planetary models — a planet described as concentric
4
+ layers with physical properties on each — and the meshes that hand such
5
+ a model to a numerical solver.
6
+
7
+ With this library you can build a model (PREM out of the box, simple
8
+ layered models, any mineos deck file, or a model type of your own), ask
9
+ it for its properties with the discontinuities treated honestly, deform
10
+ it with ellipticity or surface topography, and mesh it: a
11
+ spectral-element mesh along the radius, or a 2D or 3D finite-element
12
+ mesh for [MFEM](https://mfem.org), refined where you need it. A
13
+ sub-package, `planetmodel.randomfield`, draws Matérn random fields on
14
+ balls, annuli and layers.
15
+
16
+ ## Design
17
+
18
+ **A skeleton.** The list of boundary radii, increasing, possibly
19
+ starting above zero for a hollow shell. It answers geometric questions
20
+ (which layer a radius lies in, the intervals between boundaries) and
21
+ supports surgery: refine, truncate, hollow, extend, coarsen.
22
+
23
+ **A geometry.** A skeleton placed in the physical world by one
24
+ continuous mapping from the reference (spherical) body to the physical
25
+ one, plus names for the layers and interfaces. Ellipticity or surface
26
+ topography is therefore a property of the whole model, stated once,
27
+ rather than something each field has to know about. The shipped mapping
28
+ is the radial stretch `m(X) = (r + h) e_r` driven by any callable
29
+ `h(r, theta, phi)`; any object with `__call__`,
30
+ `deformation_gradient` and `jacobian` serves as a mapping, and a
31
+ geometry checks the invariants (orientation preserved, continuous,
32
+ kinked only on boundaries) when it is built.
33
+
34
+ **A field on one layer.** An interval, a name, a character (the tensor
35
+ rank and weight that say how the values transform), and
36
+ `evaluate(r, theta, phi, *, frame)` giving components in the local
37
+ spherical frame or in Cartesian ones. A discontinuity is simply two
38
+ layers: ask each side and get each side's answer. Radial fields sit on
39
+ an algebra that is exact on polynomials — PREM's moduli `rho v^2` are
40
+ exact polynomials, not fits — and fields may be complex-valued, which
41
+ is what a model frozen at a frequency holds.
42
+
43
+ **One model class.** A model is a geometry with named fields on every
44
+ layer; what a name like `"rho"` means comes from the shipped vocabulary
45
+ or from the specs the model is given. A model type (`PREM`,
46
+ `LayeredIsotropicElastic`, `MineosModel`, or your own) derives from
47
+ `Model` alone — there is no hierarchy of model types — and the mixins
48
+ of `planetmodel.behaviours` add the standard derivations: the Love
49
+ moduli beside the velocities, the elastic tensor and its Voigt average,
50
+ gravity, and the constant-Q and Maxwell rheologies frozen at a
51
+ frequency.
52
+
53
+ **Units in one place.** The model's `Scales` say what one stored unit
54
+ of length, mass and time is in SI, and `converted` re-expresses the
55
+ whole model, exactly for polynomials. Nothing else names a unit: radii
56
+ are numbers, tolerances are relative, and the meshers hand the
57
+ geometry's numbers to gmsh unchanged.
58
+
59
+ **Two meshers.** `planetmodel.mesh1d` lays Gauss–Lobatto–Legendre
60
+ elements along the radius with every boundary an element boundary,
61
+ evaluates a model's fields and gravity on the nodes, and samples a
62
+ model on an angular grid.
63
+ `planetmodel.mesh3d` meshes a geometry, full or hollow, in 2D or 3D,
64
+ with buffer shells outside it when a far-field condition needs room;
65
+ element sizes are controlled per interface, and refinements
66
+ (`NearPoints`, `near_curve`, or any size function of your own) make the
67
+ mesh finer towards a coastline, a source region or a station network
68
+ without coarsening anything elsewhere. Every mesh comes with a JSON
69
+ manifest saying what each attribute means, and exports to MFEM together
70
+ with the mapping's displacement and the model's fields.
71
+
72
+ **Executable contracts.** `planetmodel.testing` holds one `check_*`
73
+ function per protocol, and the shipped implementations and your own are
74
+ held to the same call.
75
+
76
+ ## Installing
77
+
78
+ ```
79
+ pip install planetmodel # numpy and scipy only
80
+ pip install 'planetmodel[meshing]' # 2D and 3D meshes via gmsh
81
+ pip install 'planetmodel[mfem]' # export to MFEM (PyMFEM)
82
+ pip install 'planetmodel[harmonics]' # grid transforms via pyshtools and ducc0
83
+ pip install 'planetmodel[plot]' # matplotlib, for the figures
84
+ pip install 'planetmodel[notebook]' # ipykernel, to run tutorials cell by cell
85
+ ```
86
+
87
+ Python 3.12 or later. Nothing optional is imported by `import
88
+ planetmodel`.
89
+
90
+ ## A first look
91
+
92
+ ```python
93
+ from planetmodel import PREM, RadialMesh, flattening, gravity
94
+
95
+ m = PREM() # exact polynomials, SI units
96
+
97
+ # Fields live on layers, so a discontinuity gives two honest answers,
98
+ # one from each side.
99
+ r_cmb = m.geometry.interface("cmb").radius
100
+ print(m.layer("outer_core")["rho"](r_cmb)) # density, core side
101
+ print(m.layer("lowermost_mantle")["rho"](r_cmb)) # density, mantle side
102
+ print(gravity(m, [r_cmb, 6371e3])) # gravity at the CMB and surface
103
+
104
+ # Reshape the model: non-dimensionalise it, flatten it by 1/300, and
105
+ # lay a radial spectral-element mesh over it.
106
+ nd = m.nondimensionalised().stretched(flattening(1 / 300, rmax=1.0))
107
+ mesh = RadialMesh(nd, ngll=5, drmax=0.05)
108
+ print(mesh.nodal(nd, "rho").shape) # density at every mesh node
109
+ ```
110
+
111
+ ## Where to go next
112
+
113
+ - `examples/tutorials/`: eleven walkthroughs, from a skeleton to random
114
+ fields and deck files, each a `# %%` script that runs headless.
115
+ - `src/planetmodel/mesh3d/manifest.py`: the manifest beside every mesh,
116
+ its schema described from the consumer's side.
117
+ - `CONTRIBUTING.md`: the development setup, the hooks, the test
118
+ selections and how a release is made.
119
+
120
+ ## Tests
121
+
122
+ ```
123
+ poetry run pytest # the fast suite
124
+ poetry run pytest -m "not slow" # with gmsh and MFEM
125
+ poetry run ruff check .
126
+ ```
127
+
128
+ ## Licence
129
+
130
+ BSD-3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "planetmodel"
3
- version = "1.2.3"
3
+ version = "1.2.4"
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 = [
@@ -104,7 +104,7 @@ from .units import EARTH_MEAN_DENSITY, G_SI, Dimensions, Scales
104
104
  from .vocabulary import CONSTANTS, VOCABULARY, Constant, FieldSpec
105
105
  from . import testing # noqa: F401
106
106
 
107
- __version__ = "1.2.3"
107
+ __version__ = "1.2.4"
108
108
 
109
109
  __all__ = [
110
110
  "Skeleton",
@@ -146,10 +146,12 @@ class Deck:
146
146
 
147
147
  @property
148
148
  def names(self) -> tuple[str, ...]:
149
+ """The column names, in the deck's order."""
149
150
  return tuple(self.columns)
150
151
 
151
152
  @property
152
153
  def nknots(self) -> int:
154
+ """The number of knots, repeated radii counted twice."""
153
155
  return self.radius.size
154
156
 
155
157
  def __getitem__(self, name: str) -> np.ndarray:
@@ -189,6 +191,7 @@ class Deck:
189
191
 
190
192
  @property
191
193
  def nlayers(self) -> int:
194
+ """The number of layers the repeated radii cut the deck into."""
192
195
  return len(self.layers())
193
196
 
194
197
  def __repr__(self) -> str:
@@ -63,7 +63,9 @@ class RadialDisplacement(Protocol):
63
63
  """
64
64
 
65
65
  def __call__(self, r: ArrayLike, theta: ArrayLike, phi: ArrayLike
66
- ) -> np.ndarray: ...
66
+ ) -> np.ndarray:
67
+ """The displacement at the broadcast coordinates."""
68
+ ...
67
69
 
68
70
 
69
71
  def _broadcast(r: ArrayLike, theta: ArrayLike, phi: ArrayLike
@@ -80,19 +82,23 @@ class ZeroDisplacement:
80
82
  knots: tuple[float, ...] = ()
81
83
 
82
84
  def __call__(self, r: ArrayLike, theta: ArrayLike, phi: ArrayLike) -> np.ndarray:
85
+ """Zeros of the broadcast shape."""
83
86
  r, _, _ = _broadcast(r, theta, phi)
84
87
  return np.zeros(r.shape)
85
88
 
86
89
  def radial_derivative(self, r: ArrayLike, theta: ArrayLike,
87
90
  phi: ArrayLike) -> np.ndarray:
91
+ """dh/dr: zeros of the broadcast shape."""
88
92
  return self(r, theta, phi)
89
93
 
90
94
  def angular_gradient(self, r: ArrayLike, theta: ArrayLike, phi: ArrayLike
91
95
  ) -> tuple[np.ndarray, np.ndarray]:
96
+ """(dh/dtheta, dh/dphi): zeros of the broadcast shape."""
92
97
  z = self(r, theta, phi)
93
98
  return z, z.copy()
94
99
 
95
100
  def bounds(self) -> tuple[float, float]:
101
+ """(min h, max h): both zero."""
96
102
  return 0.0, 0.0
97
103
 
98
104
  def __repr__(self) -> str:
@@ -125,6 +131,7 @@ class CallableDisplacement:
125
131
  self.name = name
126
132
 
127
133
  def __call__(self, r: ArrayLike, theta: ArrayLike, phi: ArrayLike) -> np.ndarray:
134
+ """The wrapped callable at the broadcast coordinates."""
128
135
  r, theta, phi = _broadcast(r, theta, phi)
129
136
  return np.asarray(self._fn(r, theta, phi), dtype=float)
130
137
 
@@ -235,10 +242,12 @@ class LayerLinear:
235
242
 
236
243
  @property
237
244
  def boundaries(self) -> np.ndarray:
245
+ """The skeleton boundaries the reliefs sit on, innermost first."""
238
246
  return self._b
239
247
 
240
248
  @property
241
249
  def reliefs(self) -> tuple[Relief | None, ...]:
250
+ """One relief per boundary, None where a boundary carries none."""
242
251
  return tuple(self._reliefs)
243
252
 
244
253
  def _relief(self, j: int, theta: np.ndarray, phi: np.ndarray) -> np.ndarray:
@@ -264,6 +273,7 @@ class LayerLinear:
264
273
  return r, theta, phi, lo, hi, below, above
265
274
 
266
275
  def __call__(self, r: ArrayLike, theta: ArrayLike, phi: ArrayLike) -> np.ndarray:
276
+ """The reliefs of the point's layer, interpolated linearly in r."""
267
277
  r, theta, phi, lo, hi, below, above = self._pieces(r, theta, phi)
268
278
  t = (r - lo) / (hi - lo)
269
279
  return (1.0 - t) * below + t * above
@@ -7,10 +7,9 @@ no units: a discontinuity is two layers asked separately, and what the
7
7
  numbers mean is the model's business.
8
8
 
9
9
  `evaluate(r, theta, phi)` broadcasts its coordinates and returns float64
10
- (complex128 for a complex-valued field, as a model frozen at a frequency
11
- holds)
12
- of the broadcast shape followed by the character's Voigt shape for
13
- ranks 2 and 4, or its component shape otherwise. Components are given
10
+ — complex128 for a complex-valued field, such as a model frozen at a
11
+ frequency holds — of the broadcast shape followed by the character's
12
+ Voigt shape for ranks 2 and 4, or its component shape otherwise. Components are given
14
13
  in the local spherical frame (e_r, e_theta, e_phi) at the point unless
15
14
  `frame="cartesian"` asks for Cartesian ones; a rank-1 field rotates as
16
15
  R v, a Voigt rank-2 as M v and a Voigt rank-4 as M C M^T with M the Bond
@@ -78,7 +77,10 @@ class Field(Protocol):
78
77
  name: str | None
79
78
 
80
79
  def evaluate(self, r: ArrayLike, theta: ArrayLike, phi: ArrayLike, *,
81
- frame: str = "spherical") -> np.ndarray: ...
80
+ frame: str = "spherical") -> np.ndarray:
81
+ """The components at the broadcast coordinates, in `frame`:
82
+ the broadcast shape followed by the character's stored shape."""
83
+ ...
82
84
 
83
85
 
84
86
  def _interval(interval: tuple[float, float]) -> tuple[float, float]:
@@ -116,14 +118,17 @@ class FieldBase:
116
118
 
117
119
  @property
118
120
  def interval(self) -> tuple[float, float]:
121
+ """The (lo, hi) span of radius the field lives on."""
119
122
  return self._interval
120
123
 
121
124
  @property
122
125
  def character(self) -> Character:
126
+ """The field's law under a mapping: rank, weight, Voigt or not."""
123
127
  return self._character
124
128
 
125
129
  @property
126
130
  def name(self) -> str | None:
131
+ """The field's name, or None for an unnamed one."""
127
132
  return self._name
128
133
 
129
134
  @property
@@ -133,6 +138,7 @@ class FieldBase:
133
138
 
134
139
  @property
135
140
  def stored_shape(self) -> tuple[int, ...]:
141
+ """The trailing shape of the values: `stored_shape` of the character."""
136
142
  return stored_shape(self._character)
137
143
 
138
144
  # -- the question -------------------------------------------------------
@@ -425,6 +431,7 @@ class RadialField(FieldBase):
425
431
  name=self._name)
426
432
 
427
433
  def renamed(self, name: str | None) -> "RadialField":
434
+ """The same field under `name`."""
428
435
  return self._map(lambda f: f, name=name)
429
436
 
430
437
  def __repr__(self) -> str:
@@ -490,6 +497,7 @@ class AnalyticField(FieldBase):
490
497
  rtol=self._rtol)
491
498
 
492
499
  def renamed(self, name: str | None) -> "AnalyticField":
500
+ """The same field under `name`."""
493
501
  return AnalyticField(self._interval, self._fn, character=self._character,
494
502
  name=name, frame=self._frame, rtol=self._rtol)
495
503
 
@@ -562,10 +570,12 @@ class ComposedField(FieldBase):
562
570
 
563
571
  @property
564
572
  def fn(self) -> Callable[..., ArrayLike]:
573
+ """The pointwise function applied to the sources' values."""
565
574
  return self._fn
566
575
 
567
576
  @property
568
577
  def sources(self) -> tuple[Field, ...]:
578
+ """The fields whose values the function is applied to."""
569
579
  return self._sources
570
580
 
571
581
  def _values(self, r: np.ndarray, theta: np.ndarray | None,
@@ -575,6 +585,7 @@ class ComposedField(FieldBase):
575
585
  return to_stored(raw, r.shape, self._character, self)
576
586
 
577
587
  def on_interval(self, lo: float, hi: float) -> "ComposedField":
588
+ """The same composition with every source restated on (lo, hi)."""
578
589
  return ComposedField(self._fn, [s.on_interval(lo, hi) for s in self._sources],
579
590
  character=self._character, name=self._name)
580
591
 
@@ -587,6 +598,7 @@ class ComposedField(FieldBase):
587
598
  character=self._character, name=self._name)
588
599
 
589
600
  def renamed(self, name: str | None) -> "ComposedField":
601
+ """The same field under `name`."""
590
602
  return ComposedField(self._fn, self._sources, character=self._character,
591
603
  name=name)
592
604
 
@@ -161,10 +161,12 @@ class Geometry:
161
161
 
162
162
  @property
163
163
  def skeleton(self) -> Skeleton:
164
+ """The boundary radii the geometry is built over."""
164
165
  return self._sk
165
166
 
166
167
  @property
167
168
  def mapping(self) -> Mapping:
169
+ """The mapping taking the reference body to the physical one."""
168
170
  return self._m
169
171
 
170
172
  @property
@@ -174,6 +176,7 @@ class Geometry:
174
176
 
175
177
  @property
176
178
  def nlayers(self) -> int:
179
+ """The number of layers."""
177
180
  return self._sk.nlayers
178
181
 
179
182
  @property
@@ -183,15 +186,19 @@ class Geometry:
183
186
 
184
187
  @property
185
188
  def is_hollow(self) -> bool:
189
+ """Whether the innermost boundary is above zero."""
186
190
  return self._sk.is_hollow
187
191
 
188
192
  @property
189
193
  def layers(self) -> tuple[LayerInfo, ...]:
194
+ """Index, interval and name of every layer, centre outward."""
190
195
  return tuple(LayerInfo(i, self._sk.interval(i), name=self._layer_names[i])
191
196
  for i in range(self._sk.nlayers))
192
197
 
193
198
  @property
194
199
  def interfaces(self) -> tuple[InterfaceInfo, ...]:
200
+ """Index, radius, `between` and name of every interface, centre
201
+ outward; see the module docstring for the numbering."""
195
202
  b = self._sk.boundaries
196
203
  L = self._sk.nlayers
197
204
  faces = []
@@ -78,15 +78,25 @@ class LayerFunction(Protocol):
78
78
 
79
79
  interval: tuple[float, float]
80
80
 
81
- def __call__(self, r: ArrayLike) -> np.ndarray: ...
81
+ def __call__(self, r: ArrayLike) -> np.ndarray:
82
+ """The values at `r`, of its shape."""
83
+ ...
82
84
 
83
- def derivative(self, *, nu: int = 1) -> LayerFunction: ...
85
+ def derivative(self, *, nu: int = 1) -> LayerFunction:
86
+ """The nu-th derivative as a layer function; nu=0 is the function."""
87
+ ...
84
88
 
85
- def integrate(self, a: float, b: float) -> float | complex: ...
89
+ def integrate(self, a: float, b: float) -> float | complex:
90
+ """The signed integral from a to b."""
91
+ ...
86
92
 
87
- def on_interval(self, lo: float, hi: float) -> LayerFunction: ...
93
+ def on_interval(self, lo: float, hi: float) -> LayerFunction:
94
+ """The same function restated on [lo, hi]."""
95
+ ...
88
96
 
89
- def rescaled(self, *, k: float, v: float) -> LayerFunction: ...
97
+ def rescaled(self, *, k: float, v: float) -> LayerFunction:
98
+ """v f(r / k) on the interval scaled by k."""
99
+ ...
90
100
 
91
101
 
92
102
  #: What `as_layer_function` accepts: a layer function, a scipy `PPoly` or
@@ -137,6 +147,7 @@ class PolynomialLayer:
137
147
 
138
148
  @property
139
149
  def interval(self) -> tuple[float, float]:
150
+ """The (lo, hi) span of radius the function is stated on."""
140
151
  return self._interval
141
152
 
142
153
  @property
@@ -146,12 +157,15 @@ class PolynomialLayer:
146
157
 
147
158
  @property
148
159
  def degree(self) -> int:
160
+ """The polynomial degree of the pieces."""
149
161
  return self._p.c.shape[0] - 1
150
162
 
151
163
  def __call__(self, r: ArrayLike) -> np.ndarray:
164
+ """The polynomial at `r`, of its shape."""
152
165
  return as_values(self._p(np.asarray(r, dtype=float)))
153
166
 
154
167
  def derivative(self, *, nu: int = 1) -> "PolynomialLayer":
168
+ """The nu-th derivative, exact on the coefficients."""
155
169
  if nu < 0:
156
170
  raise ValueError("nu must be non-negative")
157
171
  if nu == 0:
@@ -324,6 +338,7 @@ class NumericLayer:
324
338
 
325
339
  @property
326
340
  def interval(self) -> tuple[float, float]:
341
+ """The (lo, hi) span of radius the function is stated on."""
327
342
  return self._interval
328
343
 
329
344
  @property
@@ -332,11 +347,14 @@ class NumericLayer:
332
347
  return self._fn
333
348
 
334
349
  def __call__(self, r: ArrayLike) -> np.ndarray:
350
+ """The callable at `r`, broadcast to its shape."""
335
351
  r = np.asarray(r, dtype=float)
336
352
  out = as_values(self._fn(r))
337
353
  return np.broadcast_to(out, r.shape).copy() if out.shape != r.shape else out
338
354
 
339
355
  def derivative(self, *, nu: int = 1) -> "NumericLayer":
356
+ """The nu-th derivative: the supplied one, else a central
357
+ difference with a step relative to the interval's width."""
340
358
  if nu < 0:
341
359
  raise ValueError("nu must be non-negative")
342
360
  if nu == 0:
@@ -371,6 +389,7 @@ class NumericLayer:
371
389
  return NumericLayer((lo, hi), self._fn, derivative=self._d, dstep=self._dstep)
372
390
 
373
391
  def rescaled(self, *, k: float, v: float) -> "NumericLayer":
392
+ """v f(r / k) as a closure, on the interval scaled by k."""
374
393
  k, v = float(k), float(v)
375
394
  if k <= 0.0:
376
395
  raise ValueError(f"the coordinate factor must be positive, got {k}")