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.
- planetmodel-1.2.4/PKG-INFO +166 -0
- planetmodel-1.2.4/README.md +130 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/pyproject.toml +1 -1
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/__init__.py +1 -1
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/deck.py +3 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/displacement.py +11 -1
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/fields.py +17 -5
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/geometry.py +7 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/layerfunction.py +24 -5
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mapping.py +26 -3
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/materials.py +2 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh1d/__init__.py +8 -1
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/__init__.py +5 -3
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_geometry.py +1 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_sizing.py +48 -13
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/layered.py +9 -4
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/manifest.py +79 -3
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/spec.py +206 -12
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/model.py +18 -1
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/pushforward.py +1 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/randomfield/__init__.py +2 -2
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/sampling.py +2 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/testing.py +42 -1
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/units.py +2 -0
- planetmodel-1.2.3/PKG-INFO +0 -161
- planetmodel-1.2.3/README.md +0 -125
- {planetmodel-1.2.3 → planetmodel-1.2.4}/LICENSE +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/behaviours.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/catalogue.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/character.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/frames.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/harmonics.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh1d/gll.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh1d/gravity.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh1d/mesh.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_orient.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_session.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_tagging.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_validate.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/_writer.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/export.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/mesh3d/offset.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/plotting.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/randomfield/basis.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/randomfield/fields.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/randomfield/mesh.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/randomfield/operator.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/rheology.py +0 -0
- {planetmodel-1.2.3 → planetmodel-1.2.4}/src/planetmodel/skeleton.py +0 -0
- {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
|
+
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.
|
|
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
|
-
|
|
11
|
-
holds
|
|
12
|
-
|
|
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}")
|