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.
- earthsciencekit/__init__.py +6 -0
- earthsciencekit/_validation.py +62 -0
- earthsciencekit/atmosphere/__init__.py +57 -0
- earthsciencekit/atmosphere/core/__init__.py +5 -0
- earthsciencekit/atmosphere/core/base.py +10 -0
- earthsciencekit/atmosphere/systems/__init__.py +1 -0
- earthsciencekit/atmosphere/systems/dynamics.py +208 -0
- earthsciencekit/atmosphere/systems/hydrostatics.py +214 -0
- earthsciencekit/atmosphere/systems/moisture.py +162 -0
- earthsciencekit/atmosphere/systems/thermodynamics.py +222 -0
- earthsciencekit/atmosphere/tests/__init__.py +0 -0
- earthsciencekit/atmosphere/tests/test_models.py +741 -0
- earthsciencekit/atmosphere/tests/test_physics.py +81 -0
- earthsciencekit/atmosphere/visualizers/__init__.py +9 -0
- earthsciencekit/climate/__init__.py +63 -0
- earthsciencekit/climate/core/__init__.py +5 -0
- earthsciencekit/climate/core/base.py +10 -0
- earthsciencekit/climate/systems/__init__.py +1 -0
- earthsciencekit/climate/systems/carbon.py +76 -0
- earthsciencekit/climate/systems/forcing.py +124 -0
- earthsciencekit/climate/systems/radiation.py +214 -0
- earthsciencekit/climate/systems/response.py +402 -0
- earthsciencekit/climate/systems/sea_level.py +77 -0
- earthsciencekit/climate/tests/__init__.py +0 -0
- earthsciencekit/climate/tests/test_models.py +1015 -0
- earthsciencekit/climate/tests/test_physics.py +16 -0
- earthsciencekit/climate/visualizers/__init__.py +9 -0
- earthsciencekit/constants/__init__.py +23 -0
- earthsciencekit/geophysics/__init__.py +77 -0
- earthsciencekit/geophysics/core/__init__.py +5 -0
- earthsciencekit/geophysics/core/base.py +10 -0
- earthsciencekit/geophysics/systems/__init__.py +1 -0
- earthsciencekit/geophysics/systems/deformation.py +161 -0
- earthsciencekit/geophysics/systems/dynamo.py +159 -0
- earthsciencekit/geophysics/systems/figure.py +292 -0
- earthsciencekit/geophysics/systems/gravity.py +235 -0
- earthsciencekit/geophysics/systems/magnetism.py +354 -0
- earthsciencekit/geophysics/tests/__init__.py +0 -0
- earthsciencekit/geophysics/tests/test_models.py +898 -0
- earthsciencekit/geophysics/tests/test_physics.py +171 -0
- earthsciencekit/geophysics/visualizers/__init__.py +9 -0
- earthsciencekit/integrators/__init__.py +62 -0
- earthsciencekit/oceanography/__init__.py +61 -0
- earthsciencekit/oceanography/core/__init__.py +5 -0
- earthsciencekit/oceanography/core/base.py +10 -0
- earthsciencekit/oceanography/systems/__init__.py +1 -0
- earthsciencekit/oceanography/systems/circulation.py +388 -0
- earthsciencekit/oceanography/systems/hydrography.py +212 -0
- earthsciencekit/oceanography/systems/inertial.py +86 -0
- earthsciencekit/oceanography/systems/waves.py +281 -0
- earthsciencekit/oceanography/tests/__init__.py +0 -0
- earthsciencekit/oceanography/tests/test_models.py +916 -0
- earthsciencekit/oceanography/tests/test_physics.py +102 -0
- earthsciencekit/oceanography/visualizers/__init__.py +9 -0
- earthsciencekit/plate_tectonics/__init__.py +59 -0
- earthsciencekit/plate_tectonics/core/__init__.py +5 -0
- earthsciencekit/plate_tectonics/core/base.py +10 -0
- earthsciencekit/plate_tectonics/systems/__init__.py +1 -0
- earthsciencekit/plate_tectonics/systems/boundaries.py +201 -0
- earthsciencekit/plate_tectonics/systems/cooling.py +238 -0
- earthsciencekit/plate_tectonics/systems/isostasy.py +157 -0
- earthsciencekit/plate_tectonics/systems/kinematics.py +290 -0
- earthsciencekit/plate_tectonics/tests/__init__.py +0 -0
- earthsciencekit/plate_tectonics/tests/test_models.py +920 -0
- earthsciencekit/plate_tectonics/tests/test_physics.py +94 -0
- earthsciencekit/plate_tectonics/visualizers/__init__.py +9 -0
- earthsciencekit/py.typed +0 -0
- earthsciencekit/results.py +73 -0
- earthsciencekit/seismology/__init__.py +59 -0
- earthsciencekit/seismology/core/__init__.py +5 -0
- earthsciencekit/seismology/core/base.py +10 -0
- earthsciencekit/seismology/systems/__init__.py +1 -0
- earthsciencekit/seismology/systems/interfaces.py +102 -0
- earthsciencekit/seismology/systems/rays.py +221 -0
- earthsciencekit/seismology/systems/sources.py +249 -0
- earthsciencekit/seismology/systems/waves.py +268 -0
- earthsciencekit/seismology/tests/__init__.py +0 -0
- earthsciencekit/seismology/tests/test_models.py +852 -0
- earthsciencekit/seismology/tests/test_physics.py +100 -0
- earthsciencekit/seismology/visualizers/__init__.py +9 -0
- earthsciencekit/tests/__init__.py +0 -0
- earthsciencekit/tests/test_core.py +181 -0
- earthsciencekit/tests/test_relations.py +36 -0
- earthsciencekit/units/__init__.py +66 -0
- earthsciencekit/visualizers/__init__.py +45 -0
- earthsciencekit/volcanology/__init__.py +61 -0
- earthsciencekit/volcanology/core/__init__.py +5 -0
- earthsciencekit/volcanology/core/base.py +10 -0
- earthsciencekit/volcanology/systems/__init__.py +1 -0
- earthsciencekit/volcanology/systems/eruptions.py +161 -0
- earthsciencekit/volcanology/systems/geochemistry.py +226 -0
- earthsciencekit/volcanology/systems/geochronology.py +304 -0
- earthsciencekit/volcanology/systems/magma.py +190 -0
- earthsciencekit/volcanology/tests/__init__.py +0 -0
- earthsciencekit/volcanology/tests/test_models.py +485 -0
- earthsciencekit/volcanology/tests/test_physics.py +176 -0
- earthsciencekit/volcanology/visualizers/__init__.py +9 -0
- earthsciencekit-0.2.0.dist-info/METADATA +127 -0
- earthsciencekit-0.2.0.dist-info/RECORD +102 -0
- earthsciencekit-0.2.0.dist-info/WHEEL +5 -0
- earthsciencekit-0.2.0.dist-info/licenses/LICENSE +15 -0
- 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,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)
|