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,16 @@
1
+ """Physical identities, limits and dynamic checks for earthsciencekit.climate."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ from earthsciencekit.climate.systems.response import simulate_one_box, step_temperature
7
+
8
+
9
+ def test_one_box_exact_solution_and_nonzero_start():
10
+ time = np.linspace(10, 20, 51)
11
+ result = simulate_one_box(4, 2, 2, time, initial_anomaly=-1)
12
+ np.testing.assert_allclose(result.values[:, 0], 2 - 3 * np.exp(-(time - time[0])), atol=2e-8)
13
+ zero = simulate_one_box(4, 2, 2, time)
14
+ np.testing.assert_allclose(zero.values[:, 0], step_temperature(4, 2, 2, time - time[0]), atol=2e-8)
15
+ with pytest.raises(ValueError):
16
+ simulate_one_box(4, [2, 3], 2, time)
@@ -0,0 +1,9 @@
1
+ """Plots for earthsciencekit.climate.
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,23 @@
1
+ """Explicit reference constants, not an evolving observational dataset.
2
+
3
+ SI units apply. Earth angular speed and material properties are educational
4
+ reference values. EPSILON_WATER is the ratio of dry-air to vapor gas constants.
5
+ """
6
+
7
+ G = 6.67430e-11 # m^3 kg^-1 s^-2
8
+ MU0 = 1.2566370614359173e-6 # H/m, conventional nonmagnetic approximation
9
+ R_GAS = 8.31446261815324 # J/(mol K)
10
+ AVOGADRO = 6.02214076e23 # 1/mol
11
+ R_DRY_AIR = 287.05 # J/(kg K)
12
+ CP_DRY_AIR = 1004.0 # J/(kg K)
13
+ R_VAPOR = 461.5 # J/(kg K)
14
+ EPSILON_WATER = R_DRY_AIR / R_VAPOR
15
+ OMEGA_EARTH = 7.2921150e-5 # rad/s
16
+ SIGMA_SB = 5.670374419e-8 # W/(m^2 K^4)
17
+ GRAVITY = 9.80665 # m/s^2, standard gravity
18
+ EARTH_RADIUS = 6371008.8 # m, mean radius reference
19
+ SECONDS_PER_YEAR = 365.25 * 86400 # s, Julian year
20
+ LAMBDA_U238 = 1.55125e-10 / SECONDS_PER_YEAR # 1/s, Jaffey et al. (1971)
21
+ LAMBDA_U235 = 9.8485e-10 / SECONDS_PER_YEAR # 1/s, Jaffey et al. (1971)
22
+ U238_U235 = 137.88 # 1, present-day atomic ratio, Steiger and Jaeger (1977)
23
+ LIBBY_MEAN_LIFE = 8033 * SECONDS_PER_YEAR # s, conventional radiocarbon mean life (5568-year half-life)
@@ -0,0 +1,77 @@
1
+ """Inspectable geophysics models: gravity, the figure of the Earth and geomagnetism; SI units unless stated otherwise."""
2
+
3
+ from earthsciencekit.geophysics.systems.deformation import (
4
+ equivalent_water_height,
5
+ love_number_h2,
6
+ love_number_k2,
7
+ tidal_gravimetric_factor,
8
+ )
9
+ from earthsciencekit.geophysics.systems.dynamo import (
10
+ magnetic_decay_time,
11
+ magnetic_reynolds_number,
12
+ rikitake_fixed_point,
13
+ simulate_rikitake_dynamo,
14
+ )
15
+ from earthsciencekit.geophysics.systems.figure import (
16
+ centrifugal_ratio,
17
+ clairaut_gravity,
18
+ degree_geoid_height,
19
+ flattening_from_j2,
20
+ homogeneous_flattening,
21
+ nodal_precession_rate,
22
+ normal_gravity,
23
+ stokes_function,
24
+ )
25
+ from earthsciencekit.geophysics.systems.gravity import (
26
+ bouguer_anomaly,
27
+ bouguer_correction,
28
+ earth_mean_density,
29
+ free_air_correction,
30
+ sphere_gravity,
31
+ torsion_balance_gravitational_constant,
32
+ )
33
+ from earthsciencekit.geophysics.systems.magnetism import (
34
+ block_magnetic_anomaly,
35
+ dipole_field,
36
+ dipole_inclination,
37
+ direction_angle,
38
+ field_elements,
39
+ gauss_dipole_moment,
40
+ paleolatitude,
41
+ virtual_geomagnetic_pole,
42
+ zonal_field,
43
+ )
44
+
45
+ __all__ = [
46
+ "sphere_gravity",
47
+ "bouguer_correction",
48
+ "free_air_correction",
49
+ "bouguer_anomaly",
50
+ "torsion_balance_gravitational_constant",
51
+ "earth_mean_density",
52
+ "centrifugal_ratio",
53
+ "homogeneous_flattening",
54
+ "clairaut_gravity",
55
+ "normal_gravity",
56
+ "flattening_from_j2",
57
+ "nodal_precession_rate",
58
+ "stokes_function",
59
+ "degree_geoid_height",
60
+ "dipole_field",
61
+ "dipole_inclination",
62
+ "paleolatitude",
63
+ "zonal_field",
64
+ "gauss_dipole_moment",
65
+ "field_elements",
66
+ "direction_angle",
67
+ "virtual_geomagnetic_pole",
68
+ "block_magnetic_anomaly",
69
+ "magnetic_reynolds_number",
70
+ "magnetic_decay_time",
71
+ "rikitake_fixed_point",
72
+ "love_number_h2",
73
+ "love_number_k2",
74
+ "tidal_gravimetric_factor",
75
+ "equivalent_water_height",
76
+ "simulate_rikitake_dynamo",
77
+ ]
@@ -0,0 +1,5 @@
1
+ """Result containers for earthsciencekit.geophysics."""
2
+
3
+ from earthsciencekit.geophysics.core.base import TimeSeries
4
+
5
+ __all__ = ["TimeSeries"]
@@ -0,0 +1,10 @@
1
+ """Result containers for earthsciencekit.geophysics.
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.geophysics, grouped by topic."""
@@ -0,0 +1,161 @@
1
+ """Deformation of the Earth: tidal Love numbers and mass change from time-variable gravity.
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
+ "love_number_h2",
12
+ "love_number_k2",
13
+ "tidal_gravimetric_factor",
14
+ "equivalent_water_height",
15
+ ]
16
+
17
+
18
+ def _rigidity_ratio(rigidity, density, gravity, radius):
19
+ rigidity = quantity("rigidity", rigidity, lower=0)
20
+ density = quantity("density", density, lower=0, strict_lower=True)
21
+ gravity = quantity("gravity", gravity, lower=0, strict_lower=True)
22
+ radius = quantity("radius", radius, lower=0, strict_lower=True)
23
+ return 19 * rigidity / (2 * density * gravity * radius)
24
+
25
+
26
+ @model
27
+ def love_number_h2(rigidity, density, gravity, radius):
28
+ """Degree-2 displacement Love number of a homogeneous elastic Earth.
29
+
30
+ The solid Earth rises and falls with the tide by a fraction of the equilibrium tide that measures its rigidity.
31
+
32
+ Parameters
33
+ ----------
34
+ rigidity : float or array_like
35
+ Pa; constraint: nonnegative. Shear modulus; zero gives a fluid Earth.
36
+ density : float or array_like
37
+ kg/m^3; constraint: positive.
38
+ gravity : float or array_like
39
+ m/s^2; constraint: positive.
40
+ radius : float or array_like
41
+ m; constraint: positive.
42
+
43
+ Returns
44
+ -------
45
+ numpy scalar or ndarray
46
+ Model value [1]: radial displacement over the equilibrium tide height.
47
+
48
+ Notes
49
+ -----
50
+ Uniform, incompressible, self-gravitating elastic sphere (Kelvin's solution); ``h2 = 5 k2 / 3``.
51
+ Equation: ``2.5 / (1 + 19 * rigidity / (2 * density * gravity * radius))``.
52
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
53
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
54
+ """
55
+ return 2.5 / (1 + _rigidity_ratio(rigidity, density, gravity, radius))
56
+
57
+
58
+ @model
59
+ def love_number_k2(rigidity, density, gravity, radius):
60
+ """Degree-2 potential Love number of a homogeneous elastic Earth.
61
+
62
+ The tidal bulge adds its own gravitational potential, a fraction k2 of the tidal potential.
63
+
64
+ Parameters
65
+ ----------
66
+ rigidity : float or array_like
67
+ Pa; constraint: nonnegative. Shear modulus; zero gives a fluid Earth.
68
+ density : float or array_like
69
+ kg/m^3; constraint: positive.
70
+ gravity : float or array_like
71
+ m/s^2; constraint: positive.
72
+ radius : float or array_like
73
+ m; constraint: positive.
74
+
75
+ Returns
76
+ -------
77
+ numpy scalar or ndarray
78
+ Model value [1]. Scalar input returns a NumPy scalar; arrays broadcast.
79
+
80
+ Notes
81
+ -----
82
+ Uniform, incompressible, self-gravitating elastic sphere (Kelvin's solution); the fluid limit is 3/2.
83
+ Equation: ``1.5 / (1 + 19 * rigidity / (2 * density * gravity * radius))``.
84
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
85
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
86
+ """
87
+ return 1.5 / (1 + _rigidity_ratio(rigidity, density, gravity, radius))
88
+
89
+
90
+ @model
91
+ def tidal_gravimetric_factor(h2, k2):
92
+ """Gravimetric factor of the body tide.
93
+
94
+ A gravimeter on a yielding Earth sees the tide amplified by its uplift and reduced by the bulge's attraction.
95
+
96
+ Parameters
97
+ ----------
98
+ h2 : float or array_like
99
+ 1; constraint: nonnegative.
100
+ k2 : float or array_like
101
+ 1; constraint: nonnegative.
102
+
103
+ Returns
104
+ -------
105
+ numpy scalar or ndarray
106
+ Model value [1]: observed over rigid-Earth tidal gravity change.
107
+
108
+ Notes
109
+ -----
110
+ Degree-2 tide on a spherically symmetric elastic Earth, without ocean loading. About 1.16 for the real Earth.
111
+ Equation: ``1 + h2 - 1.5 * k2``.
112
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
113
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
114
+ """
115
+ h2 = quantity("h2", h2, lower=0)
116
+ k2 = quantity("k2", k2, lower=0)
117
+ return 1 + h2 - 1.5 * k2
118
+
119
+
120
+ @model
121
+ def equivalent_water_height(coefficient_change, degree, load_love_number, radius, mean_density, water_density):
122
+ """Water layer behind a change of one Stokes coefficient.
123
+
124
+ Monthly gravity fields from a satellite pair reveal the moving mass of water, ice and groundwater.
125
+
126
+ Parameters
127
+ ----------
128
+ coefficient_change : float or array_like
129
+ 1; constraint: real. Change of a fully normalized Stokes coefficient of degree ``n``.
130
+ degree : float or array_like
131
+ 1; constraint: integer >= 0.
132
+ load_love_number : float or array_like
133
+ 1; constraint: greater than -1. Elastic yielding ``k'_n`` of the Earth under the load.
134
+ radius : float or array_like
135
+ m; constraint: positive.
136
+ mean_density : float or array_like
137
+ kg/m^3; constraint: positive. Mean density of the Earth.
138
+ water_density : float or array_like
139
+ kg/m^3; constraint: positive.
140
+
141
+ Returns
142
+ -------
143
+ numpy scalar or ndarray
144
+ Model value [m]: coefficient of the same fully normalized harmonic in the water thickness.
145
+
146
+ Notes
147
+ -----
148
+ Thin surface layer on an elastic Earth (Wahr et al., 1998); valid while the layer is much thinner than the
149
+ resolution of the truncated expansion.
150
+ Equation: ``radius * mean_density * (2 * degree + 1) * coefficient_change / (3 * water_density * (1 + load_love_number))``.
151
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
152
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
153
+ """
154
+ coefficient_change = quantity("coefficient_change", coefficient_change)
155
+ degree = quantity("degree", degree, lower=0)
156
+ require(degree == np.round(degree), "degree must be an integer")
157
+ load_love_number = quantity("load_love_number", load_love_number, lower=-1, strict_lower=True)
158
+ radius = quantity("radius", radius, lower=0, strict_lower=True)
159
+ mean_density = quantity("mean_density", mean_density, lower=0, strict_lower=True)
160
+ water_density = quantity("water_density", water_density, lower=0, strict_lower=True)
161
+ return radius * mean_density * (2 * degree + 1) * coefficient_change / (3 * water_density * (1 + load_love_number))
@@ -0,0 +1,159 @@
1
+ """Geodynamo: magnetic diffusion, the magnetic Reynolds number and Rikitake's two-disc dynamo.
2
+
3
+ SI units unless stated otherwise.
4
+ """
5
+
6
+ import numpy as np
7
+
8
+ from earthsciencekit._validation import model, quantity, require, safe_arithmetic
9
+ from earthsciencekit.constants import MU0
10
+
11
+ __all__ = [
12
+ "magnetic_reynolds_number",
13
+ "magnetic_decay_time",
14
+ "rikitake_fixed_point",
15
+ "simulate_rikitake_dynamo",
16
+ ]
17
+
18
+
19
+ @model
20
+ def magnetic_reynolds_number(speed, length, conductivity):
21
+ """Magnetic Reynolds number.
22
+
23
+ A conducting fluid can stretch and regenerate a magnetic field only if advection beats diffusion.
24
+
25
+ Parameters
26
+ ----------
27
+ speed : float or array_like
28
+ m/s; constraint: nonnegative.
29
+ length : float or array_like
30
+ m; constraint: positive.
31
+ conductivity : float or array_like
32
+ S/m; constraint: positive.
33
+
34
+ Returns
35
+ -------
36
+ numpy scalar or ndarray
37
+ Model value [1]. Scalar input returns a NumPy scalar; arrays broadcast.
38
+
39
+ Notes
40
+ -----
41
+ Order-of-magnitude scale; dynamos need values above a geometry-dependent threshold of order 10 to 100.
42
+ Equation: ``MU0 * conductivity * speed * length``.
43
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
44
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
45
+ """
46
+ speed = quantity("speed", speed, lower=0)
47
+ length = quantity("length", length, lower=0, strict_lower=True)
48
+ conductivity = quantity("conductivity", conductivity, lower=0, strict_lower=True)
49
+ return MU0 * conductivity * speed * length
50
+
51
+
52
+ @model
53
+ def magnetic_decay_time(radius, conductivity):
54
+ """Free decay time of the slowest magnetic mode of a conducting sphere.
55
+
56
+ Without a dynamo the Earth's field would die in tens of thousands of years, far less than its age.
57
+
58
+ Parameters
59
+ ----------
60
+ radius : float or array_like
61
+ m; constraint: positive.
62
+ conductivity : float or array_like
63
+ S/m; constraint: positive.
64
+
65
+ Returns
66
+ -------
67
+ numpy scalar or ndarray
68
+ Model value [s]: e-folding time of the field of the slowest dipole mode.
69
+
70
+ Notes
71
+ -----
72
+ Uniform stationary conductor surrounded by an insulator.
73
+ Equation: ``MU0 * conductivity * radius**2 / np.pi**2``.
74
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
75
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
76
+ """
77
+ radius = quantity("radius", radius, lower=0, strict_lower=True)
78
+ conductivity = quantity("conductivity", conductivity, lower=0, strict_lower=True)
79
+ return MU0 * conductivity * radius**2 / np.pi**2
80
+
81
+
82
+ @model
83
+ def rikitake_fixed_point(mu, k):
84
+ """Steady state of Rikitake's two-disc dynamo.
85
+
86
+ The two coupled discs have a pair of steady states of opposite polarity, which the chaotic flow visits in turn.
87
+
88
+ Parameters
89
+ ----------
90
+ mu : float or array_like
91
+ 1; constraint: positive. Scaled resistance.
92
+ k : float or array_like
93
+ 1; constraint: positive. Sets ``A = mu * (k**2 - 1 / k**2)``.
94
+
95
+ Returns
96
+ -------
97
+ ndarray, shape (..., 3)
98
+ Model value [1]: the scaled currents ``x``, ``y`` and the scaled angular speed ``z`` of the positive state; the
99
+ other state is ``(-x, -y, z)``.
100
+
101
+ Notes
102
+ -----
103
+ Nondimensional system of :func:`simulate_rikitake_dynamo`. Both states are unstable for the usual parameters.
104
+ Equation: ``[k, 1 / k, mu * k**2]``.
105
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
106
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
107
+ """
108
+ mu = quantity("mu", mu, lower=0, strict_lower=True)
109
+ k = quantity("k", k, lower=0, strict_lower=True)
110
+ return np.stack(np.broadcast_arrays(k, 1 / k, mu * k**2), axis=-1)
111
+
112
+
113
+ @safe_arithmetic
114
+ def simulate_rikitake_dynamo(mu, k, time_scale, time, *, initial_state=(1.0, 0.0, 0.0)):
115
+ """Integrate Rikitake's coupled two-disc dynamo.
116
+
117
+ Parameters
118
+ ----------
119
+ mu : float
120
+ Positive scalar scaled resistance [1].
121
+ k : float
122
+ Positive scalar setting the torque asymmetry ``A = mu * (k**2 - 1 / k**2)`` [1].
123
+ time_scale : float
124
+ Positive scalar seconds per nondimensional time unit [s].
125
+ time : array_like
126
+ Nonnegative strictly increasing sampled times [s].
127
+ initial_state : sequence of float
128
+ Scaled currents ``x``, ``y`` and angular speed ``z`` at the first time [1].
129
+
130
+ Returns
131
+ -------
132
+ TimeSeries
133
+ The scaled currents ``x`` and ``y`` (whose sign is the polarity) and the scaled angular speed ``z`` [1].
134
+
135
+ Notes
136
+ -----
137
+ ``dx/dt = -mu * x + y * z``, ``dy/dt = -mu * y + (z - A) * x``, ``dz/dt = 1 - x * y`` in units of ``time_scale``.
138
+ The steady states are :func:`rikitake_fixed_point`. Trajectories oscillate about one and jump irregularly to the
139
+ other, reversing both currents: a chaotic analogue of geomagnetic reversals, not a model of the core.
140
+ """
141
+ from earthsciencekit._validation import time_grid
142
+ from earthsciencekit.geophysics.core.base import TimeSeries
143
+ from earthsciencekit.integrators import integrate_ode
144
+
145
+ mu = quantity("mu", mu, lower=0, strict_lower=True)
146
+ k = quantity("k", k, lower=0, strict_lower=True)
147
+ time_scale = quantity("time_scale", time_scale, lower=0, strict_lower=True)
148
+ initial_state = quantity("initial_state", initial_state)
149
+ require(all(v.ndim == 0 for v in (mu, k, time_scale)), "dynamo parameters must be scalars")
150
+ require(initial_state.shape == (3,), "initial_state must hold x, y and z")
151
+ time = time_grid(time)
152
+ a = mu * (k**2 - 1 / k**2)
153
+
154
+ def rhs(t, state):
155
+ x, y, z = state
156
+ return [(-mu * x + y * z) / time_scale, (-mu * y + (z - a) * x) / time_scale, (1 - x * y) / time_scale]
157
+
158
+ result = integrate_ode(rhs, initial_state, time, names=("x", "y", "z"), units=("1", "1", "1"), method="DOP853", rtol=1e-10)
159
+ return TimeSeries(result.time, result.values, result.names, result.units)