planetmodel 1.2.4__tar.gz → 1.3.0__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 (86) hide show
  1. {planetmodel-1.2.4 → planetmodel-1.3.0}/PKG-INFO +24 -14
  2. {planetmodel-1.2.4 → planetmodel-1.3.0}/README.md +19 -10
  3. {planetmodel-1.2.4 → planetmodel-1.3.0}/pyproject.toml +32 -7
  4. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/__init__.py +47 -8
  5. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/behaviours.py +107 -38
  6. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/catalogue.py +10 -4
  7. planetmodel-1.3.0/src/planetmodel/conversion/__init__.py +66 -0
  8. planetmodel-1.3.0/src/planetmodel/conversion/building.py +394 -0
  9. planetmodel-1.3.0/src/planetmodel/conversion/laws.py +102 -0
  10. planetmodel-1.3.0/src/planetmodel/conversion/lithosphere.py +218 -0
  11. planetmodel-1.3.0/src/planetmodel/conversion/parameters.py +285 -0
  12. planetmodel-1.3.0/src/planetmodel/conversion/profiles.py +120 -0
  13. planetmodel-1.3.0/src/planetmodel/conversion/prony.py +222 -0
  14. planetmodel-1.3.0/src/planetmodel/conversion/tables.py +189 -0
  15. planetmodel-1.3.0/src/planetmodel/datasets/__init__.py +100 -0
  16. planetmodel-1.3.0/src/planetmodel/datasets/building.py +548 -0
  17. planetmodel-1.3.0/src/planetmodel/datasets/coefficients.py +126 -0
  18. planetmodel-1.3.0/src/planetmodel/datasets/decks.py +66 -0
  19. planetmodel-1.3.0/src/planetmodel/datasets/entries.py +699 -0
  20. planetmodel-1.3.0/src/planetmodel/datasets/fetch.py +312 -0
  21. planetmodel-1.3.0/src/planetmodel/datasets/grids.py +910 -0
  22. planetmodel-1.3.0/src/planetmodel/datasets/models.py +196 -0
  23. planetmodel-1.3.0/src/planetmodel/datasets/processing.py +502 -0
  24. planetmodel-1.3.0/src/planetmodel/datasets/projections.py +230 -0
  25. planetmodel-1.3.0/src/planetmodel/datasets/registry.py +155 -0
  26. planetmodel-1.3.0/src/planetmodel/datasets/sph.py +230 -0
  27. planetmodel-1.3.0/src/planetmodel/datasets/strata.py +429 -0
  28. planetmodel-1.3.0/src/planetmodel/datasets/surfaces.py +238 -0
  29. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/deck.py +225 -27
  30. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/displacement.py +170 -44
  31. planetmodel-1.3.0/src/planetmodel/expansions.py +284 -0
  32. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/fields.py +57 -21
  33. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/frames.py +54 -5
  34. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/geometry.py +24 -41
  35. planetmodel-1.3.0/src/planetmodel/gridded.py +555 -0
  36. planetmodel-1.3.0/src/planetmodel/harmonics.py +395 -0
  37. planetmodel-1.3.0/src/planetmodel/lateral.py +434 -0
  38. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/layerfunction.py +88 -31
  39. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mapping.py +208 -88
  40. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/materials.py +44 -12
  41. planetmodel-1.3.0/src/planetmodel/means.py +94 -0
  42. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh1d/__init__.py +4 -3
  43. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh1d/gll.py +4 -3
  44. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh1d/gravity.py +90 -11
  45. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh1d/mesh.py +118 -21
  46. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/_geometry.py +18 -2
  47. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/_sizing.py +25 -8
  48. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/_tagging.py +1 -17
  49. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/export.py +106 -26
  50. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/layered.py +2 -1
  51. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/manifest.py +49 -7
  52. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/spec.py +27 -7
  53. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/model.py +67 -16
  54. planetmodel-1.3.0/src/planetmodel/modelfile.py +1551 -0
  55. planetmodel-1.3.0/src/planetmodel/perturbation.py +259 -0
  56. planetmodel-1.3.0/src/planetmodel/plotting/__init__.py +33 -0
  57. planetmodel-1.3.0/src/planetmodel/plotting/maps.py +81 -0
  58. planetmodel-1.3.0/src/planetmodel/plotting/profiles.py +120 -0
  59. planetmodel-1.3.0/src/planetmodel/plotting/sections.py +106 -0
  60. planetmodel-1.3.0/src/planetmodel/plotting/spectra.py +81 -0
  61. planetmodel-1.3.0/src/planetmodel/plotting/style.py +86 -0
  62. planetmodel-1.3.0/src/planetmodel/plotting/vtk.py +210 -0
  63. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/pushforward.py +16 -5
  64. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/randomfield/__init__.py +4 -4
  65. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/randomfield/basis.py +11 -10
  66. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/randomfield/fields.py +17 -25
  67. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/randomfield/operator.py +37 -12
  68. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/rheology.py +106 -13
  69. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/sampling.py +220 -45
  70. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/skeleton.py +36 -14
  71. planetmodel-1.3.0/src/planetmodel/slices.py +1076 -0
  72. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/testing.py +493 -28
  73. planetmodel-1.3.0/src/planetmodel/units.py +395 -0
  74. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/vocabulary.py +69 -6
  75. planetmodel-1.2.4/src/planetmodel/harmonics.py +0 -205
  76. planetmodel-1.2.4/src/planetmodel/plotting.py +0 -99
  77. planetmodel-1.2.4/src/planetmodel/units.py +0 -206
  78. {planetmodel-1.2.4 → planetmodel-1.3.0}/LICENSE +0 -0
  79. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/character.py +0 -0
  80. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/__init__.py +0 -0
  81. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/_orient.py +0 -0
  82. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/_session.py +0 -0
  83. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/_validate.py +0 -0
  84. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/_writer.py +0 -0
  85. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/mesh3d/offset.py +0 -0
  86. {planetmodel-1.2.4 → planetmodel-1.3.0}/src/planetmodel/randomfield/mesh.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: planetmodel
3
- Version: 1.2.4
3
+ Version: 1.3.0
4
4
  Summary: Spherically layered planetary models: reference bodies, fields, mappings and meshes
5
5
  License-Expression: BSD-3-Clause
6
6
  License-File: LICENSE
@@ -14,26 +14,32 @@ Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Topic :: Scientific/Engineering :: Physics
17
- Provides-Extra: harmonics
17
+ Provides-Extra: maps
18
18
  Provides-Extra: meshing
19
19
  Provides-Extra: mfem
20
20
  Provides-Extra: netcdf
21
21
  Provides-Extra: notebook
22
22
  Provides-Extra: plot
23
- Requires-Dist: ducc0 (>=0.41) ; extra == "harmonics"
23
+ Requires-Dist: cartopy (>=0.24,<1) ; extra == "maps"
24
+ Requires-Dist: ducc0 (>=0.41,<1)
24
25
  Requires-Dist: gmsh (>=4.13) ; extra == "meshing"
25
26
  Requires-Dist: ipykernel (>=6.29) ; extra == "notebook"
27
+ Requires-Dist: matplotlib (>=3.11,<4) ; extra == "maps"
26
28
  Requires-Dist: matplotlib (>=3.11,<4) ; extra == "plot"
27
29
  Requires-Dist: mfem (>=4.8) ; extra == "mfem"
28
30
  Requires-Dist: netCDF4 (>=1.7) ; extra == "netcdf"
29
31
  Requires-Dist: numpy (>=2.0,<3)
30
- Requires-Dist: pyshtools (>=4.14) ; extra == "harmonics"
31
32
  Requires-Dist: scipy (>=1.15,<2)
32
33
  Project-URL: Homepage, https://github.com/da380/planetmodel
33
34
  Project-URL: Repository, https://github.com/da380/planetmodel
34
35
  Description-Content-Type: text/markdown
35
36
 
36
- # planetmodel
37
+ # planetmodel: A library for spherical and aspherical planetary models
38
+
39
+ [![CI](https://github.com/da380/planetmodel/actions/workflows/ci.yml/badge.svg)](https://github.com/da380/planetmodel/actions/workflows/ci.yml)
40
+ [![PyPI version](https://img.shields.io/pypi/v/planetmodel.svg)](https://pypi.org/project/planetmodel/)
41
+ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
42
+ [![Documentation Status](https://readthedocs.org/projects/planetmodel/badge/?version=latest)](https://planetmodel.readthedocs.io/en/latest/?badge=latest)
37
43
 
38
44
  Spherically layered planetary models — a planet described as concentric
39
45
  layers with physical properties on each — and the meshes that hand such
@@ -111,16 +117,19 @@ held to the same call.
111
117
  ## Installing
112
118
 
113
119
  ```
114
- pip install planetmodel # numpy and scipy only
120
+ pip install planetmodel # numpy, scipy and ducc0
115
121
  pip install 'planetmodel[meshing]' # 2D and 3D meshes via gmsh
116
122
  pip install 'planetmodel[mfem]' # export to MFEM (PyMFEM)
117
- pip install 'planetmodel[harmonics]' # grid transforms via pyshtools and ducc0
123
+ pip install 'planetmodel[netcdf]' # the model file (netCDF4)
118
124
  pip install 'planetmodel[plot]' # matplotlib, for the figures
119
125
  pip install 'planetmodel[notebook]' # ipykernel, to run tutorials cell by cell
120
126
  ```
121
127
 
122
- Python 3.12 or later. Nothing optional is imported by `import
123
- planetmodel`.
128
+ Python 3.12 or later. ducc0 does the spherical-harmonic transforms;
129
+ it ships wheels for Linux on x86_64, macOS on Apple silicon and
130
+ Windows, and elsewhere pip builds it from source, which needs a C++17
131
+ compiler. `import planetmodel` loads numpy and scipy alone: ducc0 and
132
+ every extra are imported where they are used.
124
133
 
125
134
  ## A first look
126
135
 
@@ -138,17 +147,18 @@ print(gravity(m, [r_cmb, 6371e3])) # gravity at the CMB and surf
138
147
 
139
148
  # Reshape the model: non-dimensionalise it, flatten it by 1/300, and
140
149
  # lay a radial spectral-element mesh over it.
141
- nd = m.nondimensionalised().stretched(flattening(1 / 300, rmax=1.0))
150
+ nd = m.nondimensionalised().stretched(flattening(1 / 300))
142
151
  mesh = RadialMesh(nd, ngll=5, drmax=0.05)
143
152
  print(mesh.nodal(nd, "rho").shape) # density at every mesh node
144
153
  ```
145
154
 
146
155
  ## Where to go next
147
156
 
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.
157
+ - `examples/tutorials/`: twelve walkthroughs, from a skeleton to random
158
+ fields, deck files and a laterally varying model in a model file, each
159
+ a `# %%` script that runs headless.
160
+ - `docs/source/formats/`: the model file and the manifest beside every
161
+ mesh, described for the codes that read them (also on Read the Docs).
152
162
  - `CONTRIBUTING.md`: the development setup, the hooks, the test
153
163
  selections and how a release is made.
154
164
 
@@ -1,4 +1,9 @@
1
- # planetmodel
1
+ # planetmodel: A library for spherical and aspherical planetary models
2
+
3
+ [![CI](https://github.com/da380/planetmodel/actions/workflows/ci.yml/badge.svg)](https://github.com/da380/planetmodel/actions/workflows/ci.yml)
4
+ [![PyPI version](https://img.shields.io/pypi/v/planetmodel.svg)](https://pypi.org/project/planetmodel/)
5
+ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
6
+ [![Documentation Status](https://readthedocs.org/projects/planetmodel/badge/?version=latest)](https://planetmodel.readthedocs.io/en/latest/?badge=latest)
2
7
 
3
8
  Spherically layered planetary models — a planet described as concentric
4
9
  layers with physical properties on each — and the meshes that hand such
@@ -76,16 +81,19 @@ held to the same call.
76
81
  ## Installing
77
82
 
78
83
  ```
79
- pip install planetmodel # numpy and scipy only
84
+ pip install planetmodel # numpy, scipy and ducc0
80
85
  pip install 'planetmodel[meshing]' # 2D and 3D meshes via gmsh
81
86
  pip install 'planetmodel[mfem]' # export to MFEM (PyMFEM)
82
- pip install 'planetmodel[harmonics]' # grid transforms via pyshtools and ducc0
87
+ pip install 'planetmodel[netcdf]' # the model file (netCDF4)
83
88
  pip install 'planetmodel[plot]' # matplotlib, for the figures
84
89
  pip install 'planetmodel[notebook]' # ipykernel, to run tutorials cell by cell
85
90
  ```
86
91
 
87
- Python 3.12 or later. Nothing optional is imported by `import
88
- planetmodel`.
92
+ Python 3.12 or later. ducc0 does the spherical-harmonic transforms;
93
+ it ships wheels for Linux on x86_64, macOS on Apple silicon and
94
+ Windows, and elsewhere pip builds it from source, which needs a C++17
95
+ compiler. `import planetmodel` loads numpy and scipy alone: ducc0 and
96
+ every extra are imported where they are used.
89
97
 
90
98
  ## A first look
91
99
 
@@ -103,17 +111,18 @@ print(gravity(m, [r_cmb, 6371e3])) # gravity at the CMB and surf
103
111
 
104
112
  # Reshape the model: non-dimensionalise it, flatten it by 1/300, and
105
113
  # lay a radial spectral-element mesh over it.
106
- nd = m.nondimensionalised().stretched(flattening(1 / 300, rmax=1.0))
114
+ nd = m.nondimensionalised().stretched(flattening(1 / 300))
107
115
  mesh = RadialMesh(nd, ngll=5, drmax=0.05)
108
116
  print(mesh.nodal(nd, "rho").shape) # density at every mesh node
109
117
  ```
110
118
 
111
119
  ## Where to go next
112
120
 
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.
121
+ - `examples/tutorials/`: twelve walkthroughs, from a skeleton to random
122
+ fields, deck files and a laterally varying model in a model file, each
123
+ a `# %%` script that runs headless.
124
+ - `docs/source/formats/`: the model file and the manifest beside every
125
+ mesh, described for the codes that read them (also on Read the Docs).
117
126
  - `CONTRIBUTING.md`: the development setup, the hooks, the test
118
127
  selections and how a release is made.
119
128
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "planetmodel"
3
- version = "1.2.4"
3
+ version = "1.3.0"
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 = [
@@ -21,6 +21,7 @@ requires-python = ">=3.12"
21
21
  dependencies = [
22
22
  "numpy>=2.0,<3",
23
23
  "scipy>=1.15,<2",
24
+ "ducc0>=0.41,<1",
24
25
  ]
25
26
 
26
27
  [project.urls]
@@ -32,20 +33,35 @@ meshing = ["gmsh>=4.13"]
32
33
  mfem = ["mfem>=4.8"]
33
34
  netcdf = ["netCDF4>=1.7"]
34
35
  plot = ["matplotlib>=3.11,<4"]
36
+ maps = ["matplotlib>=3.11,<4", "cartopy>=0.24,<1"]
35
37
  notebook = ["ipykernel>=6.29"]
36
- harmonics = ["pyshtools (>=4.14)", "ducc0 (>=0.41)"]
37
38
 
38
39
  [dependency-groups]
39
40
  dev = [
40
41
  "pytest>=9",
42
+ # opens model files in the tests as CF tools do
43
+ "xarray>=2024.10",
41
44
  "ruff>=0.6",
42
45
  "pre-commit (>=4)",
43
46
  "pytest-cov (>=6)",
44
47
  ]
48
+ docs = [
49
+ # The lower bound: the apidoc template in docs/apidoc_templates is
50
+ # named package.rst.jinja, the name Sphinx looks for from 7.4
51
+ # onwards. The upper: Sphinx 9's autodoc evaluates PEP 695 `type`
52
+ # aliases, which fails on the names imported under TYPE_CHECKING.
53
+ "sphinx>=7.4,<9",
54
+ "furo>=2024.8",
55
+ ]
45
56
 
46
57
  [tool.poetry]
47
58
  packages = [{include = "planetmodel", from = "src"}]
48
59
 
60
+ # The docs group serves Read the Docs and local documentation builds
61
+ # (`poetry install --with docs`); a plain install leaves it out.
62
+ [tool.poetry.group.docs]
63
+ optional = true
64
+
49
65
  [build-system]
50
66
  requires = ["poetry-core>=2.0.0,<3.0.0"]
51
67
  build-backend = "poetry.core.masonry.api"
@@ -54,15 +70,24 @@ build-backend = "poetry.core.masonry.api"
54
70
  testpaths = ["tests"]
55
71
  pythonpath = ["src"]
56
72
  norecursedirs = [".*", "build", "dist"]
57
- # The fast suite is the default: it needs numpy and scipy alone, and the
58
- # `plot` and `harmonics` extras where installed. `-m "not slow"` adds the
59
- # gmsh unit geometries and the MFEM exports, `-m ""` everything.
60
- addopts = "-m 'not gmsh and not mfem and not slow'"
73
+ # The fast suite is the default: it needs the dependencies alone, and the
74
+ # `plot`, `netcdf` and `maps` extras where installed. `-m "not slow and not
75
+ # network"` adds the gmsh unit geometries and the MFEM exports, `-m "not
76
+ # network"` everything off the network, and `-m network` the registry check,
77
+ # which fetches every data set.
78
+ addopts = "-m 'not gmsh and not mfem and not slow and not network'"
79
+ # netCDF4 1.7.4 sets an array's shape when writing a variable of two or
80
+ # more dimensions, which numpy 2.5 deprecates; nothing of ours to change.
81
+ filterwarnings = [
82
+ "ignore:Setting the shape on a NumPy array:DeprecationWarning",
83
+ ]
61
84
  markers = [
62
85
  "slow: long-running tests (minutes); deselect with -m 'not slow'",
63
86
  "gmsh: requires the optional gmsh dependency (planetmodel[meshing])",
64
87
  "mfem: requires PyMFEM (planetmodel[mfem])",
65
- "harmonics: requires pyshtools (planetmodel[harmonics])",
88
+ "netcdf: requires netCDF4 (planetmodel[netcdf])",
89
+ "maps: requires cartopy (planetmodel[maps])",
90
+ "network: fetches published data sets from their hosts; run with -m network",
66
91
  ]
67
92
 
68
93
  [tool.ruff]
@@ -20,11 +20,19 @@ Modules reached by name, one level down:
20
20
  catalogue PREM, LayeredIsotropicElastic and MineosModel, model types
21
21
  deck tabulated models: decks, their formats, fields by interpolation
22
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)
23
+ lateral values between the nodes of an angular grid: the lateral rules
24
+ modelfile the model file: a model in one netCDF file (netCDF4, optional)
25
+ harmonics real spherical harmonics, through ducc0
26
+ slices a field on a sphere, a section or a profile; spectra and
27
+ correlations by degree
28
+ plotting profiles, sections, maps and spectra drawn (matplotlib and
29
+ cartopy, optional), and VTK files for ParaView
25
30
  mesh1d radial spectral-element meshes (GLL), nodal values, gravity
31
+ and hydrostatic pressure
26
32
  mesh3d 2D and 3D meshes via gmsh, and MFEM export
27
33
  randomfield Matern random fields on balls, annuli and layers
34
+ datasets published models: their entries, fetched on demand into a
35
+ cache, read into the library's objects
28
36
  testing the executable contracts, `check_field` and its kin
29
37
  """
30
38
 
@@ -40,7 +48,10 @@ from .behaviours import (
40
48
  from .catalogue import PREM, LayeredIsotropicElastic, MineosModel
41
49
  from .character import DENSITY, ELASTIC, SCALAR, STRESS, VECTOR, Character, Symmetry
42
50
  from .deck import (
51
+ GIA,
43
52
  MINEOS,
53
+ ND,
54
+ TVEL,
44
55
  Deck,
45
56
  DeckFormat,
46
57
  Tabulated,
@@ -56,6 +67,7 @@ from .displacement import (
56
67
  flattening,
57
68
  layer_linear,
58
69
  )
70
+ from .expansions import HarmonicField, HarmonicFunction
59
71
  from .fields import (
60
72
  AnalyticField,
61
73
  ComposedField,
@@ -64,8 +76,10 @@ from .fields import (
64
76
  RadialField,
65
77
  constant_field,
66
78
  )
79
+ from .frames import from_geographic, geographic
67
80
  from .geometry import Geometry, InterfaceInfo, LayerInfo
68
- from .harmonics import analyse_grid, real_harmonics, synthesise, synthesise_grid
81
+ from .gridded import GriddedDisplacement, GriddedField, GriddedFunction
82
+ from .harmonics import real_harmonics, synthesise
69
83
  from .layerfunction import (
70
84
  LayerFunction,
71
85
  NumericLayer,
@@ -87,9 +101,11 @@ from .mapping import (
87
101
  validity_lattice,
88
102
  )
89
103
  from .materials import ElasticField, elastic_moduli, is_fluid, kappa_mu, moduli
104
+ from .means import spherical_mean, spherically_averaged
90
105
  from .mesh1d import RadialMesh
91
- from .mesh1d.gravity import gravity, gravity_fields, mass
106
+ from .mesh1d.gravity import gravity, gravity_fields, mass, pressure, pressure_fields
92
107
  from .model import CoarseningFit, Layer, Model
108
+ from .perturbation import PerturbedField, perturbed, unperturbed
93
109
  from .pushforward import PulledBackField, PushedForwardField, pull_back, push_forward
94
110
  from .rheology import (
95
111
  dispersive_moduli,
@@ -98,13 +114,20 @@ from .rheology import (
98
114
  is_viscoelastic,
99
115
  reference_omega,
100
116
  )
101
- from .sampling import AngularGrid, Sample, equiangular, gauss_legendre, sample
117
+ from .sampling import (
118
+ AngularGrid,
119
+ Sample,
120
+ driscoll_healy,
121
+ equiangular,
122
+ gauss_legendre,
123
+ sample,
124
+ )
102
125
  from .skeleton import CoarseningMap, Location, Skeleton
103
126
  from .units import EARTH_MEAN_DENSITY, G_SI, Dimensions, Scales
104
127
  from .vocabulary import CONSTANTS, VOCABULARY, Constant, FieldSpec
105
128
  from . import testing # noqa: F401
106
129
 
107
- __version__ = "1.2.4"
130
+ __version__ = "1.3.0"
108
131
 
109
132
  __all__ = [
110
133
  "Skeleton",
@@ -148,6 +171,16 @@ __all__ = [
148
171
  "AnalyticField",
149
172
  "ComposedField",
150
173
  "constant_field",
174
+ "GriddedField",
175
+ "GriddedFunction",
176
+ "GriddedDisplacement",
177
+ "HarmonicField",
178
+ "HarmonicFunction",
179
+ "PerturbedField",
180
+ "perturbed",
181
+ "unperturbed",
182
+ "spherical_mean",
183
+ "spherically_averaged",
151
184
  "ElasticField",
152
185
  "is_fluid",
153
186
  "moduli",
@@ -185,6 +218,9 @@ __all__ = [
185
218
  "Deck",
186
219
  "DeckFormat",
187
220
  "MINEOS",
221
+ "GIA",
222
+ "ND",
223
+ "TVEL",
188
224
  "read_deck",
189
225
  "write_deck",
190
226
  "deck_layers",
@@ -195,14 +231,17 @@ __all__ = [
195
231
  "Sample",
196
232
  "sample",
197
233
  "gauss_legendre",
234
+ "driscoll_healy",
198
235
  "equiangular",
236
+ "geographic",
237
+ "from_geographic",
199
238
  "real_harmonics",
200
239
  "synthesise",
201
- "synthesise_grid",
202
- "analyse_grid",
203
240
  "gravity",
204
241
  "mass",
205
242
  "gravity_fields",
243
+ "pressure",
244
+ "pressure_fields",
206
245
  "testing",
207
246
  "__version__",
208
247
  ]
@@ -39,22 +39,23 @@ import numpy as np
39
39
  from . import materials, rheology
40
40
  from .character import DENSITY, SCALAR
41
41
  from .fields import ComposedField, Field
42
- from .mesh1d.gravity import gravity, gravity_fields, mass
42
+ from .mesh1d.gravity import gravity, gravity_fields, mass, pressure, pressure_fields
43
43
 
44
44
  if TYPE_CHECKING:
45
- from .model import CoarseningFit, Model
45
+ from .model import CoarseningFit, Layer, Model
46
46
 
47
47
  __all__ = ["layer_method", "with_moduli", "with_velocities", "Elastic", "ConstantQ",
48
48
  "SelfGravitating", "Viscoelastic", "ELASTIC_FAMILIES", "elastic_family"]
49
49
 
50
50
  #: The velocity names an elastic description may be given in.
51
- VELOCITY_NAMES = ("vp", "vs", "vpv", "vph", "vsv", "vsh", "eta")
51
+ VELOCITY_NAMES = materials.VELOCITY_NAMES
52
52
 
53
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,
54
+ #: that is complete on its own: the isotropic and the transversely
55
+ #: isotropic velocities beside rho, the isotropic moduli, the five Love
56
+ #: moduli, and the tensor stored under `elastic_moduli`.
57
+ ELASTIC_FAMILIES = (materials.VELOCITY_SETS[materials.Symmetry.ISOTROPIC],
58
+ materials.VELOCITY_SETS[materials.Symmetry.VTI],
58
59
  materials.MODULI_NAMES[materials.Symmetry.ISOTROPIC],
59
60
  materials.MODULI_NAMES[materials.Symmetry.VTI],
60
61
  ("elastic_moduli",))
@@ -69,6 +70,40 @@ def elastic_family(name: str) -> tuple[str, ...]:
69
70
  f"{list(materials.ELASTIC_NAMES)}")
70
71
 
71
72
 
73
+ #: The fields a frozen layer's complex moduli were derived from.
74
+ _FROZEN_FROM = ("rho",) + materials.ELASTIC_NAMES + rheology.RHEOLOGY_NAMES
75
+
76
+
77
+ def _refuse_frozen(model: Model, why: str) -> None:
78
+ """Refuse a change that would leave a frozen model's moduli stale."""
79
+ if rheology.is_frozen(model):
80
+ raise ValueError(
81
+ f"the model is frozen at omega = {float(model.constant('omega')):g} "
82
+ f"and {why}, so the frozen moduli would go stale; make the change "
83
+ "on the elastic model and freeze it again")
84
+
85
+
86
+ def _refuse_other_symmetry(layer: Layer, name: str) -> None:
87
+ """Refuse a velocity of one symmetry on a layer holding velocities of
88
+ the other, naming what the layer holds and the way out."""
89
+ sets = materials.VELOCITY_SETS
90
+ own = next((s for s, names in sets.items() if name in names), None)
91
+ held = [n for s, names in sets.items() if s is not own for n in names
92
+ if n in layer]
93
+ if own is None or not held:
94
+ return
95
+ if own is materials.Symmetry.ISOTROPIC:
96
+ remedy = ("replace one of those, or describe the layer by vp and vs "
97
+ "with isotropic() first")
98
+ else:
99
+ remedy = (f"no one velocity completes {list(sets[own])}; replace one of "
100
+ "the Love moduli A, C, F, L, N instead, and the velocities are "
101
+ "derived again from the five")
102
+ where = f"layer {layer.index}" + (f" ({layer.name!r})" if layer.name else "")
103
+ raise ValueError(f"{where} holds the velocities {held}, so {name!r} would "
104
+ f"contradict them; {remedy}")
105
+
106
+
72
107
  def _description_family(model: Model, group: Sequence[int]) -> tuple[str, ...]:
73
108
  """The elastic names a merged group is refit through: the first
74
109
  description every layer of the group holds, velocities preferred
@@ -160,18 +195,19 @@ class Elastic:
160
195
  vanishing of shear, and `isotropic()` the model re-described by its
161
196
  Voigt average, kappa and mu with the five and the velocities
162
197
  recomputed from them. A layer holding a general anisotropic tensor
163
- is refused by the three that read the five, until the general Voigt
164
- average is written.
198
+ is refused by the three that read the five.
165
199
 
166
200
  The derived fields are kept in step by `with_field`: replacing an
167
201
  elastic field on a layer makes the family it belongs to (rho with
168
- the velocities, kappa and mu, or the five) the description, drops
169
- the other elastic names from that layer, and completes the
170
- description again, so the five follow a new `vs` and the velocities
171
- a new `L`; a field added beside the existing ones is attached as it
172
- is. A change that goes through `replaced` directly, as the
173
- library's own transformations do, is the caller's to keep
174
- consistent.
202
+ the isotropic or the transversely isotropic velocities, kappa and
203
+ mu, or the five) the description, drops the other elastic names from
204
+ that layer, and completes the description again, so the five follow
205
+ a new `vs` and the velocities a new `L`; a field added beside the
206
+ existing ones is attached as it is, and a velocity of the other
207
+ symmetry than the layer's is refused, as is any change to what a
208
+ frozen model's moduli were derived from. A change that goes through
209
+ `replaced` directly, as the library's own transformations do, is the
210
+ caller's to keep consistent.
175
211
  """
176
212
 
177
213
  def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -192,16 +228,30 @@ class Elastic:
192
228
 
193
229
  Replacing an elastic field makes its family the layer's
194
230
  description and derives the other elastic fields from it again;
195
- a field added beside the existing ones, or a family too
196
- incomplete to describe the medium, is attached as it is.
231
+ replacing rho on a layer holding velocities makes the velocities
232
+ the description, so the moduli follow the new density. A field
233
+ added beside the existing ones, or a family too incomplete to
234
+ describe the medium, is attached as it is. A velocity of one
235
+ symmetry on a layer holding velocities of the other is refused,
236
+ added or replaced: no single velocity completes the other set,
237
+ and `isotropic()` re-describes a layer by vp and vs. On a frozen
238
+ model (`rheology.is_frozen`) rho, the elastic names and the
239
+ rheology names are refused, the frozen moduli having been derived
240
+ from them.
197
241
  """
198
242
  from .model import Model
243
+ if name in _FROZEN_FROM or rheology.is_rheology_name(name):
244
+ _refuse_frozen(self, f"its moduli were derived from {name!r}")
245
+ _refuse_other_symmetry(self.layer(which), name)
199
246
  out = Model.with_field(self, which, name, field, replace=replace)
200
- if not replace or name not in materials.ELASTIC_NAMES:
201
- return out
202
247
  i = self.layer(which).index
248
+ if name == "rho" and any(n in out.layer(i) for n in VELOCITY_NAMES):
249
+ keep = VELOCITY_NAMES
250
+ elif not replace or name not in materials.ELASTIC_NAMES:
251
+ return out
252
+ else:
253
+ keep = elastic_family(name)
203
254
  layers = [dict(layer.fields) for layer in out.layers]
204
- keep = elastic_family(name)
205
255
  fields = {k: f for k, f in layers[i].items()
206
256
  if k not in materials.ELASTIC_NAMES or k in keep}
207
257
  try:
@@ -223,9 +273,11 @@ class Elastic:
223
273
  from the fitted description, so the coarse model satisfies the
224
274
  elastic relations exactly. A group whose layers share no such
225
275
  description (one speaking only through `elastic_moduli`, say)
226
- is refused. Layers left whole are untouched.
276
+ is refused, as is a frozen model. Layers left whole are
277
+ untouched.
227
278
  """
228
279
  from .model import Model
280
+ _refuse_frozen(self, "coarsening refits the elastic description")
229
281
  _, cmap = self._coarsening(keep=keep, drop=drop)
230
282
  layers = [dict(layer.fields) for layer in self.layers]
231
283
  merged = [group for group in cmap.layers if len(group) > 1]
@@ -293,12 +345,28 @@ class ConstantQ:
293
345
  name="elastic_moduli")
294
346
 
295
347
 
348
+ def _with_radial(model: Model, fields: Sequence[Field], name: str,
349
+ replace: bool) -> Model:
350
+ """The model with one field per layer attached under `name`, a layer
351
+ already holding it refused unless `replace`."""
352
+ layers = []
353
+ for layer, field in zip(model.layers, fields):
354
+ if name in layer and not replace:
355
+ raise ValueError(
356
+ f"layer {layer.index} ({layer.name!r}) already holds {name!r}; "
357
+ "pass replace=True to replace it")
358
+ layers.append({**layer.fields, name: field})
359
+ return model.replaced(layers=layers)
360
+
361
+
296
362
  class SelfGravitating:
297
- """The gravity and mass of a spherically symmetric model with a radial
298
- density on every layer: the reference body's, computed on each call
299
- and never stored, as numbers by `gravity` and `mass`, as one radial
300
- field per layer by `gravity_fields`, and as a copy of the model
301
- holding that field under the vocabulary name `g` by `with_gravity`;
363
+ """The gravity, mass and pressure of a spherically symmetric model with
364
+ a radial density on every layer: the reference body's, computed on
365
+ each call and never stored, as numbers by `gravity`, `mass` and
366
+ `pressure`, as one radial field per layer by `gravity_fields` and
367
+ `pressure_fields`, and as a copy of the model holding that field
368
+ under the vocabulary name `g` by `with_gravity`, `pressure` by
369
+ `with_pressure`;
302
370
  `with_stratification` attaches d rho / d Phi_0 = rho'(r) / g(r) to
303
371
  the fluid layers. A density that depends on direction is refused,
304
372
  and the geometry's mapping does not enter (see `mesh1d.gravity`)."""
@@ -306,20 +374,21 @@ class SelfGravitating:
306
374
  gravity = gravity
307
375
  mass = mass
308
376
  gravity_fields = gravity_fields
377
+ pressure = pressure
378
+ pressure_fields = pressure_fields
309
379
 
310
380
  def with_gravity(self: Model, *, name: str = "g", replace: bool = False) -> Model:
311
381
  """The model with its gravity attached to every layer as a radial
312
382
  field under `name`, exact where the density is polynomial; a
313
383
  layer already holding `name` is refused unless `replace`."""
314
- fields = gravity_fields(self)
315
- layers = []
316
- for layer, field in zip(self.layers, fields):
317
- if name in layer and not replace:
318
- raise ValueError(
319
- f"layer {layer.index} ({layer.name!r}) already holds {name!r}; "
320
- "pass replace=True to replace it")
321
- layers.append({**layer.fields, name: field})
322
- return self.replaced(layers=layers)
384
+ return _with_radial(self, gravity_fields(self), name, replace)
385
+
386
+ def with_pressure(self: Model, *, name: str = "pressure",
387
+ replace: bool = False) -> Model:
388
+ """The model with its hydrostatic pressure attached to every layer
389
+ as a radial field under `name` (`mesh1d.gravity`); a layer already
390
+ holding `name` is refused unless `replace`."""
391
+ return _with_radial(self, pressure_fields(self), name, replace)
323
392
 
324
393
  def with_stratification(self: Model, *, name: str = "drho_dphi0",
325
394
  replace: bool = False) -> Model:
@@ -373,9 +442,9 @@ class Viscoelastic:
373
442
  frozen = rheology.frozen
374
443
 
375
444
  def elastic(self: Model) -> Model:
376
- """The model without its rheology fields (`RHEOLOGY_NAMES`), so
445
+ """The model without its rheology fields (`rheology.is_rheology_name`), so
377
446
  that every layer is elastic; a frozen model keeps its complex
378
447
  moduli, which are what its rheology became."""
379
448
  layers = [{k: f for k, f in layer.fields.items()
380
- if k not in rheology.RHEOLOGY_NAMES} for layer in self.layers]
449
+ if not rheology.is_rheology_name(k)} for layer in self.layers]
381
450
  return self.replaced(layers=layers, check=False)
@@ -56,6 +56,10 @@ __all__ = ["PREM", "LayeredIsotropicElastic", "MineosModel", "PREM_RADIUS"]
56
56
  #: The normalisation radius a of PREM's polynomials, metres.
57
57
  PREM_RADIUS = 6371e3
58
58
 
59
+ _PREM_CITATION = ("Dziewonski, A. M. and D. L. Anderson (1981). Preliminary "
60
+ "reference Earth model. Phys. Earth Planet. Inter. 25, 297-356, "
61
+ "doi:10.1016/0031-9201(81)90046-7")
62
+
59
63
  #: Table I of Dziewonski and Anderson (1981), centre outward: the radii
60
64
  #: of each region in metres and the polynomial coefficients of each
61
65
  #: column in the paper's units, ascending in x = r / a.
@@ -196,7 +200,8 @@ class PREM(Elastic, ConstantQ, SelfGravitating, Viscoelastic, Model):
196
200
  name=name)
197
201
  layers.append(fields)
198
202
  super().__init__(geometry, layers, scales=Scales.SI,
199
- name="PREM" if ocean else "PREM (no ocean)")
203
+ name="PREM" if ocean else "PREM (no ocean)",
204
+ citation=(_PREM_CITATION,))
200
205
 
201
206
 
202
207
  class LayeredIsotropicElastic(Elastic, SelfGravitating, Model):
@@ -263,11 +268,12 @@ class MineosModel(Elastic, ConstantQ, SelfGravitating, Viscoelastic, Tabulated,
263
268
  becomes the constant `omega_ref` that `moduli_at` and `frozen`
264
269
  disperse about, and its title is the model's name. The deck is
265
270
  kept as `deck`, its knots per layer as `knots` and its header as
266
- `header`, so `to_deck` writes the model back out.
271
+ `header`, so `to_deck` writes the model back out. The model is in
272
+ SI, as the deck is; `converted` gives it in other scales.
267
273
  """
268
274
 
269
275
  def __init__(self, source: str | os.PathLike[str] | Deck, *,
270
- kind: str = "cubic", scales: Scales = Scales.SI) -> None:
276
+ kind: str = "cubic") -> None:
271
277
  deck = source if isinstance(source, Deck) else read_deck(source, MINEOS)
272
278
  skeleton, layers = deck_layers(deck, kind=kind)
273
279
  layer_names, interface_names = mineos_names(deck)
@@ -282,5 +288,5 @@ class MineosModel(Elastic, ConstantQ, SelfGravitating, Viscoelastic, Tabulated,
282
288
  self.deck = deck
283
289
  self.knots = deck_knots(deck)
284
290
  self.header = deck.header
285
- super().__init__(geometry, layers, scales=scales, constants=constants,
291
+ super().__init__(geometry, layers, constants=constants,
286
292
  name=str(deck.header.get("name", "")) or None)