earthsciencekit 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. earthsciencekit/__init__.py +6 -0
  2. earthsciencekit/_validation.py +62 -0
  3. earthsciencekit/atmosphere/__init__.py +57 -0
  4. earthsciencekit/atmosphere/core/__init__.py +5 -0
  5. earthsciencekit/atmosphere/core/base.py +10 -0
  6. earthsciencekit/atmosphere/systems/__init__.py +1 -0
  7. earthsciencekit/atmosphere/systems/dynamics.py +208 -0
  8. earthsciencekit/atmosphere/systems/hydrostatics.py +214 -0
  9. earthsciencekit/atmosphere/systems/moisture.py +162 -0
  10. earthsciencekit/atmosphere/systems/thermodynamics.py +222 -0
  11. earthsciencekit/atmosphere/tests/__init__.py +0 -0
  12. earthsciencekit/atmosphere/tests/test_models.py +741 -0
  13. earthsciencekit/atmosphere/tests/test_physics.py +81 -0
  14. earthsciencekit/atmosphere/visualizers/__init__.py +9 -0
  15. earthsciencekit/climate/__init__.py +63 -0
  16. earthsciencekit/climate/core/__init__.py +5 -0
  17. earthsciencekit/climate/core/base.py +10 -0
  18. earthsciencekit/climate/systems/__init__.py +1 -0
  19. earthsciencekit/climate/systems/carbon.py +76 -0
  20. earthsciencekit/climate/systems/forcing.py +124 -0
  21. earthsciencekit/climate/systems/radiation.py +214 -0
  22. earthsciencekit/climate/systems/response.py +402 -0
  23. earthsciencekit/climate/systems/sea_level.py +77 -0
  24. earthsciencekit/climate/tests/__init__.py +0 -0
  25. earthsciencekit/climate/tests/test_models.py +1015 -0
  26. earthsciencekit/climate/tests/test_physics.py +16 -0
  27. earthsciencekit/climate/visualizers/__init__.py +9 -0
  28. earthsciencekit/constants/__init__.py +23 -0
  29. earthsciencekit/geophysics/__init__.py +77 -0
  30. earthsciencekit/geophysics/core/__init__.py +5 -0
  31. earthsciencekit/geophysics/core/base.py +10 -0
  32. earthsciencekit/geophysics/systems/__init__.py +1 -0
  33. earthsciencekit/geophysics/systems/deformation.py +161 -0
  34. earthsciencekit/geophysics/systems/dynamo.py +159 -0
  35. earthsciencekit/geophysics/systems/figure.py +292 -0
  36. earthsciencekit/geophysics/systems/gravity.py +235 -0
  37. earthsciencekit/geophysics/systems/magnetism.py +354 -0
  38. earthsciencekit/geophysics/tests/__init__.py +0 -0
  39. earthsciencekit/geophysics/tests/test_models.py +898 -0
  40. earthsciencekit/geophysics/tests/test_physics.py +171 -0
  41. earthsciencekit/geophysics/visualizers/__init__.py +9 -0
  42. earthsciencekit/integrators/__init__.py +62 -0
  43. earthsciencekit/oceanography/__init__.py +61 -0
  44. earthsciencekit/oceanography/core/__init__.py +5 -0
  45. earthsciencekit/oceanography/core/base.py +10 -0
  46. earthsciencekit/oceanography/systems/__init__.py +1 -0
  47. earthsciencekit/oceanography/systems/circulation.py +388 -0
  48. earthsciencekit/oceanography/systems/hydrography.py +212 -0
  49. earthsciencekit/oceanography/systems/inertial.py +86 -0
  50. earthsciencekit/oceanography/systems/waves.py +281 -0
  51. earthsciencekit/oceanography/tests/__init__.py +0 -0
  52. earthsciencekit/oceanography/tests/test_models.py +916 -0
  53. earthsciencekit/oceanography/tests/test_physics.py +102 -0
  54. earthsciencekit/oceanography/visualizers/__init__.py +9 -0
  55. earthsciencekit/plate_tectonics/__init__.py +59 -0
  56. earthsciencekit/plate_tectonics/core/__init__.py +5 -0
  57. earthsciencekit/plate_tectonics/core/base.py +10 -0
  58. earthsciencekit/plate_tectonics/systems/__init__.py +1 -0
  59. earthsciencekit/plate_tectonics/systems/boundaries.py +201 -0
  60. earthsciencekit/plate_tectonics/systems/cooling.py +238 -0
  61. earthsciencekit/plate_tectonics/systems/isostasy.py +157 -0
  62. earthsciencekit/plate_tectonics/systems/kinematics.py +290 -0
  63. earthsciencekit/plate_tectonics/tests/__init__.py +0 -0
  64. earthsciencekit/plate_tectonics/tests/test_models.py +920 -0
  65. earthsciencekit/plate_tectonics/tests/test_physics.py +94 -0
  66. earthsciencekit/plate_tectonics/visualizers/__init__.py +9 -0
  67. earthsciencekit/py.typed +0 -0
  68. earthsciencekit/results.py +73 -0
  69. earthsciencekit/seismology/__init__.py +59 -0
  70. earthsciencekit/seismology/core/__init__.py +5 -0
  71. earthsciencekit/seismology/core/base.py +10 -0
  72. earthsciencekit/seismology/systems/__init__.py +1 -0
  73. earthsciencekit/seismology/systems/interfaces.py +102 -0
  74. earthsciencekit/seismology/systems/rays.py +221 -0
  75. earthsciencekit/seismology/systems/sources.py +249 -0
  76. earthsciencekit/seismology/systems/waves.py +268 -0
  77. earthsciencekit/seismology/tests/__init__.py +0 -0
  78. earthsciencekit/seismology/tests/test_models.py +852 -0
  79. earthsciencekit/seismology/tests/test_physics.py +100 -0
  80. earthsciencekit/seismology/visualizers/__init__.py +9 -0
  81. earthsciencekit/tests/__init__.py +0 -0
  82. earthsciencekit/tests/test_core.py +181 -0
  83. earthsciencekit/tests/test_relations.py +36 -0
  84. earthsciencekit/units/__init__.py +66 -0
  85. earthsciencekit/visualizers/__init__.py +45 -0
  86. earthsciencekit/volcanology/__init__.py +61 -0
  87. earthsciencekit/volcanology/core/__init__.py +5 -0
  88. earthsciencekit/volcanology/core/base.py +10 -0
  89. earthsciencekit/volcanology/systems/__init__.py +1 -0
  90. earthsciencekit/volcanology/systems/eruptions.py +161 -0
  91. earthsciencekit/volcanology/systems/geochemistry.py +226 -0
  92. earthsciencekit/volcanology/systems/geochronology.py +304 -0
  93. earthsciencekit/volcanology/systems/magma.py +190 -0
  94. earthsciencekit/volcanology/tests/__init__.py +0 -0
  95. earthsciencekit/volcanology/tests/test_models.py +485 -0
  96. earthsciencekit/volcanology/tests/test_physics.py +176 -0
  97. earthsciencekit/volcanology/visualizers/__init__.py +9 -0
  98. earthsciencekit-0.2.0.dist-info/METADATA +127 -0
  99. earthsciencekit-0.2.0.dist-info/RECORD +102 -0
  100. earthsciencekit-0.2.0.dist-info/WHEEL +5 -0
  101. earthsciencekit-0.2.0.dist-info/licenses/LICENSE +15 -0
  102. earthsciencekit-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,102 @@
1
+ """Physical identities, limits and dynamic checks for earthsciencekit.oceanography."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ from earthsciencekit.oceanography.systems.circulation import (
7
+ ekman_spiral_velocity,
8
+ ekman_transport,
9
+ simulate_thermohaline_boxes,
10
+ stommel_streamfunction,
11
+ sverdrup_transport,
12
+ thermohaline_forcing,
13
+ )
14
+ from earthsciencekit.oceanography.systems.hydrography import abyssal_temperature, linear_density, simplified_seawater_density
15
+ from earthsciencekit.oceanography.systems.inertial import simulate_inertial_current
16
+ from earthsciencekit.oceanography.systems.waves import gerstner_orbit_radius
17
+
18
+
19
+ def test_inertial_current_direction_and_damping():
20
+ f, damping = 0.1, 0.01
21
+ time = np.linspace(0, 60, 101)
22
+ result = simulate_inertial_current(f, time, damping=damping)
23
+ np.testing.assert_allclose(result.values[:, 0], np.exp(-damping * time) * np.cos(f * time), atol=3e-8)
24
+ np.testing.assert_allclose(result.values[:, 1], -np.exp(-damping * time) * np.sin(f * time), atol=3e-8)
25
+ undamped = simulate_inertial_current(f, time)
26
+ np.testing.assert_allclose(np.sum(undamped.values**2, axis=1), 1, atol=3e-8)
27
+ southern = simulate_inertial_current(-f, time)
28
+ assert southern.values[1, 1] > 0 > undamped.values[1, 1]
29
+ for coriolis in [0, [f, f]]:
30
+ with pytest.raises(ValueError):
31
+ simulate_inertial_current(coriolis, time)
32
+
33
+
34
+ def test_linear_density_rejects_invalid_reference_linearization():
35
+ with pytest.raises(ValueError):
36
+ linear_density(1000, 1e5, 0, 0.001, 0.001)
37
+
38
+
39
+ def test_ekman_spiral_integrates_to_ekman_transport():
40
+ depth = np.linspace(0, 1000, 100001)
41
+ for coriolis in [1e-4, -1e-4]:
42
+ velocity = ekman_spiral_velocity(depth, 0.1, 1025, 0.05, coriolis)
43
+ assert np.isclose(np.trapezoid(velocity[:, 0], depth), 0, atol=1e-6)
44
+ assert np.isclose(np.trapezoid(velocity[:, 1], depth), ekman_transport(0.1, 1025, coriolis), rtol=1e-6)
45
+ assert np.isclose(abs(np.arctan2(velocity[0, 1], velocity[0, 0])), np.pi / 4)
46
+
47
+
48
+ def test_stommel_interior_is_sverdrup_and_beta_free_gyre_is_symmetric():
49
+ width, length, tau, rho, beta, friction = 6e6, 5e6, 0.1, 1025, 2e-11, 1e-7
50
+ x = np.array([0.3, 0.5, 0.8]) * width
51
+ psi = stommel_streamfunction(x, length / 2, tau, width, length, rho, beta, friction)
52
+ curl = -tau * np.pi / length
53
+ np.testing.assert_allclose(psi, -sverdrup_transport(curl, rho, beta) * (width - x), rtol=0.02)
54
+ symmetric = stommel_streamfunction([0.2 * width, 0.8 * width], length / 3, tau, width, length, rho, 0, friction)
55
+ assert np.isclose(symmetric[0], symmetric[1])
56
+ edges = stommel_streamfunction([0, width, width / 2], [length / 2, length / 2, 0], tau, width, length, rho, beta, friction)
57
+ np.testing.assert_allclose(edges, 0, atol=1e-6 * psi[0])
58
+
59
+
60
+ def test_thermohaline_boxes_reach_both_steady_states():
61
+ thermal, haline, forcing, volume = 3e7, 6e6, 3e7, 1e16
62
+ time = np.linspace(0, 3e10, 61)
63
+ for initial, sign in [(0.0, 1), (8.0, -1)]:
64
+ result = simulate_thermohaline_boxes(thermal, haline, forcing, volume, time, initial_salinity_difference=initial)
65
+ flow = result.values[-1, 1]
66
+ assert np.sign(flow) == sign
67
+ assert np.isclose(thermohaline_forcing(flow, thermal, haline), forcing, rtol=1e-6)
68
+ np.testing.assert_allclose(result.values[:, 1], thermal - haline * result.values[:, 0])
69
+ flows = np.linspace(0, thermal, 1001)
70
+ assert np.isclose(thermohaline_forcing(flows, thermal, haline).max(), thermal**2 / (4 * haline))
71
+ for bad in [[thermal, thermal], 0]:
72
+ with pytest.raises(ValueError):
73
+ simulate_thermohaline_boxes(bad, haline, forcing, volume, time)
74
+
75
+
76
+ def test_abyssal_profile_solves_advection_diffusion():
77
+ z = np.linspace(0, 4000, 4001)
78
+ temperature = abyssal_temperature(z, 274, 284, 4000, 1e-4, 1e-7)
79
+ gradient, curvature = np.gradient(temperature, z), np.gradient(np.gradient(temperature, z), z)
80
+ np.testing.assert_allclose(1e-7 * gradient[5:-5], 1e-4 * curvature[5:-5], rtol=1e-4)
81
+ linear = abyssal_temperature(z, 274, 284, 4000, 1e-4, 1e-12)
82
+ np.testing.assert_allclose(linear, 274 + 10 * z / 4000, atol=1e-3)
83
+
84
+
85
+ def test_simplified_density_cabbeling_and_linear_limit():
86
+ from scipy.optimize import brentq
87
+
88
+ cold = (275.15, 34.6)
89
+ rho_cold = simplified_seawater_density(*cold, 0)
90
+ warm = (293.15, brentq(lambda s: simplified_seawater_density(293.15, s, 0) - rho_cold, 30, 45, xtol=1e-14))
91
+ mixed = simplified_seawater_density((cold[0] + warm[0]) / 2, (cold[1] + warm[1]) / 2, 0)
92
+ assert mixed > rho_cold
93
+ small = np.array([1e-4, -1e-4])
94
+ nonlinear = simplified_seawater_density(283.15 + small, 35 + small[::-1], 0)
95
+ linear = linear_density(1026, small, small[::-1], 0.16550 / 1026, 0.76554 / 1026)
96
+ np.testing.assert_allclose(nonlinear, linear, atol=1e-8)
97
+
98
+
99
+ def test_gerstner_orbits_decay_one_wavelength_down():
100
+ k = 2 * np.pi / 100
101
+ ratio = gerstner_orbit_radius(100, 1.0, k) / gerstner_orbit_radius(0, 1.0, k)
102
+ assert np.isclose(ratio, np.exp(-2 * np.pi))
@@ -0,0 +1,9 @@
1
+ """Plots for earthsciencekit.oceanography.
2
+
3
+ The dynamic models return a TimeSeries, whose ``plot`` and ``animate`` methods
4
+ call these package-wide helpers.
5
+ """
6
+
7
+ from earthsciencekit.visualizers import animate_series, plot_series
8
+
9
+ __all__ = ["plot_series", "animate_series"]
@@ -0,0 +1,59 @@
1
+ """Inspectable plate tectonics models; SI units unless stated otherwise."""
2
+
3
+ from earthsciencekit.plate_tectonics.systems.boundaries import (
4
+ convergence_rate,
5
+ fault_slip_rate,
6
+ full_spreading_rate,
7
+ slab_pull_force,
8
+ spreading_age,
9
+ subduction_travel_time,
10
+ )
11
+ from earthsciencekit.plate_tectonics.systems.cooling import (
12
+ diffuse_temperature,
13
+ halfspace_heat_flux,
14
+ halfspace_temperature,
15
+ mantle_rayleigh_number,
16
+ ocean_depth,
17
+ )
18
+ from earthsciencekit.plate_tectonics.systems.isostasy import (
19
+ airy_root,
20
+ flexural_deflection,
21
+ flexural_parameter,
22
+ flexural_rigidity,
23
+ )
24
+ from earthsciencekit.plate_tectonics.systems.kinematics import (
25
+ angular_speed,
26
+ compose_euler_vectors,
27
+ euler_velocity_x,
28
+ euler_velocity_y,
29
+ euler_velocity_z,
30
+ hotspot_age,
31
+ plate_displacement,
32
+ plate_speed,
33
+ )
34
+
35
+ __all__ = [
36
+ "angular_speed",
37
+ "plate_speed",
38
+ "euler_velocity_x",
39
+ "euler_velocity_y",
40
+ "euler_velocity_z",
41
+ "plate_displacement",
42
+ "spreading_age",
43
+ "full_spreading_rate",
44
+ "convergence_rate",
45
+ "fault_slip_rate",
46
+ "subduction_travel_time",
47
+ "slab_pull_force",
48
+ "airy_root",
49
+ "flexural_rigidity",
50
+ "flexural_parameter",
51
+ "halfspace_temperature",
52
+ "halfspace_heat_flux",
53
+ "mantle_rayleigh_number",
54
+ "flexural_deflection",
55
+ "hotspot_age",
56
+ "ocean_depth",
57
+ "compose_euler_vectors",
58
+ "diffuse_temperature",
59
+ ]
@@ -0,0 +1,5 @@
1
+ """Result containers for earthsciencekit.plate_tectonics."""
2
+
3
+ from earthsciencekit.plate_tectonics.core.base import TimeSeries
4
+
5
+ __all__ = ["TimeSeries"]
@@ -0,0 +1,10 @@
1
+ """Result containers for earthsciencekit.plate_tectonics.
2
+
3
+ Algebraic models return NumPy scalars or arrays. The sampled dynamic models
4
+ return the package-wide immutable :class:`~earthsciencekit.results.TimeSeries`,
5
+ which is re-exported here as this domain's result type.
6
+ """
7
+
8
+ from earthsciencekit.results import TimeSeries
9
+
10
+ __all__ = ["TimeSeries"]
@@ -0,0 +1 @@
1
+ """Models of earthsciencekit.plate_tectonics, grouped by topic."""
@@ -0,0 +1,201 @@
1
+ """Plate boundaries: spreading ridges, transform and convergent margins, subducting slabs and slab pull.
2
+
3
+ SI units unless stated otherwise.
4
+ """
5
+
6
+ import numpy as np
7
+
8
+ from earthsciencekit._validation import model, quantity, require
9
+
10
+ __all__ = [
11
+ "spreading_age",
12
+ "full_spreading_rate",
13
+ "convergence_rate",
14
+ "fault_slip_rate",
15
+ "subduction_travel_time",
16
+ "slab_pull_force",
17
+ ]
18
+
19
+
20
+ @model
21
+ def spreading_age(distance, half_spreading_rate):
22
+ """Magnetic-stripe spreading clock.
23
+
24
+ Symmetric ocean-floor ages link ridge spreading to plate creation.
25
+
26
+ Parameters
27
+ ----------
28
+ distance : float or array_like
29
+ m; constraint: nonnegative.
30
+ half_spreading_rate : float or array_like
31
+ m/s; constraint: positive.
32
+
33
+ Returns
34
+ -------
35
+ numpy scalar or ndarray
36
+ Model value [rad/s]. Scalar input returns a NumPy scalar; arrays broadcast.
37
+
38
+ Notes
39
+ -----
40
+ Distance on one side and half-spreading rate; changes in rate are omitted.
41
+ Equation: ``distance / half_spreading_rate``.
42
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
43
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
44
+ """
45
+ distance = quantity("distance", distance, lower=0)
46
+ half_spreading_rate = quantity("half_spreading_rate", half_spreading_rate, lower=0, strict_lower=True)
47
+ return distance / half_spreading_rate
48
+
49
+
50
+ @model
51
+ def full_spreading_rate(half_spreading_rate):
52
+ """Symmetric full spreading rate.
53
+
54
+ Opposite plate velocities combine into ridge opening speed.
55
+
56
+ Parameters
57
+ ----------
58
+ half_spreading_rate : float or array_like
59
+ m/s; constraint: nonnegative.
60
+
61
+ Returns
62
+ -------
63
+ numpy scalar or ndarray
64
+ Model value [rad/s]. Scalar input returns a NumPy scalar; arrays broadcast.
65
+
66
+ Notes
67
+ -----
68
+ Symmetric spreading; asymmetric velocities require their own relative-vector calculation.
69
+ Equation: ``2 * half_spreading_rate``.
70
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
71
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
72
+ """
73
+ half_spreading_rate = quantity("half_spreading_rate", half_spreading_rate, lower=0)
74
+ return 2 * half_spreading_rate
75
+
76
+
77
+ @model
78
+ def convergence_rate(relative_speed, obliquity):
79
+ """Signed plate-boundary convergence.
80
+
81
+ Resolving relative motion normal to a boundary separates convergence and shear.
82
+
83
+ Parameters
84
+ ----------
85
+ relative_speed : float or array_like
86
+ m/s; constraint: nonnegative.
87
+ obliquity : float or array_like
88
+ rad; constraint: width.
89
+
90
+ Returns
91
+ -------
92
+ numpy scalar or ndarray
93
+ Model value [rad/s]. Scalar input returns a NumPy scalar; arrays broadcast.
94
+
95
+ Notes
96
+ -----
97
+ Positive normal component denotes convergence; negative denotes divergence.
98
+ Equation: ``relative_speed * np.cos(obliquity)``.
99
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
100
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
101
+ """
102
+ relative_speed = quantity("relative_speed", relative_speed, lower=0)
103
+ obliquity = quantity("obliquity", obliquity, lower=0, upper=2 * np.pi)
104
+ return relative_speed * np.cos(obliquity)
105
+
106
+
107
+ @model
108
+ def fault_slip_rate(relative_speed, obliquity):
109
+ """Tangential boundary slip rate.
110
+
111
+ The tangential motion component connects oblique convergence to strike slip.
112
+
113
+ Parameters
114
+ ----------
115
+ relative_speed : float or array_like
116
+ m/s; constraint: nonnegative.
117
+ obliquity : float or array_like
118
+ rad; constraint: width.
119
+
120
+ Returns
121
+ -------
122
+ numpy scalar or ndarray
123
+ Model value [rad/s]. Scalar input returns a NumPy scalar; arrays broadcast.
124
+
125
+ Notes
126
+ -----
127
+ Supplied obliquity is oriented from the positive convergence normal.
128
+ Equation: ``relative_speed * np.sin(obliquity)``.
129
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
130
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
131
+ """
132
+ relative_speed = quantity("relative_speed", relative_speed, lower=0)
133
+ obliquity = quantity("obliquity", obliquity, lower=0, upper=2 * np.pi)
134
+ return relative_speed * np.sin(obliquity)
135
+
136
+
137
+ @model
138
+ def subduction_travel_time(vertical_depth, slab_speed, dip):
139
+ """Slab path travel time.
140
+
141
+ Slab dip changes the path length required to reach a specified depth.
142
+
143
+ Parameters
144
+ ----------
145
+ vertical_depth : float or array_like
146
+ m; constraint: nonnegative.
147
+ slab_speed : float or array_like
148
+ m/s; constraint: positive.
149
+ dip : float or array_like
150
+ rad; constraint: inclination.
151
+
152
+ Returns
153
+ -------
154
+ numpy scalar or ndarray
155
+ Model value [rad/s]. Scalar input returns a NumPy scalar; arrays broadcast.
156
+
157
+ Notes
158
+ -----
159
+ Straight slab at constant speed; zero dip cannot reach depth and is rejected.
160
+ Equation: ``vertical_depth / (slab_speed * np.sin(dip))``.
161
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
162
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
163
+ """
164
+ vertical_depth = quantity("vertical_depth", vertical_depth, lower=0)
165
+ slab_speed = quantity("slab_speed", slab_speed, lower=0, strict_lower=True)
166
+ dip = quantity("dip", dip, lower=0, upper=np.pi / 2)
167
+ require(dip > 0, "slab dip must be positive")
168
+ return vertical_depth / (slab_speed * np.sin(dip))
169
+
170
+
171
+ @model
172
+ def slab_pull_force(density_excess, slab_volume, gravity):
173
+ """Slab negative-buoyancy force.
174
+
175
+ A cold slab can drive motion through its excess weight.
176
+
177
+ Parameters
178
+ ----------
179
+ density_excess : float or array_like
180
+ kg/m^3; constraint: nonnegative.
181
+ slab_volume : float or array_like
182
+ m^3; constraint: nonnegative.
183
+ gravity : float or array_like
184
+ m/s^2; constraint: positive.
185
+
186
+ Returns
187
+ -------
188
+ numpy scalar or ndarray
189
+ Model value [rad/s]. Scalar input returns a NumPy scalar; arrays broadcast.
190
+
191
+ Notes
192
+ -----
193
+ Simple buoyancy force magnitude; drag, phase changes and mantle flow are excluded.
194
+ Equation: ``density_excess * slab_volume * gravity``.
195
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
196
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
197
+ """
198
+ density_excess = quantity("density_excess", density_excess, lower=0)
199
+ slab_volume = quantity("slab_volume", slab_volume, lower=0)
200
+ gravity = quantity("gravity", gravity, lower=0, strict_lower=True)
201
+ return density_excess * slab_volume * gravity
@@ -0,0 +1,238 @@
1
+ """Cooling and convection: half-space cooling, heat flux, ocean depth, mantle Rayleigh number and periodic diffusion.
2
+
3
+ SI units unless stated otherwise.
4
+ """
5
+
6
+ import numpy as np
7
+ from scipy.special import erf
8
+
9
+ from earthsciencekit._validation import model, quantity, require, safe_arithmetic
10
+
11
+ __all__ = [
12
+ "halfspace_temperature",
13
+ "halfspace_heat_flux",
14
+ "mantle_rayleigh_number",
15
+ "ocean_depth",
16
+ "diffuse_temperature",
17
+ ]
18
+
19
+
20
+ @model
21
+ def halfspace_temperature(depth, age, diffusivity, surface_temperature, mantle_temperature):
22
+ """Cooling oceanic lithosphere.
23
+
24
+ Oceanic plates cool as their age and distance from a ridge increase.
25
+
26
+ Parameters
27
+ ----------
28
+ depth : float or array_like
29
+ m; constraint: nonnegative.
30
+ age : float or array_like
31
+ s; constraint: positive.
32
+ diffusivity : float or array_like
33
+ m^2/s; constraint: positive.
34
+ surface_temperature : float or array_like
35
+ K; constraint: positive.
36
+ mantle_temperature : float or array_like
37
+ K; constraint: positive.
38
+
39
+ Returns
40
+ -------
41
+ numpy scalar or ndarray
42
+ Model value [rad/s]. Scalar input returns a NumPy scalar; arrays broadcast.
43
+
44
+ Notes
45
+ -----
46
+ Semi-infinite conduction with fixed surface temperature; no hydrothermal or advective heat transfer.
47
+ Equation: ``surface_temperature + (mantle_temperature - surface_temperature) * erf(depth / (2 * np.sqrt(diffusivity * age)))``.
48
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
49
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
50
+ """
51
+ depth = quantity("depth", depth, lower=0)
52
+ age = quantity("age", age, lower=0, strict_lower=True)
53
+ diffusivity = quantity("diffusivity", diffusivity, lower=0, strict_lower=True)
54
+ surface_temperature = quantity("surface_temperature", surface_temperature, lower=0, strict_lower=True)
55
+ mantle_temperature = quantity("mantle_temperature", mantle_temperature, lower=0, strict_lower=True)
56
+ require(mantle_temperature >= surface_temperature, "mantle must be at least as warm as surface")
57
+ return surface_temperature + (mantle_temperature - surface_temperature) * erf(depth / (2 * np.sqrt(diffusivity * age)))
58
+
59
+
60
+ @model
61
+ def halfspace_heat_flux(age, diffusivity, conductivity, surface_temperature, mantle_temperature):
62
+ """Cooling-plate surface heat flux.
63
+
64
+ Surface heat flux decreases as an oceanic plate cools.
65
+
66
+ Parameters
67
+ ----------
68
+ age : float or array_like
69
+ s; constraint: positive.
70
+ diffusivity : float or array_like
71
+ m^2/s; constraint: positive.
72
+ conductivity : float or array_like
73
+ W/(m K); constraint: positive.
74
+ surface_temperature : float or array_like
75
+ K; constraint: positive.
76
+ mantle_temperature : float or array_like
77
+ K; constraint: positive.
78
+
79
+ Returns
80
+ -------
81
+ numpy scalar or ndarray
82
+ Model value [rad/s]. Scalar input returns a NumPy scalar; arrays broadcast.
83
+
84
+ Notes
85
+ -----
86
+ Positive upward flux; semi-infinite conductive model excludes the zero-age singularity.
87
+ Equation: ``conductivity * (mantle_temperature - surface_temperature) / np.sqrt(np.pi * diffusivity * age)``.
88
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
89
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
90
+ """
91
+ age = quantity("age", age, lower=0, strict_lower=True)
92
+ diffusivity = quantity("diffusivity", diffusivity, lower=0, strict_lower=True)
93
+ conductivity = quantity("conductivity", conductivity, lower=0, strict_lower=True)
94
+ surface_temperature = quantity("surface_temperature", surface_temperature, lower=0, strict_lower=True)
95
+ mantle_temperature = quantity("mantle_temperature", mantle_temperature, lower=0, strict_lower=True)
96
+ require(mantle_temperature >= surface_temperature, "mantle must be at least as warm as surface")
97
+ return conductivity * (mantle_temperature - surface_temperature) / np.sqrt(np.pi * diffusivity * age)
98
+
99
+
100
+ @model
101
+ def ocean_depth(age, ridge_depth, mantle_density, water_density, thermal_expansivity, diffusivity, surface_temperature, mantle_temperature):
102
+ """Ocean depth of a cooling half-space.
103
+
104
+ Thermal contraction of the cooling plate makes the sea floor deepen with the square root of its age.
105
+
106
+ Parameters
107
+ ----------
108
+ age : float or array_like
109
+ s; constraint: nonnegative.
110
+ ridge_depth : float or array_like
111
+ m; constraint: nonnegative.
112
+ mantle_density : float or array_like
113
+ kg/m^3; constraint: positive.
114
+ water_density : float or array_like
115
+ kg/m^3; constraint: positive.
116
+ thermal_expansivity : float or array_like
117
+ 1/K; constraint: positive.
118
+ diffusivity : float or array_like
119
+ m^2/s; constraint: positive.
120
+ surface_temperature : float or array_like
121
+ K; constraint: positive.
122
+ mantle_temperature : float or array_like
123
+ K; constraint: positive.
124
+
125
+ Returns
126
+ -------
127
+ numpy scalar or ndarray
128
+ Model value [m]. Scalar input returns a NumPy scalar; arrays broadcast.
129
+
130
+ Notes
131
+ -----
132
+ Isostatic half-space cooling, depth positive downward; it overestimates depth on sea floor older than about 70 million years.
133
+ Equation: ``ridge_depth + 2 * mantle_density * thermal_expansivity * (mantle_temperature - surface_temperature)
134
+ / (mantle_density - water_density) * np.sqrt(diffusivity * age / np.pi)``.
135
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
136
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
137
+ """
138
+ age = quantity("age", age, lower=0)
139
+ ridge_depth = quantity("ridge_depth", ridge_depth, lower=0)
140
+ mantle_density = quantity("mantle_density", mantle_density, lower=0, strict_lower=True)
141
+ water_density = quantity("water_density", water_density, lower=0, strict_lower=True)
142
+ thermal_expansivity = quantity("thermal_expansivity", thermal_expansivity, lower=0, strict_lower=True)
143
+ diffusivity = quantity("diffusivity", diffusivity, lower=0, strict_lower=True)
144
+ surface_temperature = quantity("surface_temperature", surface_temperature, lower=0, strict_lower=True)
145
+ mantle_temperature = quantity("mantle_temperature", mantle_temperature, lower=0, strict_lower=True)
146
+ require(mantle_density > water_density, "mantle must be denser than water")
147
+ require(mantle_temperature >= surface_temperature, "mantle must be at least as warm as surface")
148
+ subsidence = 2 * mantle_density * thermal_expansivity * (mantle_temperature - surface_temperature) / (mantle_density - water_density)
149
+ return ridge_depth + subsidence * np.sqrt(diffusivity * age / np.pi)
150
+
151
+
152
+ @model
153
+ def mantle_rayleigh_number(thermal_expansivity, density, gravity, temperature_difference, thickness, diffusivity, viscosity):
154
+ """Rayleigh number of a convecting mantle layer.
155
+
156
+ Buoyancy against viscous and thermal diffusion decides whether the mantle convects.
157
+
158
+ Parameters
159
+ ----------
160
+ thermal_expansivity : float or array_like
161
+ 1/K; constraint: positive.
162
+ density : float or array_like
163
+ kg/m^3; constraint: positive.
164
+ gravity : float or array_like
165
+ m/s^2; constraint: positive.
166
+ temperature_difference : float or array_like
167
+ K; constraint: nonnegative.
168
+ thickness : float or array_like
169
+ m; constraint: positive.
170
+ diffusivity : float or array_like
171
+ m^2/s; constraint: positive.
172
+ viscosity : float or array_like
173
+ Pa s; constraint: positive.
174
+
175
+ Returns
176
+ -------
177
+ numpy scalar or ndarray
178
+ Model value [1]. Scalar input returns a NumPy scalar; arrays broadcast.
179
+
180
+ Notes
181
+ -----
182
+ Layer heated from below with uniform properties; convection starts above about 657.5 (free slip) or 1708 (rigid walls).
183
+ Equation: ``thermal_expansivity * density * gravity * temperature_difference * thickness**3 / (diffusivity * viscosity)``.
184
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
185
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
186
+ """
187
+ thermal_expansivity = quantity("thermal_expansivity", thermal_expansivity, lower=0, strict_lower=True)
188
+ density = quantity("density", density, lower=0, strict_lower=True)
189
+ gravity = quantity("gravity", gravity, lower=0, strict_lower=True)
190
+ temperature_difference = quantity("temperature_difference", temperature_difference, lower=0)
191
+ thickness = quantity("thickness", thickness, lower=0, strict_lower=True)
192
+ diffusivity = quantity("diffusivity", diffusivity, lower=0, strict_lower=True)
193
+ viscosity = quantity("viscosity", viscosity, lower=0, strict_lower=True)
194
+ return thermal_expansivity * density * gravity * temperature_difference * thickness**3 / (diffusivity * viscosity)
195
+
196
+
197
+ @safe_arithmetic
198
+ def diffuse_temperature(initial, time, diffusivity, spacing):
199
+ """Evolve a periodic one-dimensional conductive temperature grid.
200
+
201
+ Parameters
202
+ ----------
203
+ initial : array_like, shape (m,), m >= 3
204
+ Positive temperatures [K] on an equally spaced periodic grid.
205
+ time : array_like
206
+ Strictly increasing nonnegative output times [s]. Initial state belongs
207
+ to the first supplied time; values need not start at zero.
208
+ diffusivity : float
209
+ Positive scalar thermal diffusivity [m^2/s].
210
+ spacing : float
211
+ Positive scalar grid spacing [m].
212
+
213
+ Returns
214
+ -------
215
+ TimeSeries
216
+ Temperature at each grid point [K]. Columns are labeled cell_0, ... .
217
+
218
+ Notes
219
+ -----
220
+ The centered discrete Laplacian is diagonalized with a real FFT. Time
221
+ evolution of that discrete system is exact to floating-point accuracy.
222
+ Periodicity conserves mean temperature. Spatial discretization error remains;
223
+ this is not the fixed-surface half-space cooling boundary condition.
224
+ """
225
+ from earthsciencekit._validation import time_grid
226
+ from earthsciencekit.plate_tectonics.core.base import TimeSeries
227
+
228
+ initial = quantity("initial", initial, lower=0, strict_lower=True)
229
+ require(initial.ndim == 1 and initial.size >= 3, "at least three temperature cells are required")
230
+ time = time_grid(time)
231
+ diffusivity = quantity("diffusivity", diffusivity, lower=0, strict_lower=True)
232
+ spacing = quantity("spacing", spacing, lower=0, strict_lower=True)
233
+ require(diffusivity.ndim == spacing.ndim == 0, "diffusivity and spacing must be scalars")
234
+ with np.errstate(over="raise", divide="raise", invalid="raise"):
235
+ eigenvalues = -4 * diffusivity * np.sin(np.pi * np.fft.rfftfreq(initial.size)) ** 2 / spacing**2
236
+ spectra = np.fft.rfft(initial)[None, :] * np.exp((time - time[0])[:, None] * eigenvalues)
237
+ values = np.fft.irfft(spectra, n=initial.size, axis=1)
238
+ return TimeSeries(time, values, tuple(f"cell_{i}" for i in range(initial.size)), ("K",) * initial.size)