earthsciencekit 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. earthsciencekit/__init__.py +6 -0
  2. earthsciencekit/_validation.py +62 -0
  3. earthsciencekit/atmosphere/__init__.py +57 -0
  4. earthsciencekit/atmosphere/core/__init__.py +5 -0
  5. earthsciencekit/atmosphere/core/base.py +10 -0
  6. earthsciencekit/atmosphere/systems/__init__.py +1 -0
  7. earthsciencekit/atmosphere/systems/dynamics.py +208 -0
  8. earthsciencekit/atmosphere/systems/hydrostatics.py +214 -0
  9. earthsciencekit/atmosphere/systems/moisture.py +162 -0
  10. earthsciencekit/atmosphere/systems/thermodynamics.py +222 -0
  11. earthsciencekit/atmosphere/tests/__init__.py +0 -0
  12. earthsciencekit/atmosphere/tests/test_models.py +741 -0
  13. earthsciencekit/atmosphere/tests/test_physics.py +81 -0
  14. earthsciencekit/atmosphere/visualizers/__init__.py +9 -0
  15. earthsciencekit/climate/__init__.py +63 -0
  16. earthsciencekit/climate/core/__init__.py +5 -0
  17. earthsciencekit/climate/core/base.py +10 -0
  18. earthsciencekit/climate/systems/__init__.py +1 -0
  19. earthsciencekit/climate/systems/carbon.py +76 -0
  20. earthsciencekit/climate/systems/forcing.py +124 -0
  21. earthsciencekit/climate/systems/radiation.py +214 -0
  22. earthsciencekit/climate/systems/response.py +402 -0
  23. earthsciencekit/climate/systems/sea_level.py +77 -0
  24. earthsciencekit/climate/tests/__init__.py +0 -0
  25. earthsciencekit/climate/tests/test_models.py +1015 -0
  26. earthsciencekit/climate/tests/test_physics.py +16 -0
  27. earthsciencekit/climate/visualizers/__init__.py +9 -0
  28. earthsciencekit/constants/__init__.py +23 -0
  29. earthsciencekit/geophysics/__init__.py +77 -0
  30. earthsciencekit/geophysics/core/__init__.py +5 -0
  31. earthsciencekit/geophysics/core/base.py +10 -0
  32. earthsciencekit/geophysics/systems/__init__.py +1 -0
  33. earthsciencekit/geophysics/systems/deformation.py +161 -0
  34. earthsciencekit/geophysics/systems/dynamo.py +159 -0
  35. earthsciencekit/geophysics/systems/figure.py +292 -0
  36. earthsciencekit/geophysics/systems/gravity.py +235 -0
  37. earthsciencekit/geophysics/systems/magnetism.py +354 -0
  38. earthsciencekit/geophysics/tests/__init__.py +0 -0
  39. earthsciencekit/geophysics/tests/test_models.py +898 -0
  40. earthsciencekit/geophysics/tests/test_physics.py +171 -0
  41. earthsciencekit/geophysics/visualizers/__init__.py +9 -0
  42. earthsciencekit/integrators/__init__.py +62 -0
  43. earthsciencekit/oceanography/__init__.py +61 -0
  44. earthsciencekit/oceanography/core/__init__.py +5 -0
  45. earthsciencekit/oceanography/core/base.py +10 -0
  46. earthsciencekit/oceanography/systems/__init__.py +1 -0
  47. earthsciencekit/oceanography/systems/circulation.py +388 -0
  48. earthsciencekit/oceanography/systems/hydrography.py +212 -0
  49. earthsciencekit/oceanography/systems/inertial.py +86 -0
  50. earthsciencekit/oceanography/systems/waves.py +281 -0
  51. earthsciencekit/oceanography/tests/__init__.py +0 -0
  52. earthsciencekit/oceanography/tests/test_models.py +916 -0
  53. earthsciencekit/oceanography/tests/test_physics.py +102 -0
  54. earthsciencekit/oceanography/visualizers/__init__.py +9 -0
  55. earthsciencekit/plate_tectonics/__init__.py +59 -0
  56. earthsciencekit/plate_tectonics/core/__init__.py +5 -0
  57. earthsciencekit/plate_tectonics/core/base.py +10 -0
  58. earthsciencekit/plate_tectonics/systems/__init__.py +1 -0
  59. earthsciencekit/plate_tectonics/systems/boundaries.py +201 -0
  60. earthsciencekit/plate_tectonics/systems/cooling.py +238 -0
  61. earthsciencekit/plate_tectonics/systems/isostasy.py +157 -0
  62. earthsciencekit/plate_tectonics/systems/kinematics.py +290 -0
  63. earthsciencekit/plate_tectonics/tests/__init__.py +0 -0
  64. earthsciencekit/plate_tectonics/tests/test_models.py +920 -0
  65. earthsciencekit/plate_tectonics/tests/test_physics.py +94 -0
  66. earthsciencekit/plate_tectonics/visualizers/__init__.py +9 -0
  67. earthsciencekit/py.typed +0 -0
  68. earthsciencekit/results.py +73 -0
  69. earthsciencekit/seismology/__init__.py +59 -0
  70. earthsciencekit/seismology/core/__init__.py +5 -0
  71. earthsciencekit/seismology/core/base.py +10 -0
  72. earthsciencekit/seismology/systems/__init__.py +1 -0
  73. earthsciencekit/seismology/systems/interfaces.py +102 -0
  74. earthsciencekit/seismology/systems/rays.py +221 -0
  75. earthsciencekit/seismology/systems/sources.py +249 -0
  76. earthsciencekit/seismology/systems/waves.py +268 -0
  77. earthsciencekit/seismology/tests/__init__.py +0 -0
  78. earthsciencekit/seismology/tests/test_models.py +852 -0
  79. earthsciencekit/seismology/tests/test_physics.py +100 -0
  80. earthsciencekit/seismology/visualizers/__init__.py +9 -0
  81. earthsciencekit/tests/__init__.py +0 -0
  82. earthsciencekit/tests/test_core.py +181 -0
  83. earthsciencekit/tests/test_relations.py +36 -0
  84. earthsciencekit/units/__init__.py +66 -0
  85. earthsciencekit/visualizers/__init__.py +45 -0
  86. earthsciencekit/volcanology/__init__.py +61 -0
  87. earthsciencekit/volcanology/core/__init__.py +5 -0
  88. earthsciencekit/volcanology/core/base.py +10 -0
  89. earthsciencekit/volcanology/systems/__init__.py +1 -0
  90. earthsciencekit/volcanology/systems/eruptions.py +161 -0
  91. earthsciencekit/volcanology/systems/geochemistry.py +226 -0
  92. earthsciencekit/volcanology/systems/geochronology.py +304 -0
  93. earthsciencekit/volcanology/systems/magma.py +190 -0
  94. earthsciencekit/volcanology/tests/__init__.py +0 -0
  95. earthsciencekit/volcanology/tests/test_models.py +485 -0
  96. earthsciencekit/volcanology/tests/test_physics.py +176 -0
  97. earthsciencekit/volcanology/visualizers/__init__.py +9 -0
  98. earthsciencekit-0.2.0.dist-info/METADATA +127 -0
  99. earthsciencekit-0.2.0.dist-info/RECORD +102 -0
  100. earthsciencekit-0.2.0.dist-info/WHEEL +5 -0
  101. earthsciencekit-0.2.0.dist-info/licenses/LICENSE +15 -0
  102. earthsciencekit-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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"]
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,5 @@
1
+ """Result containers for earthsciencekit.seismology."""
2
+
3
+ from earthsciencekit.seismology.core.base import TimeSeries
4
+
5
+ __all__ = ["TimeSeries"]
@@ -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)])