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,100 @@
|
|
|
1
|
+
"""Physical identities, limits and dynamic checks for earthsciencekit.seismology."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from earthsciencekit.seismology.systems.interfaces import reflection_coefficient, transmission_coefficient
|
|
7
|
+
from earthsciencekit.seismology.systems.rays import (
|
|
8
|
+
critical_angle,
|
|
9
|
+
crossover_distance,
|
|
10
|
+
head_wave_travel_time,
|
|
11
|
+
ray_parameter,
|
|
12
|
+
travel_time,
|
|
13
|
+
vertical_travel_time,
|
|
14
|
+
)
|
|
15
|
+
from earthsciencekit.seismology.systems.sources import local_magnitude, moment_from_magnitude, moment_magnitude, simulate_oscillator
|
|
16
|
+
from earthsciencekit.seismology.systems.waves import love_wave_angular_frequency, poisson_ratio, rayleigh_wave_speed, ricker_wavelet
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_seismic_oscillator_analytic():
|
|
20
|
+
frequency, Q = 1.0, 10.0
|
|
21
|
+
time = np.linspace(0, 2, 101)
|
|
22
|
+
result = simulate_oscillator(frequency, Q, time)
|
|
23
|
+
omega = 2 * np.pi * frequency
|
|
24
|
+
damping = omega / (2 * Q)
|
|
25
|
+
wd = np.sqrt(omega**2 - damping**2)
|
|
26
|
+
expected = np.exp(-damping * time) * (np.cos(wd * time) + damping / wd * np.sin(wd * time))
|
|
27
|
+
np.testing.assert_allclose(result.values[:, 0], expected, atol=3e-8)
|
|
28
|
+
energy = result.values[:, 1] ** 2 + omega**2 * result.values[:, 0] ** 2
|
|
29
|
+
assert np.all(np.diff(energy) <= 1e-8)
|
|
30
|
+
with pytest.raises(ValueError):
|
|
31
|
+
simulate_oscillator([1, 2], Q, time)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_wave_interface_energy_balance():
|
|
35
|
+
z1, z2 = 2.0, 6.0
|
|
36
|
+
r = reflection_coefficient(z1, z2)
|
|
37
|
+
t = transmission_coefficient(z1, z2)
|
|
38
|
+
assert np.isclose(r * r + (z1 / z2) * t * t, 1)
|
|
39
|
+
assert np.isclose(moment_magnitude(moment_from_magnitude(7)), 7)
|
|
40
|
+
np.testing.assert_allclose(ricker_wavelet([-1, 0, 1], 2), ricker_wavelet([1, 0, -1], 2))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_rayleigh_speed_solves_rayleigh_secular_equation():
|
|
44
|
+
vs = 1.0
|
|
45
|
+
vp = np.array([1.2, np.sqrt(3), 3.0, 10.0])
|
|
46
|
+
c = rayleigh_wave_speed(vp, vs)
|
|
47
|
+
assert np.all(c < vs)
|
|
48
|
+
np.testing.assert_allclose((2 - c**2 / vs**2) ** 2, 4 * np.sqrt(1 - c**2 / vp**2) * np.sqrt(1 - c**2 / vs**2), atol=1e-12)
|
|
49
|
+
# Viktorov's approximation (0.862 + 1.14 nu) / (1 + nu) holds to about 0.3% for nu >= 0.25.
|
|
50
|
+
nu = poisson_ratio(vp[1:], vs)
|
|
51
|
+
np.testing.assert_allclose(c[1:], (0.862 + 1.14 * nu) / (1 + nu), rtol=3e-3)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_head_wave_crosses_direct_wave_and_starts_at_reflection():
|
|
55
|
+
h, v1, v2 = 30e3, 6e3, 8e3
|
|
56
|
+
x_cross = crossover_distance(h, v1, v2)
|
|
57
|
+
assert np.isclose(head_wave_travel_time(x_cross, h, v1, v2), travel_time(x_cross, v1))
|
|
58
|
+
x_crit = 2 * h * np.tan(critical_angle(v1, v2))
|
|
59
|
+
assert np.isclose(head_wave_travel_time(x_crit, h, v1, v2), np.hypot(x_crit, 2 * h) / v1)
|
|
60
|
+
x = np.array([2e5, 3e5])
|
|
61
|
+
assert np.isclose(np.diff(head_wave_travel_time(x, h, v1, v2))[0], 1e5 / v2)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_love_dispersion_root_and_limits():
|
|
65
|
+
h, b1, b2, m1, m2 = 35e3, 3.5e3, 4.5e3, 3.4e10, 6.7e10
|
|
66
|
+
c = np.linspace(3.6e3, 4.4e3, 9)
|
|
67
|
+
omega = love_wave_angular_frequency(c, h, b1, b2, m1, m2)
|
|
68
|
+
k = omega / c
|
|
69
|
+
s1, s2 = np.sqrt(c**2 / b1**2 - 1), np.sqrt(1 - c**2 / b2**2)
|
|
70
|
+
np.testing.assert_allclose(np.tan(k * h * s1), m2 * s2 / (m1 * s1))
|
|
71
|
+
assert np.all(np.diff(omega) < 0)
|
|
72
|
+
assert love_wave_angular_frequency(b2 * (1 - 1e-12), h, b1, b2, m1, m2) < 1e-5
|
|
73
|
+
assert love_wave_angular_frequency(b1 * (1 + 1e-12), h, b1, b2, m1, m2) > 1e3
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_ray_parameter_is_snell_on_a_sphere():
|
|
77
|
+
r, v_above, v_below = 3.48e6, 13e3, 8e3
|
|
78
|
+
above = np.deg2rad(50.0)
|
|
79
|
+
p = ray_parameter(r, v_above, above)
|
|
80
|
+
below = np.arcsin(p * v_below / r)
|
|
81
|
+
assert np.isclose(ray_parameter(r, v_below, below), p)
|
|
82
|
+
# A straight ray in a uniform shell keeps r sin(i) fixed, hence the same p at every radius.
|
|
83
|
+
impact = r * np.sin(above)
|
|
84
|
+
for radius in [r, 4e6, 6.371e6]:
|
|
85
|
+
assert np.isclose(ray_parameter(radius, v_above, np.arcsin(impact / radius)), p)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_local_magnitude_is_logarithmic_in_amplitude():
|
|
89
|
+
assert np.isclose(local_magnitude(1e-6, 1e5), 0)
|
|
90
|
+
np.testing.assert_allclose(np.diff(local_magnitude([1e-4, 1e-3, 1e-2], 3e5)), 1)
|
|
91
|
+
assert local_magnitude(1e-3, 3e5) > local_magnitude(1e-3, 1e5)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_vertical_travel_time_profiles():
|
|
95
|
+
depth = np.linspace(0, 1e5, 11)
|
|
96
|
+
np.testing.assert_allclose(vertical_travel_time(depth, np.full(11, 5e3)), travel_time(depth, 5e3))
|
|
97
|
+
v0, gradient = 5e3, 0.02
|
|
98
|
+
np.testing.assert_allclose(vertical_travel_time(depth, v0 + gradient * depth), np.log1p(gradient * depth / v0) / gradient)
|
|
99
|
+
jump = vertical_travel_time([0, 1e4, 1e4, 2e4], [5e3, 5e3, 8e3, 8e3])
|
|
100
|
+
np.testing.assert_allclose(jump, [0, 2, 2, 2 + 1.25])
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Plots for earthsciencekit.seismology.
|
|
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"]
|
|
File without changes
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Independent identities, conservation checks and failure contracts."""
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
|
|
5
|
+
import matplotlib
|
|
6
|
+
|
|
7
|
+
matplotlib.use("Agg")
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from earthsciencekit import __version__, atmosphere, oceanography, seismology
|
|
13
|
+
from earthsciencekit._validation import model, quantity, require, time_grid
|
|
14
|
+
from earthsciencekit.integrators import integrate_ode
|
|
15
|
+
from earthsciencekit.results import TimeSeries
|
|
16
|
+
from earthsciencekit.units import available_units, convert
|
|
17
|
+
from earthsciencekit.visualizers import animate_series, plot_series
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_version():
|
|
21
|
+
assert __version__ == "0.2.0"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_bounds_and_scalar_array_contract():
|
|
25
|
+
np.testing.assert_array_equal(quantity("x", [0, 1], lower=0, upper=1), [0, 1])
|
|
26
|
+
assert quantity("x", 1).ndim == 0
|
|
27
|
+
for kwargs, value in [
|
|
28
|
+
({"lower": 0}, -1),
|
|
29
|
+
({"lower": 0, "strict_lower": True}, 0),
|
|
30
|
+
({"upper": 1}, 2),
|
|
31
|
+
({"upper": 1, "strict_upper": True}, 1),
|
|
32
|
+
]:
|
|
33
|
+
with pytest.raises(ValueError):
|
|
34
|
+
quantity("x", value, **kwargs)
|
|
35
|
+
with pytest.raises(ValueError):
|
|
36
|
+
require([True, False], "invalid")
|
|
37
|
+
require([True, True], "valid")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_arithmetic_rejection():
|
|
41
|
+
with pytest.raises(ValueError, match="arithmetic"):
|
|
42
|
+
seismology.moment_from_magnitude(1000)
|
|
43
|
+
|
|
44
|
+
@model
|
|
45
|
+
def nonfinite():
|
|
46
|
+
return float("inf")
|
|
47
|
+
|
|
48
|
+
with pytest.raises(ValueError, match="nonfinite"):
|
|
49
|
+
nonfinite()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.parametrize("bad", [[], [1], [0, 0], [1, 0], [-1, 1], [[0, 1]], [0, np.nan]])
|
|
53
|
+
def test_time_grid_bad(bad):
|
|
54
|
+
with pytest.raises(ValueError):
|
|
55
|
+
time_grid(bad)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_units():
|
|
59
|
+
assert convert(1, "km", "m") == 1000
|
|
60
|
+
assert convert(0, "degC", "K") == 273.15
|
|
61
|
+
assert convert(273.15, "K", "degC") == 0
|
|
62
|
+
assert convert(1, "Sv", "m^3/s") == 1e6
|
|
63
|
+
assert convert(1, "year", "day") == 365.25
|
|
64
|
+
assert convert(1, "Ga", "Ma") == 1000 and convert(1, "kyr", "year") == 1000 and convert(1, "GPa", "MPa") == 1000
|
|
65
|
+
np.testing.assert_allclose(convert(convert([1, 5], "cm/year", "m/s"), "m/s", "cm/year"), [1, 5])
|
|
66
|
+
assert convert(180, "degree", "rad") == np.pi
|
|
67
|
+
assert np.isclose(convert(1, "mGal", "m/s^2"), 1e-5) and np.isclose(convert(5e4, "nT", "T"), 5e-5)
|
|
68
|
+
units = available_units()
|
|
69
|
+
units.pop("K")
|
|
70
|
+
assert "K" in available_units()
|
|
71
|
+
for source, target, value in [("m", "K", 1), ("bad", "m", 1), ([], "m", 1), ("K", "K", -1)]:
|
|
72
|
+
with pytest.raises(ValueError):
|
|
73
|
+
convert(value, source, target)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_result_copy_summary_and_plots(tmp_path):
|
|
77
|
+
time = np.array([0.0, 1.0, 2.0])
|
|
78
|
+
values = np.array([[1.0, 2.0], [0.0, 3.0], [-1.0, 4.0]])
|
|
79
|
+
result = TimeSeries(time, values, ("a", "b"), ("m", "K"))
|
|
80
|
+
time[0] = -100
|
|
81
|
+
values[0, 0] = 999
|
|
82
|
+
assert result.time[0] == 0 and result.values[0, 0] == 1
|
|
83
|
+
assert not result.time.flags.writeable and not result.values.flags.writeable
|
|
84
|
+
assert result.summary()["a"] == {"minimum": -1.0, "maximum": 1.0, "final": -1.0, "unit": "m"}
|
|
85
|
+
figure, axes = result.plot()
|
|
86
|
+
assert len(axes) == 2 and axes[0].get_ylabel() == "a [m]"
|
|
87
|
+
plt.close(figure)
|
|
88
|
+
animation = result.animate(interval=15)
|
|
89
|
+
animation.save(tmp_path / "history.gif", writer="pillow")
|
|
90
|
+
assert (tmp_path / "history.gif").stat().st_size > 100
|
|
91
|
+
plt.close(animation._fig)
|
|
92
|
+
with pytest.raises(ValueError):
|
|
93
|
+
plot_series([1])
|
|
94
|
+
with pytest.raises(ValueError):
|
|
95
|
+
animate_series(result, interval=0)
|
|
96
|
+
with pytest.raises(ValueError):
|
|
97
|
+
animate_series(result, interval=[1, 2])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.parametrize(
|
|
101
|
+
"time, values, names, units",
|
|
102
|
+
[
|
|
103
|
+
([0], [[1]], ("x",), ("m",)),
|
|
104
|
+
([0, 0], [[1], [2]], ("x",), ("m",)),
|
|
105
|
+
([0, 1], [1, 2], ("x",), ("m",)),
|
|
106
|
+
([0, 1], [[1]], ("x",), ("m",)),
|
|
107
|
+
([0, 1], [[1], [2]], (), ()),
|
|
108
|
+
([0, 1], [[1], [2]], ("",), ("m",)),
|
|
109
|
+
([0, 1], [[1, 2], [3, 4]], ("x", "x"), ("m", "m")),
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
def test_result_rejects_bad_shape_and_labels(time, values, names, units):
|
|
113
|
+
with pytest.raises(ValueError):
|
|
114
|
+
TimeSeries(time, values, names, units)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_ode_reference_and_defaults():
|
|
118
|
+
time = np.linspace(0, 3, 40)
|
|
119
|
+
result = integrate_ode(lambda t, y: -y, [2], time)
|
|
120
|
+
np.testing.assert_allclose(result.values[:, 0], 2 * np.exp(-time), rtol=3e-8)
|
|
121
|
+
assert result.names == ("x0",) and result.units == ("1",)
|
|
122
|
+
reference = integrate_ode(lambda t, y: [y[1], -y[0]], [1, 0], time, names=("position", "velocity"), units=("m", "m/s"), method="DOP853")
|
|
123
|
+
np.testing.assert_allclose(reference.values[:, 0], np.cos(time), atol=2e-8)
|
|
124
|
+
np.testing.assert_allclose(np.sum(reference.values**2, axis=1), 1, atol=4e-8)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.parametrize(
|
|
128
|
+
"kwargs", [{"rtol": 0}, {"atol": np.nan}, {"atol": [1, 2]}, {"method": "BAD"}, {"names": ["a", "b"]}, {"units": [""]}]
|
|
129
|
+
)
|
|
130
|
+
def test_ode_invalid_options(kwargs):
|
|
131
|
+
with pytest.raises(ValueError):
|
|
132
|
+
integrate_ode(lambda t, y: y, [1], [0, 1], **kwargs)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_ode_failures(monkeypatch):
|
|
136
|
+
for rhs, initial, time in [
|
|
137
|
+
(0, [1], [0, 1]),
|
|
138
|
+
(lambda t, y: y, [[1]], [0, 1]),
|
|
139
|
+
(lambda t, y: y, [1], [0]),
|
|
140
|
+
(lambda t, y: y, [1], [1, 0]),
|
|
141
|
+
(lambda t, y: [1, 2], [1], [0, 1]),
|
|
142
|
+
(lambda t, y: [np.nan], [1], [0, 1]),
|
|
143
|
+
]:
|
|
144
|
+
with pytest.raises(ValueError):
|
|
145
|
+
integrate_ode(rhs, initial, time)
|
|
146
|
+
monkeypatch.setattr("earthsciencekit.integrators.solve_ivp", lambda *a, **k: SimpleNamespace(success=False, message="test failure"))
|
|
147
|
+
with pytest.raises(ValueError, match="test failure"):
|
|
148
|
+
integrate_ode(lambda t, y: -y, [1], [0, 1])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_humidity_and_wave_limits():
|
|
152
|
+
ratio = atmosphere.mixing_ratio(1000, 100000)
|
|
153
|
+
humidity = atmosphere.specific_humidity(ratio)
|
|
154
|
+
assert np.isclose(humidity / (1 - humidity), ratio)
|
|
155
|
+
assert atmosphere.coriolis_parameter(-np.pi / 4) < 0 < atmosphere.coriolis_parameter(np.pi / 4)
|
|
156
|
+
k, h, g = 1e-6, 10.0, 9.81
|
|
157
|
+
omega = oceanography.wave_angular_frequency(k, h, g)
|
|
158
|
+
assert np.isclose(omega / k, oceanography.shallow_wave_speed(h, g), rtol=1e-9)
|
|
159
|
+
k, h = 1.0, 100.0
|
|
160
|
+
omega = oceanography.wave_angular_frequency(k, h, g)
|
|
161
|
+
assert np.isclose(omega / k, oceanography.deep_wave_speed(2 * np.pi / k, g))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_dynamic_overflow_is_normalized():
|
|
165
|
+
with pytest.raises(ValueError, match="arithmetic"):
|
|
166
|
+
seismology.simulate_oscillator(1e308, 10, [0, 1])
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.parametrize("name", ["plate_tectonics", "seismology", "atmosphere", "climate", "oceanography", "geophysics", "volcanology"])
|
|
170
|
+
def test_domain_family_layout(name):
|
|
171
|
+
import importlib
|
|
172
|
+
import inspect
|
|
173
|
+
|
|
174
|
+
domain = importlib.import_module(f"earthsciencekit.{name}")
|
|
175
|
+
core = importlib.import_module(f"earthsciencekit.{name}.core")
|
|
176
|
+
plots = importlib.import_module(f"earthsciencekit.{name}.visualizers")
|
|
177
|
+
assert core.TimeSeries is TimeSeries
|
|
178
|
+
assert plots.plot_series is plot_series and plots.animate_series is animate_series
|
|
179
|
+
for public in domain.__all__:
|
|
180
|
+
function = getattr(domain, public)
|
|
181
|
+
assert inspect.getmodule(function).__name__.startswith(f"earthsciencekit.{name}.systems."), public
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Cross-parameter relations that simple scalar bounds cannot establish."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from earthsciencekit import atmosphere as a
|
|
6
|
+
from earthsciencekit import climate as c
|
|
7
|
+
from earthsciencekit import plate_tectonics as p
|
|
8
|
+
from earthsciencekit import seismology as s
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.parametrize(
|
|
12
|
+
"function,args",
|
|
13
|
+
[
|
|
14
|
+
(p.plate_speed, [1, 1, 4]),
|
|
15
|
+
(p.subduction_travel_time, [1, 1, 0]),
|
|
16
|
+
(p.airy_root, [1, 3, 2]),
|
|
17
|
+
(p.halfspace_temperature, [1, 1, 1, 300, 200]),
|
|
18
|
+
(p.halfspace_heat_flux, [1, 1, 1, 300, 200]),
|
|
19
|
+
(p.ocean_depth, [1, 2500, 1000, 1000, 1, 1, 300, 1300]),
|
|
20
|
+
(p.ocean_depth, [1, 2500, 2, 1, 1, 1, 300, 200]),
|
|
21
|
+
(s.poisson_ratio, [1, 1]),
|
|
22
|
+
(s.critical_angle, [2, 1]),
|
|
23
|
+
(s.rayleigh_wave_speed, [1, 1]),
|
|
24
|
+
(s.head_wave_travel_time, [4, 1, 2, 1]),
|
|
25
|
+
(s.head_wave_travel_time, [1, 1, 1, 2]),
|
|
26
|
+
(s.crossover_distance, [1, 2, 2]),
|
|
27
|
+
(s.love_wave_angular_frequency, [1, 1, 1, 2, 1, 1]),
|
|
28
|
+
(s.love_wave_angular_frequency, [2, 1, 1, 2, 1, 1]),
|
|
29
|
+
(a.mixing_ratio, [1000, 100]),
|
|
30
|
+
(a.two_layer_temperature, [0, 288, 0.0065, 50000]),
|
|
31
|
+
(c.ice_albedo, [273, 0.2, 0.6, 273, 5]),
|
|
32
|
+
],
|
|
33
|
+
)
|
|
34
|
+
def test_cross_parameter_rejections(function, args):
|
|
35
|
+
with pytest.raises(ValueError):
|
|
36
|
+
function(*args)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Small explicit unit conversions; raw model arguments carry no unit metadata."""
|
|
2
|
+
|
|
3
|
+
from earthsciencekit._validation import model, quantity, require
|
|
4
|
+
from earthsciencekit.constants import SECONDS_PER_YEAR
|
|
5
|
+
|
|
6
|
+
# scale and offset convert each unit to its SI base; temperature offsets are exact.
|
|
7
|
+
_UNITS = {
|
|
8
|
+
"m": ("length", 1, 0),
|
|
9
|
+
"km": ("length", 1000, 0),
|
|
10
|
+
"cm": ("length", 0.01, 0),
|
|
11
|
+
"s": ("time", 1, 0),
|
|
12
|
+
"day": ("time", 86400, 0),
|
|
13
|
+
"year": ("time", SECONDS_PER_YEAR, 0),
|
|
14
|
+
"kyr": ("time", 1e3 * SECONDS_PER_YEAR, 0),
|
|
15
|
+
"Ma": ("time", 1e6 * SECONDS_PER_YEAR, 0),
|
|
16
|
+
"Ga": ("time", 1e9 * SECONDS_PER_YEAR, 0),
|
|
17
|
+
"Pa": ("pressure", 1, 0),
|
|
18
|
+
"hPa": ("pressure", 100, 0),
|
|
19
|
+
"MPa": ("pressure", 1e6, 0),
|
|
20
|
+
"GPa": ("pressure", 1e9, 0),
|
|
21
|
+
"K": ("temperature", 1, 0),
|
|
22
|
+
"degC": ("temperature", 1, 273.15),
|
|
23
|
+
"m/s": ("speed", 1, 0),
|
|
24
|
+
"cm/year": ("speed", 0.01 / SECONDS_PER_YEAR, 0),
|
|
25
|
+
"m/s^2": ("acceleration", 1, 0),
|
|
26
|
+
"mGal": ("acceleration", 1e-5, 0),
|
|
27
|
+
"T": ("magnetic field", 1, 0),
|
|
28
|
+
"nT": ("magnetic field", 1e-9, 0),
|
|
29
|
+
"rad": ("angle", 1, 0),
|
|
30
|
+
"degree": ("angle", 0.017453292519943295, 0),
|
|
31
|
+
"m^3/s": ("volume flow", 1, 0),
|
|
32
|
+
"Sv": ("volume flow", 1e6, 0),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@model
|
|
37
|
+
def convert(value, source, target):
|
|
38
|
+
"""Convert a finite scalar or array between listed compatible units.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
value : float or array_like
|
|
43
|
+
Quantity in source units. Temperature conversions require nonnegative kelvin.
|
|
44
|
+
source, target : str
|
|
45
|
+
Exact case-sensitive unit labels from available_units().
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
numpy scalar or ndarray
|
|
50
|
+
Quantity in target units. Unknown units or incompatible dimensions fail.
|
|
51
|
+
"""
|
|
52
|
+
value = quantity("value", value)
|
|
53
|
+
require(isinstance(source, str) and isinstance(target, str), "unit labels must be strings")
|
|
54
|
+
require(source in _UNITS and target in _UNITS, "unsupported unit label")
|
|
55
|
+
dimension, scale, offset = _UNITS[source]
|
|
56
|
+
target_dimension, target_scale, target_offset = _UNITS[target]
|
|
57
|
+
require(dimension == target_dimension, "incompatible unit dimensions")
|
|
58
|
+
base = value * scale + offset
|
|
59
|
+
if dimension == "temperature":
|
|
60
|
+
require(base >= 0, "temperature is below absolute zero")
|
|
61
|
+
return (base - target_offset) / target_scale
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def available_units():
|
|
65
|
+
"""Return a new mapping of supported unit labels to dimension names."""
|
|
66
|
+
return {name: spec[0] for name, spec in _UNITS.items()}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Labeled figures and lightweight animations for sampled histories."""
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import numpy as np
|
|
5
|
+
from matplotlib.animation import FuncAnimation
|
|
6
|
+
|
|
7
|
+
from earthsciencekit._validation import quantity, require
|
|
8
|
+
from earthsciencekit.results import TimeSeries
|
|
9
|
+
|
|
10
|
+
__all__ = ["plot_series", "animate_series"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def plot_series(series):
|
|
14
|
+
"""Plot a TimeSeries with one state and explicit unit label per axis."""
|
|
15
|
+
require(isinstance(series, TimeSeries), "series must be a TimeSeries")
|
|
16
|
+
figure, axes = plt.subplots(len(series.names), 1, squeeze=False, sharex=True)
|
|
17
|
+
axes = axes[:, 0]
|
|
18
|
+
for i, axis in enumerate(axes):
|
|
19
|
+
axis.plot(series.time, series.values[:, i])
|
|
20
|
+
axis.set_ylabel(f"{series.names[i]} [{series.units[i]}]")
|
|
21
|
+
axis.grid(alpha=0.2)
|
|
22
|
+
axes[-1].set_xlabel("time [s]")
|
|
23
|
+
figure.tight_layout()
|
|
24
|
+
return figure, axes
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def animate_series(series, *, interval=40):
|
|
28
|
+
"""Reveal a sampled history progressively; return a Matplotlib animation.
|
|
29
|
+
|
|
30
|
+
Keep the returned object alive when displaying it, or call ``save`` with an
|
|
31
|
+
appropriate Matplotlib writer. Interval is positive scalar milliseconds.
|
|
32
|
+
"""
|
|
33
|
+
interval = quantity("interval", interval, lower=0, strict_lower=True)
|
|
34
|
+
require(interval.ndim == 0, "interval must be a scalar")
|
|
35
|
+
figure, axes = plot_series(series)
|
|
36
|
+
lines = [axis.lines[0] for axis in axes]
|
|
37
|
+
for line in lines:
|
|
38
|
+
line.set_data([], [])
|
|
39
|
+
|
|
40
|
+
def update(frame):
|
|
41
|
+
for i, line in enumerate(lines):
|
|
42
|
+
line.set_data(series.time[: frame + 1], series.values[: frame + 1, i])
|
|
43
|
+
return lines
|
|
44
|
+
|
|
45
|
+
return FuncAnimation(figure, update, frames=np.arange(series.time.size), interval=float(interval), blit=True)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Inspectable volcanology and geochemistry models; SI units unless stated otherwise."""
|
|
2
|
+
|
|
3
|
+
from earthsciencekit.volcanology.systems.eruptions import (
|
|
4
|
+
column_height,
|
|
5
|
+
plume_rise_height,
|
|
6
|
+
simulate_eruption_column,
|
|
7
|
+
volcanic_explosivity_index,
|
|
8
|
+
)
|
|
9
|
+
from earthsciencekit.volcanology.systems.geochemistry import (
|
|
10
|
+
delta_value,
|
|
11
|
+
equilibrium_crystallization_melt,
|
|
12
|
+
fractional_crystallization_melt,
|
|
13
|
+
isotope_temperature,
|
|
14
|
+
lattice_strain_partition,
|
|
15
|
+
rayleigh_fractionation,
|
|
16
|
+
)
|
|
17
|
+
from earthsciencekit.volcanology.systems.geochronology import (
|
|
18
|
+
concordia_ratios,
|
|
19
|
+
decay_constant,
|
|
20
|
+
isochron_fit,
|
|
21
|
+
lead_lead_age,
|
|
22
|
+
parent_fraction,
|
|
23
|
+
radiocarbon_age,
|
|
24
|
+
radiogenic_daughter,
|
|
25
|
+
radiogenic_lead_ratio,
|
|
26
|
+
radiometric_age,
|
|
27
|
+
)
|
|
28
|
+
from earthsciencekit.volcanology.systems.magma import (
|
|
29
|
+
arrhenius_viscosity,
|
|
30
|
+
dike_ascent_velocity,
|
|
31
|
+
mogi_displacement,
|
|
32
|
+
shaw_viscosity,
|
|
33
|
+
water_solubility,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"rayleigh_fractionation",
|
|
38
|
+
"delta_value",
|
|
39
|
+
"decay_constant",
|
|
40
|
+
"parent_fraction",
|
|
41
|
+
"radiometric_age",
|
|
42
|
+
"radiogenic_daughter",
|
|
43
|
+
"fractional_crystallization_melt",
|
|
44
|
+
"equilibrium_crystallization_melt",
|
|
45
|
+
"lattice_strain_partition",
|
|
46
|
+
"isotope_temperature",
|
|
47
|
+
"radiocarbon_age",
|
|
48
|
+
"radiogenic_lead_ratio",
|
|
49
|
+
"lead_lead_age",
|
|
50
|
+
"concordia_ratios",
|
|
51
|
+
"plume_rise_height",
|
|
52
|
+
"mogi_displacement",
|
|
53
|
+
"isochron_fit",
|
|
54
|
+
"arrhenius_viscosity",
|
|
55
|
+
"shaw_viscosity",
|
|
56
|
+
"column_height",
|
|
57
|
+
"dike_ascent_velocity",
|
|
58
|
+
"water_solubility",
|
|
59
|
+
"volcanic_explosivity_index",
|
|
60
|
+
"simulate_eruption_column",
|
|
61
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Result containers for earthsciencekit.volcanology.
|
|
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.volcanology, grouped by topic."""
|