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,94 @@
|
|
|
1
|
+
"""Physical identities, limits and dynamic checks for earthsciencekit.plate_tectonics."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from earthsciencekit.plate_tectonics.systems.cooling import diffuse_temperature, halfspace_heat_flux, mantle_rayleigh_number, ocean_depth
|
|
7
|
+
from earthsciencekit.plate_tectonics.systems.isostasy import flexural_deflection, flexural_parameter
|
|
8
|
+
from earthsciencekit.plate_tectonics.systems.kinematics import (
|
|
9
|
+
angular_speed,
|
|
10
|
+
compose_euler_vectors,
|
|
11
|
+
euler_velocity_x,
|
|
12
|
+
euler_velocity_y,
|
|
13
|
+
euler_velocity_z,
|
|
14
|
+
hotspot_age,
|
|
15
|
+
plate_speed,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_periodic_diffusion_conservation_and_eigenmode():
|
|
20
|
+
n, spacing, diffusivity = 16, 2.0, 0.1
|
|
21
|
+
initial = 300 + 10 * np.cos(2 * np.pi * np.arange(n) / n)
|
|
22
|
+
time = np.linspace(10, 110, 31)
|
|
23
|
+
result = diffuse_temperature(initial, time, diffusivity, spacing)
|
|
24
|
+
np.testing.assert_allclose(result.values.mean(axis=1), 300, atol=1e-12)
|
|
25
|
+
rate = 4 * diffusivity * np.sin(np.pi / n) ** 2 / spacing**2
|
|
26
|
+
expected = 300 + (initial - 300)[None, :] * np.exp(-rate * (time - time[0]))[:, None]
|
|
27
|
+
np.testing.assert_allclose(result.values, expected, atol=1e-12)
|
|
28
|
+
assert result.values.min() >= initial.min() and result.values.max() <= initial.max()
|
|
29
|
+
for initial, diffusion, spacing in [([300, 310], 1, 1), ([[300] * 3], 1, 1), ([300] * 3, [1, 2], 1), ([300] * 3, 0, 1)]:
|
|
30
|
+
with pytest.raises(ValueError):
|
|
31
|
+
diffuse_temperature(initial, [0, 1], diffusion, spacing)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_plate_rotation_tangent_and_speed():
|
|
35
|
+
omega = np.array([1.0, 2.0, 3.0]) * 1e-9
|
|
36
|
+
r = np.array([3.0, 4.0, 5.0]) * 1e6
|
|
37
|
+
v = np.array(
|
|
38
|
+
[
|
|
39
|
+
euler_velocity_x(omega[1], omega[2], r[1], r[2]),
|
|
40
|
+
euler_velocity_y(omega[2], omega[0], r[2], r[0]),
|
|
41
|
+
euler_velocity_z(omega[0], omega[1], r[0], r[1]),
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
np.testing.assert_allclose(v, np.cross(omega, r))
|
|
45
|
+
assert abs(np.dot(v, r)) < 1e-10
|
|
46
|
+
distance = np.arccos(np.dot(omega, r) / (np.linalg.norm(omega) * np.linalg.norm(r)))
|
|
47
|
+
assert np.isclose(plate_speed(np.linalg.norm(omega), np.linalg.norm(r), distance), np.linalg.norm(v))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_flexure_balances_load_and_places_bulge():
|
|
51
|
+
rigidity, contrast, gravity, load = 1e23, 2300.0, 9.8, 2e12
|
|
52
|
+
alpha = flexural_parameter(rigidity, contrast, gravity)
|
|
53
|
+
x = np.linspace(-25 * alpha, 25 * alpha, 200001)
|
|
54
|
+
w = flexural_deflection(x, load, rigidity, contrast, gravity)
|
|
55
|
+
assert np.isclose(np.trapezoid(contrast * gravity * w, x), load, rtol=1e-6)
|
|
56
|
+
w0 = flexural_deflection(0, load, rigidity, contrast, gravity)
|
|
57
|
+
assert np.isclose(flexural_deflection(3 * np.pi * alpha / 4, load, rigidity, contrast, gravity), 0, atol=1e-12 * w0)
|
|
58
|
+
assert np.isclose(flexural_deflection(np.pi * alpha, load, rigidity, contrast, gravity), -np.exp(-np.pi) * w0)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_ocean_depth_matches_heat_flux_and_parsons_sclater():
|
|
62
|
+
year = 365.25 * 86400
|
|
63
|
+
age = np.array([1, 4, 25]) * 1e6 * year
|
|
64
|
+
depth = ocean_depth(age, 2500, 3300, 1000, 3e-5, 1e-6, 273, 1553)
|
|
65
|
+
np.testing.assert_allclose((depth - 2500) / np.sqrt(age / (1e6 * year)), 350, rtol=0.01)
|
|
66
|
+
np.testing.assert_allclose(np.diff(depth - 2500) / (depth - 2500)[:-1], [1, 1.5])
|
|
67
|
+
# Subsidence is the thermal contraction of the heat lost since the ridge: alpha * int(q dt) / (c_p (rho_m - rho_w)).
|
|
68
|
+
flux = halfspace_heat_flux(age, 1e-6, 3.3, 273, 1553)
|
|
69
|
+
heat_capacity = 3.3 / (3300 * 1e-6)
|
|
70
|
+
np.testing.assert_allclose(depth - 2500, 3e-5 * 2 * flux * age / (heat_capacity * (3300 - 1000)))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_euler_circuit_closes_and_velocities_add():
|
|
74
|
+
omega_ab = np.array([1.0, -2.0, 0.5]) * 1e-9
|
|
75
|
+
omega_bc = np.array([-0.3, 0.8, 2.0]) * 1e-9
|
|
76
|
+
omega_ac = compose_euler_vectors(omega_ab, omega_bc)
|
|
77
|
+
np.testing.assert_allclose(compose_euler_vectors(omega_ac, -omega_ac), 0)
|
|
78
|
+
r = np.array([3.0, 4.0, 5.0]) * 1e6
|
|
79
|
+
np.testing.assert_allclose(np.cross(omega_ac, r), np.cross(omega_ab, r) + np.cross(omega_bc, r))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_hotspot_age_is_rotation_angle_over_rate():
|
|
83
|
+
omega, radius, colatitude = 2e-15, 6.371e6, np.array([0.3, 1.0, 2.5])
|
|
84
|
+
angle = 0.2
|
|
85
|
+
ages = hotspot_age(angle * radius * np.sin(colatitude), omega, radius, colatitude)
|
|
86
|
+
np.testing.assert_allclose(ages, angle / omega)
|
|
87
|
+
assert np.isclose(angular_speed(angle, ages[0]), omega)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_mantle_rayleigh_number_cubic_in_thickness():
|
|
91
|
+
args = (3e-5, 4000, 9.8, 2500, 1e6, 1e-6, 1e21)
|
|
92
|
+
ratio = mantle_rayleigh_number(*args[:4], 2 * args[4], *args[5:]) / mantle_rayleigh_number(*args)
|
|
93
|
+
assert np.isclose(ratio, 8)
|
|
94
|
+
assert mantle_rayleigh_number(3e-5, 4000, 9.8, 0, 1e6, 1e-6, 1e21) == 0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Plots for earthsciencekit.plate_tectonics.
|
|
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"]
|
earthsciencekit/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Immutable sampled histories shared by the numerical models."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ._validation import quantity, require
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class TimeSeries:
|
|
12
|
+
"""A time grid in seconds and one or more state columns.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
time : array_like, shape (n,)
|
|
17
|
+
Finite, strictly increasing seconds; at least two samples.
|
|
18
|
+
values : array_like, shape (n, m)
|
|
19
|
+
Finite real state columns in the units given by ``units``.
|
|
20
|
+
names, units : tuple of str, length m
|
|
21
|
+
Unique state names and explicit output unit labels.
|
|
22
|
+
|
|
23
|
+
Notes
|
|
24
|
+
-----
|
|
25
|
+
Input data are copied and made read-only. Samples do not imply a continuous
|
|
26
|
+
interpolation or independently validate the governing model.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
time: np.ndarray
|
|
30
|
+
values: np.ndarray
|
|
31
|
+
names: tuple[str, ...]
|
|
32
|
+
units: tuple[str, ...]
|
|
33
|
+
|
|
34
|
+
def __post_init__(self):
|
|
35
|
+
time = quantity("time", self.time)
|
|
36
|
+
values = quantity("values", self.values)
|
|
37
|
+
require(time.ndim == 1 and time.size >= 2, "time must be a vector with at least two samples")
|
|
38
|
+
require(np.all(np.diff(time) > 0), "time must strictly increase")
|
|
39
|
+
require(values.ndim == 2 and values.shape[0] == time.size, "values must have shape (time, state)")
|
|
40
|
+
names, units = tuple(self.names), tuple(self.units)
|
|
41
|
+
require(len(names) == values.shape[1] == len(units), "one name and unit per state required")
|
|
42
|
+
require(all(isinstance(s, str) and s for s in names + units), "names and units must be nonempty strings")
|
|
43
|
+
require(len(set(names)) == len(names), "state names must be unique")
|
|
44
|
+
time.setflags(write=False)
|
|
45
|
+
values.setflags(write=False)
|
|
46
|
+
object.__setattr__(self, "time", time)
|
|
47
|
+
object.__setattr__(self, "values", values)
|
|
48
|
+
object.__setattr__(self, "names", names)
|
|
49
|
+
object.__setattr__(self, "units", units)
|
|
50
|
+
|
|
51
|
+
def summary(self):
|
|
52
|
+
"""Return each state's minimum, maximum, final value and unit label."""
|
|
53
|
+
return {
|
|
54
|
+
name: {
|
|
55
|
+
"minimum": float(self.values[:, i].min()),
|
|
56
|
+
"maximum": float(self.values[:, i].max()),
|
|
57
|
+
"final": float(self.values[-1, i]),
|
|
58
|
+
"unit": self.units[i],
|
|
59
|
+
}
|
|
60
|
+
for i, name in enumerate(self.names)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def plot(self):
|
|
64
|
+
"""Create one labeled Matplotlib axis per state; return figure and axes."""
|
|
65
|
+
from .visualizers import plot_series
|
|
66
|
+
|
|
67
|
+
return plot_series(self)
|
|
68
|
+
|
|
69
|
+
def animate(self, *, interval=40):
|
|
70
|
+
"""Create a history-reveal animation; interval is milliseconds per frame."""
|
|
71
|
+
from .visualizers import animate_series
|
|
72
|
+
|
|
73
|
+
return animate_series(self, interval=interval)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Inspectable seismology models; SI units unless stated otherwise."""
|
|
2
|
+
|
|
3
|
+
from earthsciencekit.seismology.systems.interfaces import (
|
|
4
|
+
reflection_coefficient,
|
|
5
|
+
seismic_impedance,
|
|
6
|
+
transmission_coefficient,
|
|
7
|
+
)
|
|
8
|
+
from earthsciencekit.seismology.systems.rays import (
|
|
9
|
+
critical_angle,
|
|
10
|
+
crossover_distance,
|
|
11
|
+
head_wave_travel_time,
|
|
12
|
+
ray_parameter,
|
|
13
|
+
travel_time,
|
|
14
|
+
vertical_travel_time,
|
|
15
|
+
)
|
|
16
|
+
from earthsciencekit.seismology.systems.sources import (
|
|
17
|
+
gutenberg_richter,
|
|
18
|
+
local_magnitude,
|
|
19
|
+
moment_from_magnitude,
|
|
20
|
+
moment_magnitude,
|
|
21
|
+
omori_rate,
|
|
22
|
+
seismic_moment,
|
|
23
|
+
simulate_oscillator,
|
|
24
|
+
)
|
|
25
|
+
from earthsciencekit.seismology.systems.waves import (
|
|
26
|
+
attenuated_amplitude,
|
|
27
|
+
love_wave_angular_frequency,
|
|
28
|
+
p_wave_speed,
|
|
29
|
+
poisson_ratio,
|
|
30
|
+
rayleigh_wave_speed,
|
|
31
|
+
ricker_wavelet,
|
|
32
|
+
s_wave_speed,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"p_wave_speed",
|
|
37
|
+
"s_wave_speed",
|
|
38
|
+
"poisson_ratio",
|
|
39
|
+
"travel_time",
|
|
40
|
+
"seismic_impedance",
|
|
41
|
+
"reflection_coefficient",
|
|
42
|
+
"transmission_coefficient",
|
|
43
|
+
"critical_angle",
|
|
44
|
+
"seismic_moment",
|
|
45
|
+
"moment_magnitude",
|
|
46
|
+
"moment_from_magnitude",
|
|
47
|
+
"gutenberg_richter",
|
|
48
|
+
"omori_rate",
|
|
49
|
+
"ricker_wavelet",
|
|
50
|
+
"attenuated_amplitude",
|
|
51
|
+
"rayleigh_wave_speed",
|
|
52
|
+
"head_wave_travel_time",
|
|
53
|
+
"crossover_distance",
|
|
54
|
+
"love_wave_angular_frequency",
|
|
55
|
+
"ray_parameter",
|
|
56
|
+
"local_magnitude",
|
|
57
|
+
"vertical_travel_time",
|
|
58
|
+
"simulate_oscillator",
|
|
59
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Result containers for earthsciencekit.seismology.
|
|
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.seismology, grouped by topic."""
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Interfaces: seismic impedance and normal-incidence reflection and transmission.
|
|
2
|
+
|
|
3
|
+
SI units unless stated otherwise.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from earthsciencekit._validation import model, quantity
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"seismic_impedance",
|
|
10
|
+
"reflection_coefficient",
|
|
11
|
+
"transmission_coefficient",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@model
|
|
16
|
+
def seismic_impedance(density, speed):
|
|
17
|
+
"""Acoustic impedance.
|
|
18
|
+
|
|
19
|
+
Impedance contrasts control normal-incidence reflection.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
density : float or array_like
|
|
24
|
+
kg/m^3; constraint: positive.
|
|
25
|
+
speed : float or array_like
|
|
26
|
+
m/s; constraint: positive.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
numpy scalar or ndarray
|
|
31
|
+
Model value [m/s]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
32
|
+
|
|
33
|
+
Notes
|
|
34
|
+
-----
|
|
35
|
+
Scalar acoustic impedance; elastic mode conversion is excluded.
|
|
36
|
+
Equation: ``density * speed``.
|
|
37
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
38
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
39
|
+
"""
|
|
40
|
+
density = quantity("density", density, lower=0, strict_lower=True)
|
|
41
|
+
speed = quantity("speed", speed, lower=0, strict_lower=True)
|
|
42
|
+
return density * speed
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@model
|
|
46
|
+
def reflection_coefficient(impedance1, impedance2):
|
|
47
|
+
"""Normal-incidence pressure reflection.
|
|
48
|
+
|
|
49
|
+
An interface divides incident energy into reflected and transmitted waves.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
impedance1 : float or array_like
|
|
54
|
+
kg/(m^2 s); constraint: positive.
|
|
55
|
+
impedance2 : float or array_like
|
|
56
|
+
kg/(m^2 s); constraint: positive.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
numpy scalar or ndarray
|
|
61
|
+
Model value [m/s]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
62
|
+
|
|
63
|
+
Notes
|
|
64
|
+
-----
|
|
65
|
+
Pressure amplitude convention at normal incidence; coefficient can be negative.
|
|
66
|
+
Equation: ``(impedance2 - impedance1) / (impedance2 + impedance1)``.
|
|
67
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
68
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
69
|
+
"""
|
|
70
|
+
impedance1 = quantity("impedance1", impedance1, lower=0, strict_lower=True)
|
|
71
|
+
impedance2 = quantity("impedance2", impedance2, lower=0, strict_lower=True)
|
|
72
|
+
return (impedance2 - impedance1) / (impedance2 + impedance1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@model
|
|
76
|
+
def transmission_coefficient(impedance1, impedance2):
|
|
77
|
+
"""Normal-incidence pressure transmission.
|
|
78
|
+
|
|
79
|
+
Pressure transmission differs from transmitted energy fraction.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
impedance1 : float or array_like
|
|
84
|
+
kg/(m^2 s); constraint: positive.
|
|
85
|
+
impedance2 : float or array_like
|
|
86
|
+
kg/(m^2 s); constraint: positive.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
numpy scalar or ndarray
|
|
91
|
+
Model value [m/s]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
92
|
+
|
|
93
|
+
Notes
|
|
94
|
+
-----
|
|
95
|
+
Normal-incidence acoustic pressure amplitudes, not displacement amplitudes.
|
|
96
|
+
Equation: ``2 * impedance2 / (impedance2 + impedance1)``.
|
|
97
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
98
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
99
|
+
"""
|
|
100
|
+
impedance1 = quantity("impedance1", impedance1, lower=0, strict_lower=True)
|
|
101
|
+
impedance2 = quantity("impedance2", impedance2, lower=0, strict_lower=True)
|
|
102
|
+
return 2 * impedance2 / (impedance2 + impedance1)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Rays and travel times: direct, head and vertical waves, crossover distance, critical angle and ray parameter.
|
|
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
|
+
"travel_time",
|
|
12
|
+
"critical_angle",
|
|
13
|
+
"head_wave_travel_time",
|
|
14
|
+
"crossover_distance",
|
|
15
|
+
"ray_parameter",
|
|
16
|
+
"vertical_travel_time",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@model
|
|
21
|
+
def travel_time(distance, speed):
|
|
22
|
+
"""Homogeneous travel time.
|
|
23
|
+
|
|
24
|
+
Arrival times connect source distance and propagation speed.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
distance : float or array_like
|
|
29
|
+
m; constraint: nonnegative.
|
|
30
|
+
speed : float or array_like
|
|
31
|
+
m/s; constraint: positive.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
numpy scalar or ndarray
|
|
36
|
+
Model value [m/s]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
37
|
+
|
|
38
|
+
Notes
|
|
39
|
+
-----
|
|
40
|
+
Straight path in a homogeneous medium; not a ray tracer.
|
|
41
|
+
Equation: ``distance / speed``.
|
|
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
|
+
speed = quantity("speed", speed, lower=0, strict_lower=True)
|
|
47
|
+
return distance / speed
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@model
|
|
51
|
+
def critical_angle(speed1, speed2):
|
|
52
|
+
"""Snell refraction critical angle.
|
|
53
|
+
|
|
54
|
+
Snell refraction explains head waves above a faster layer.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
speed1 : float or array_like
|
|
59
|
+
m/s; constraint: positive.
|
|
60
|
+
speed2 : float or array_like
|
|
61
|
+
m/s; constraint: positive.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
numpy scalar or ndarray
|
|
66
|
+
Model value [m/s]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
67
|
+
|
|
68
|
+
Notes
|
|
69
|
+
-----
|
|
70
|
+
Incidence from the slower medium; second speed must exceed first.
|
|
71
|
+
Equation: ``np.arcsin(speed1 / speed2)``.
|
|
72
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
73
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
74
|
+
"""
|
|
75
|
+
speed1 = quantity("speed1", speed1, lower=0, strict_lower=True)
|
|
76
|
+
speed2 = quantity("speed2", speed2, lower=0, strict_lower=True)
|
|
77
|
+
require(speed2 > speed1, "second medium must be faster")
|
|
78
|
+
return np.arcsin(speed1 / speed2)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@model
|
|
82
|
+
def head_wave_travel_time(distance, thickness, speed1, speed2):
|
|
83
|
+
"""Head-wave travel time below a single layer.
|
|
84
|
+
|
|
85
|
+
A wave critically refracted along a faster layer overtakes the direct wave at long range.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
distance : float or array_like
|
|
90
|
+
m; constraint: nonnegative, at least the critical distance.
|
|
91
|
+
thickness : float or array_like
|
|
92
|
+
m; constraint: positive.
|
|
93
|
+
speed1 : float or array_like
|
|
94
|
+
m/s; constraint: positive.
|
|
95
|
+
speed2 : float or array_like
|
|
96
|
+
m/s; constraint: positive.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
numpy scalar or ndarray
|
|
101
|
+
Model value [s]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
102
|
+
|
|
103
|
+
Notes
|
|
104
|
+
-----
|
|
105
|
+
Flat layer of speed1 over a faster half-space of speed2, surface source and receiver.
|
|
106
|
+
The head wave exists only beyond the critical distance ``2 * thickness * tan(critical_angle)``.
|
|
107
|
+
Equation: ``distance / speed2 + 2 * thickness * np.sqrt(speed2**2 - speed1**2) / (speed1 * speed2)``.
|
|
108
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
109
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
110
|
+
"""
|
|
111
|
+
distance = quantity("distance", distance, lower=0)
|
|
112
|
+
thickness = quantity("thickness", thickness, lower=0, strict_lower=True)
|
|
113
|
+
speed1 = quantity("speed1", speed1, lower=0, strict_lower=True)
|
|
114
|
+
speed2 = quantity("speed2", speed2, lower=0, strict_lower=True)
|
|
115
|
+
require(speed2 > speed1, "second medium must be faster")
|
|
116
|
+
require(distance >= 2 * thickness * speed1 / np.sqrt(speed2**2 - speed1**2), "distance is inside the critical distance")
|
|
117
|
+
return distance / speed2 + 2 * thickness * np.sqrt(speed2**2 - speed1**2) / (speed1 * speed2)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@model
|
|
121
|
+
def crossover_distance(thickness, speed1, speed2):
|
|
122
|
+
"""Head-wave crossover distance.
|
|
123
|
+
|
|
124
|
+
The range at which the refracted wave arrives first measures the depth of the layer.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
thickness : float or array_like
|
|
129
|
+
m; constraint: positive.
|
|
130
|
+
speed1 : float or array_like
|
|
131
|
+
m/s; constraint: positive.
|
|
132
|
+
speed2 : float or array_like
|
|
133
|
+
m/s; constraint: positive.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
numpy scalar or ndarray
|
|
138
|
+
Model value [m]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
139
|
+
|
|
140
|
+
Notes
|
|
141
|
+
-----
|
|
142
|
+
Flat layer of speed1 over a faster half-space of speed2; direct and head-wave times are equal there.
|
|
143
|
+
Equation: ``2 * thickness * np.sqrt((speed2 + speed1) / (speed2 - speed1))``.
|
|
144
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
145
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
146
|
+
"""
|
|
147
|
+
thickness = quantity("thickness", thickness, lower=0, strict_lower=True)
|
|
148
|
+
speed1 = quantity("speed1", speed1, lower=0, strict_lower=True)
|
|
149
|
+
speed2 = quantity("speed2", speed2, lower=0, strict_lower=True)
|
|
150
|
+
require(speed2 > speed1, "second medium must be faster")
|
|
151
|
+
return 2 * thickness * np.sqrt((speed2 + speed1) / (speed2 - speed1))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@model
|
|
155
|
+
def ray_parameter(radius, speed, incidence_angle):
|
|
156
|
+
"""Ray parameter in a spherically layered Earth.
|
|
157
|
+
|
|
158
|
+
Snell's law on a sphere keeps one quantity fixed along each ray, which fixes where it emerges.
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
radius : float or array_like
|
|
163
|
+
m; constraint: positive.
|
|
164
|
+
speed : float or array_like
|
|
165
|
+
m/s; constraint: positive.
|
|
166
|
+
incidence_angle : float or array_like
|
|
167
|
+
rad; constraint: between 0 and pi/2, measured from the radial direction.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
numpy scalar or ndarray
|
|
172
|
+
Model value [s/rad]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
173
|
+
|
|
174
|
+
Notes
|
|
175
|
+
-----
|
|
176
|
+
Spherically symmetric isotropic Earth; the value is the same at every point of one ray.
|
|
177
|
+
Equation: ``radius * np.sin(incidence_angle) / speed``.
|
|
178
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
179
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
180
|
+
"""
|
|
181
|
+
radius = quantity("radius", radius, lower=0, strict_lower=True)
|
|
182
|
+
speed = quantity("speed", speed, lower=0, strict_lower=True)
|
|
183
|
+
incidence_angle = quantity("incidence_angle", incidence_angle, lower=0, upper=np.pi / 2)
|
|
184
|
+
return radius * np.sin(incidence_angle) / speed
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@model
|
|
188
|
+
def vertical_travel_time(depth, speed):
|
|
189
|
+
"""Vertical travel time through a tabulated speed profile.
|
|
190
|
+
|
|
191
|
+
Integrating slowness through a reference Earth model predicts arrival times.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
depth : array_like, shape (n,), n >= 2
|
|
196
|
+
m; constraint: nonnegative and nondecreasing. A repeated depth marks a discontinuity.
|
|
197
|
+
speed : array_like, shape (n,)
|
|
198
|
+
m/s; constraint: positive.
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
ndarray, shape (n,)
|
|
203
|
+
Model value [s]: one-way vertical time from the first depth to each depth.
|
|
204
|
+
|
|
205
|
+
Notes
|
|
206
|
+
-----
|
|
207
|
+
Speed varies linearly in depth between tabulated points; the integral of each segment is exact.
|
|
208
|
+
Equation: ``cumsum(dz * np.log(v2 / v1) / (v2 - v1))``, which is ``dz / v1`` where ``v2 == v1``.
|
|
209
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
210
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
211
|
+
"""
|
|
212
|
+
depth = quantity("depth", depth, lower=0)
|
|
213
|
+
speed = quantity("speed", speed, lower=0, strict_lower=True)
|
|
214
|
+
require(depth.ndim == 1 and depth.size >= 2, "depth must be a vector with at least two samples")
|
|
215
|
+
require(speed.shape == depth.shape, "speed must have the same shape as depth")
|
|
216
|
+
require(np.all(np.diff(depth) >= 0), "depth must not decrease")
|
|
217
|
+
change = np.diff(speed) / speed[:-1]
|
|
218
|
+
factor = np.ones_like(change)
|
|
219
|
+
varying = change != 0
|
|
220
|
+
factor[varying] = np.log1p(change[varying]) / change[varying]
|
|
221
|
+
return np.concatenate([[0.0], np.cumsum(np.diff(depth) / speed[:-1] * factor)])
|