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,388 @@
|
|
|
1
|
+
"""Circulation: geostrophy, Ekman and Sverdrup transport, the Stommel gyre, the Rossby radius and thermohaline boxes.
|
|
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
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"geostrophic_current",
|
|
12
|
+
"ekman_transport",
|
|
13
|
+
"rossby_radius",
|
|
14
|
+
"volume_transport",
|
|
15
|
+
"ekman_spiral_velocity",
|
|
16
|
+
"sverdrup_transport",
|
|
17
|
+
"stommel_streamfunction",
|
|
18
|
+
"thermohaline_forcing",
|
|
19
|
+
"simulate_thermohaline_boxes",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@model
|
|
24
|
+
def geostrophic_current(surface_slope, coriolis, gravity):
|
|
25
|
+
"""Sea-surface geostrophic component.
|
|
26
|
+
|
|
27
|
+
Sea-surface tilt can reveal large-scale currents.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
surface_slope : float or array_like
|
|
32
|
+
1; constraint: real.
|
|
33
|
+
coriolis : float or array_like
|
|
34
|
+
1/s; constraint: nonzero.
|
|
35
|
+
gravity : float or array_like
|
|
36
|
+
m/s^2; constraint: positive.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
numpy scalar or ndarray
|
|
41
|
+
Model value [Pa]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
42
|
+
|
|
43
|
+
Notes
|
|
44
|
+
-----
|
|
45
|
+
Signed cross-slope component away from the equator; no ageostrophic velocity.
|
|
46
|
+
Equation: ``gravity * surface_slope / coriolis``.
|
|
47
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
48
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
49
|
+
"""
|
|
50
|
+
surface_slope = quantity("surface_slope", surface_slope)
|
|
51
|
+
coriolis = quantity("coriolis", coriolis)
|
|
52
|
+
require(coriolis != 0, "coriolis must be nonzero")
|
|
53
|
+
gravity = quantity("gravity", gravity, lower=0, strict_lower=True)
|
|
54
|
+
return gravity * surface_slope / coriolis
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@model
|
|
58
|
+
def ekman_transport(wind_stress, density, coriolis):
|
|
59
|
+
"""Cross-wind Ekman transport magnitude.
|
|
60
|
+
|
|
61
|
+
Wind stress and rotation drive vertically integrated transport.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
wind_stress : float or array_like
|
|
66
|
+
Pa; constraint: real.
|
|
67
|
+
density : float or array_like
|
|
68
|
+
kg/m^3; constraint: positive.
|
|
69
|
+
coriolis : float or array_like
|
|
70
|
+
1/s; constraint: nonzero.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
numpy scalar or ndarray
|
|
75
|
+
Model value [Pa]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
76
|
+
|
|
77
|
+
Notes
|
|
78
|
+
-----
|
|
79
|
+
Signed cross-wind component per unit width; direction requires a consistent vector convention.
|
|
80
|
+
Equation: ``wind_stress / (density * coriolis)``.
|
|
81
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
82
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
83
|
+
"""
|
|
84
|
+
wind_stress = quantity("wind_stress", wind_stress)
|
|
85
|
+
density = quantity("density", density, lower=0, strict_lower=True)
|
|
86
|
+
coriolis = quantity("coriolis", coriolis)
|
|
87
|
+
require(coriolis != 0, "coriolis must be nonzero")
|
|
88
|
+
return wind_stress / (density * coriolis)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@model
|
|
92
|
+
def ekman_spiral_velocity(depth, wind_stress, density, eddy_viscosity, coriolis):
|
|
93
|
+
"""Velocity profile of the Ekman spiral.
|
|
94
|
+
|
|
95
|
+
Wind stress diffused downward by friction turns and decays with depth under rotation.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
depth : float or array_like
|
|
100
|
+
m; constraint: nonnegative.
|
|
101
|
+
wind_stress : float or array_like
|
|
102
|
+
Pa; constraint: real. Signed stress along the wind axis.
|
|
103
|
+
density : float or array_like
|
|
104
|
+
kg/m^3; constraint: positive.
|
|
105
|
+
eddy_viscosity : float or array_like
|
|
106
|
+
m^2/s; constraint: positive. Constant vertical eddy viscosity.
|
|
107
|
+
coriolis : float or array_like
|
|
108
|
+
1/s; constraint: nonzero.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
ndarray, shape (..., 2)
|
|
113
|
+
Model value [m/s]: the downwind component and the crosswind component, positive to the right of the wind.
|
|
114
|
+
|
|
115
|
+
Notes
|
|
116
|
+
-----
|
|
117
|
+
Steady, infinitely deep ocean with constant eddy viscosity and no pressure gradient. The surface current is 45 degrees
|
|
118
|
+
to the right of the wind in the northern hemisphere (left in the southern). The crosswind component integrated over
|
|
119
|
+
depth equals :func:`ekman_transport`; the downwind component integrates to zero.
|
|
120
|
+
Equation: ``v0 * np.exp(-s) * [np.sin(np.pi / 4 - s), np.sign(coriolis) * np.cos(np.pi / 4 - s)]``
|
|
121
|
+
with ``s = depth / d``, ``d = np.sqrt(2 * eddy_viscosity / abs(coriolis))`` and
|
|
122
|
+
``v0 = wind_stress / (density * np.sqrt(eddy_viscosity * abs(coriolis)))``.
|
|
123
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
124
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
125
|
+
"""
|
|
126
|
+
depth = quantity("depth", depth, lower=0)
|
|
127
|
+
wind_stress = quantity("wind_stress", wind_stress)
|
|
128
|
+
density = quantity("density", density, lower=0, strict_lower=True)
|
|
129
|
+
eddy_viscosity = quantity("eddy_viscosity", eddy_viscosity, lower=0, strict_lower=True)
|
|
130
|
+
coriolis = quantity("coriolis", coriolis)
|
|
131
|
+
require(coriolis != 0, "coriolis must be nonzero")
|
|
132
|
+
scaled = depth / np.sqrt(2 * eddy_viscosity / np.abs(coriolis))
|
|
133
|
+
surface = wind_stress / (density * np.sqrt(eddy_viscosity * np.abs(coriolis)))
|
|
134
|
+
decay = surface * np.exp(-scaled)
|
|
135
|
+
return np.stack([decay * np.sin(np.pi / 4 - scaled), np.sign(coriolis) * decay * np.cos(np.pi / 4 - scaled)], axis=-1)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@model
|
|
139
|
+
def volume_transport(speed, width, depth):
|
|
140
|
+
"""Section volume transport.
|
|
141
|
+
|
|
142
|
+
Integrating velocity over a section produces volume transport.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
speed : float or array_like
|
|
147
|
+
m/s; constraint: real.
|
|
148
|
+
width : float or array_like
|
|
149
|
+
m; constraint: positive.
|
|
150
|
+
depth : float or array_like
|
|
151
|
+
m; constraint: positive.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
numpy scalar or ndarray
|
|
156
|
+
Model value [Pa]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
157
|
+
|
|
158
|
+
Notes
|
|
159
|
+
-----
|
|
160
|
+
Uniform signed velocity and rectangular section; output m cubed per second, not Sverdrups.
|
|
161
|
+
Equation: ``speed * width * depth``.
|
|
162
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
163
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
164
|
+
"""
|
|
165
|
+
speed = quantity("speed", speed)
|
|
166
|
+
width = quantity("width", width, lower=0, strict_lower=True)
|
|
167
|
+
depth = quantity("depth", depth, lower=0, strict_lower=True)
|
|
168
|
+
return speed * width * depth
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@model
|
|
172
|
+
def sverdrup_transport(wind_stress_curl, density, beta):
|
|
173
|
+
"""Sverdrup meridional transport.
|
|
174
|
+
|
|
175
|
+
Wind-stress curl drives interior flow across planetary vorticity contours.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
wind_stress_curl : float or array_like
|
|
180
|
+
Pa/m; constraint: real.
|
|
181
|
+
density : float or array_like
|
|
182
|
+
kg/m^3; constraint: positive.
|
|
183
|
+
beta : float or array_like
|
|
184
|
+
1/(m s); constraint: positive. Northward gradient of the Coriolis parameter.
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
numpy scalar or ndarray
|
|
189
|
+
Model value [m^2/s]: depth-integrated northward transport per unit zonal width.
|
|
190
|
+
|
|
191
|
+
Notes
|
|
192
|
+
-----
|
|
193
|
+
Steady linear interior balance away from boundaries, friction and the equator's singular Ekman layer.
|
|
194
|
+
Negative curl (subtropical gyres) drives equatorward flow.
|
|
195
|
+
Equation: ``wind_stress_curl / (density * beta)``.
|
|
196
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
197
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
198
|
+
"""
|
|
199
|
+
wind_stress_curl = quantity("wind_stress_curl", wind_stress_curl)
|
|
200
|
+
density = quantity("density", density, lower=0, strict_lower=True)
|
|
201
|
+
beta = quantity("beta", beta, lower=0, strict_lower=True)
|
|
202
|
+
return wind_stress_curl / (density * beta)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@model
|
|
206
|
+
def stommel_streamfunction(x, y, wind_stress, basin_width, basin_length, density, beta, friction):
|
|
207
|
+
"""Transport streamfunction of Stommel's wind-driven gyre.
|
|
208
|
+
|
|
209
|
+
Bottom friction and the beta effect confine the return flow to a western boundary current.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
x : float or array_like
|
|
214
|
+
m; constraint: 0 <= x <= basin_width. Eastward distance from the western wall.
|
|
215
|
+
y : float or array_like
|
|
216
|
+
m; constraint: 0 <= y <= basin_length. Northward distance from the southern wall.
|
|
217
|
+
wind_stress : float or array_like
|
|
218
|
+
Pa; constraint: real. Amplitude of the zonal stress ``-wind_stress * cos(pi y / basin_length)``.
|
|
219
|
+
basin_width : float or array_like
|
|
220
|
+
m; constraint: positive.
|
|
221
|
+
basin_length : float or array_like
|
|
222
|
+
m; constraint: positive.
|
|
223
|
+
density : float or array_like
|
|
224
|
+
kg/m^3; constraint: positive.
|
|
225
|
+
beta : float or array_like
|
|
226
|
+
1/(m s); constraint: nonnegative. Zero gives Stommel's symmetric uniform-rotation gyre.
|
|
227
|
+
friction : float or array_like
|
|
228
|
+
1/s; constraint: positive. Linear bottom-drag coefficient on the depth-integrated flow.
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
numpy scalar or ndarray
|
|
233
|
+
Model value [m^3/s]. Northward transport is the eastward derivative.
|
|
234
|
+
|
|
235
|
+
Notes
|
|
236
|
+
-----
|
|
237
|
+
Steady linear barotropic vorticity balance ``friction * laplacian(psi) + beta * dpsi/dx = curl(tau) / density`` in a
|
|
238
|
+
rectangular basin with ``psi = 0`` on the walls. Positive wind stress gives a clockwise (subtropical) gyre whose
|
|
239
|
+
western boundary layer has width ``friction / beta``.
|
|
240
|
+
Equation: ``wind_stress * basin_length / (np.pi * density * friction) * np.sin(np.pi * y / basin_length) * X``
|
|
241
|
+
with ``X = 1 + ((np.exp(m2 * L) - 1) * np.exp(m1 * (x - L)) + (np.exp(-m1 * L) - 1) * np.exp(m2 * x))
|
|
242
|
+
/ (1 - np.exp((m2 - m1) * L))``, ``L = basin_width``,
|
|
243
|
+
``m1, m2 = (-beta +/- np.sqrt(beta**2 + 4 * friction**2 * k**2)) / (2 * friction)`` and ``k = np.pi / basin_length``.
|
|
244
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
245
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
246
|
+
"""
|
|
247
|
+
x = quantity("x", x, lower=0)
|
|
248
|
+
y = quantity("y", y, lower=0)
|
|
249
|
+
wind_stress = quantity("wind_stress", wind_stress)
|
|
250
|
+
basin_width = quantity("basin_width", basin_width, lower=0, strict_lower=True)
|
|
251
|
+
basin_length = quantity("basin_length", basin_length, lower=0, strict_lower=True)
|
|
252
|
+
density = quantity("density", density, lower=0, strict_lower=True)
|
|
253
|
+
beta = quantity("beta", beta, lower=0)
|
|
254
|
+
friction = quantity("friction", friction, lower=0, strict_lower=True)
|
|
255
|
+
require(x <= basin_width, "x must lie inside the basin")
|
|
256
|
+
require(y <= basin_length, "y must lie inside the basin")
|
|
257
|
+
k = np.pi / basin_length
|
|
258
|
+
root = np.sqrt(beta**2 + 4 * friction**2 * k**2)
|
|
259
|
+
m1 = 2 * friction * k**2 / (beta + root)
|
|
260
|
+
m2 = -(beta + root) / (2 * friction)
|
|
261
|
+
shape = 1 + ((np.exp(m2 * basin_width) - 1) * np.exp(m1 * (x - basin_width)) + (np.exp(-m1 * basin_width) - 1) * np.exp(m2 * x)) / (
|
|
262
|
+
1 - np.exp((m2 - m1) * basin_width)
|
|
263
|
+
)
|
|
264
|
+
return wind_stress * basin_length / (np.pi * density * friction) * np.sin(np.pi * y / basin_length) * shape
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@model
|
|
268
|
+
def rossby_radius(buoyancy_frequency, depth, coriolis):
|
|
269
|
+
"""Baroclinic deformation scale.
|
|
270
|
+
|
|
271
|
+
Stratification and rotation set a horizontal adjustment scale.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
buoyancy_frequency : float or array_like
|
|
276
|
+
1/s; constraint: positive.
|
|
277
|
+
depth : float or array_like
|
|
278
|
+
m; constraint: positive.
|
|
279
|
+
coriolis : float or array_like
|
|
280
|
+
1/s; constraint: nonzero.
|
|
281
|
+
|
|
282
|
+
Returns
|
|
283
|
+
-------
|
|
284
|
+
numpy scalar or ndarray
|
|
285
|
+
Model value [Pa]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
286
|
+
|
|
287
|
+
Notes
|
|
288
|
+
-----
|
|
289
|
+
First-mode constant-N rigid-boundary estimate N H over pi f; not a general layered solution.
|
|
290
|
+
Equation: ``buoyancy_frequency * depth / (np.pi * np.abs(coriolis))``.
|
|
291
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
292
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
293
|
+
"""
|
|
294
|
+
buoyancy_frequency = quantity("buoyancy_frequency", buoyancy_frequency, lower=0, strict_lower=True)
|
|
295
|
+
depth = quantity("depth", depth, lower=0, strict_lower=True)
|
|
296
|
+
coriolis = quantity("coriolis", coriolis)
|
|
297
|
+
require(coriolis != 0, "coriolis must be nonzero")
|
|
298
|
+
return buoyancy_frequency * depth / (np.pi * np.abs(coriolis))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@model
|
|
302
|
+
def thermohaline_forcing(flow, thermal_flow, haline_coefficient):
|
|
303
|
+
"""Salt forcing that holds Stommel's two-box overturning steady.
|
|
304
|
+
|
|
305
|
+
Salt advection by the overturning must balance the freshwater-driven salinity contrast.
|
|
306
|
+
|
|
307
|
+
Parameters
|
|
308
|
+
----------
|
|
309
|
+
flow : float or array_like
|
|
310
|
+
m^3/s; constraint: at most thermal_flow. Overturning, positive when polar water sinks.
|
|
311
|
+
thermal_flow : float or array_like
|
|
312
|
+
m^3/s; constraint: positive. Overturning the temperature contrast alone would drive.
|
|
313
|
+
haline_coefficient : float or array_like
|
|
314
|
+
m^3/s per g/kg; constraint: positive. Overturning lost per unit salinity contrast.
|
|
315
|
+
|
|
316
|
+
Returns
|
|
317
|
+
-------
|
|
318
|
+
numpy scalar or ndarray
|
|
319
|
+
Model value [g/kg m^3/s]: the virtual salt flux into the equatorial box.
|
|
320
|
+
|
|
321
|
+
Notes
|
|
322
|
+
-----
|
|
323
|
+
Temperatures held fixed by fast restoring; flow ``q = thermal_flow - haline_coefficient * dS`` and steady salt budget
|
|
324
|
+
``abs(q) * dS = forcing``. Read as forcing against flow, the curve folds at ``q = thermal_flow / 2``: for
|
|
325
|
+
``0 < forcing < thermal_flow**2 / (4 * haline_coefficient)`` a thermal and a haline state coexist.
|
|
326
|
+
Equation: ``np.abs(flow) * (thermal_flow - flow) / haline_coefficient``.
|
|
327
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
328
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
329
|
+
"""
|
|
330
|
+
flow = quantity("flow", flow)
|
|
331
|
+
thermal_flow = quantity("thermal_flow", thermal_flow, lower=0, strict_lower=True)
|
|
332
|
+
haline_coefficient = quantity("haline_coefficient", haline_coefficient, lower=0, strict_lower=True)
|
|
333
|
+
require(flow <= thermal_flow, "flow above thermal_flow implies a negative salinity contrast")
|
|
334
|
+
return np.abs(flow) * (thermal_flow - flow) / haline_coefficient
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@safe_arithmetic
|
|
338
|
+
def simulate_thermohaline_boxes(thermal_flow, haline_coefficient, salt_forcing, volume, time, *, initial_salinity_difference=0.0):
|
|
339
|
+
"""Integrate Stommel's two-box thermohaline circulation.
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
thermal_flow : float
|
|
344
|
+
Positive scalar overturning driven by the fixed temperature contrast [m^3/s].
|
|
345
|
+
haline_coefficient : float
|
|
346
|
+
Positive scalar overturning lost per unit salinity contrast [m^3/s per g/kg].
|
|
347
|
+
salt_forcing : float
|
|
348
|
+
Nonnegative scalar virtual salt flux into the equatorial box [g/kg m^3/s].
|
|
349
|
+
volume : float
|
|
350
|
+
Positive scalar volume of each box [m^3].
|
|
351
|
+
time : array_like
|
|
352
|
+
Nonnegative strictly increasing sampled times [s].
|
|
353
|
+
initial_salinity_difference : float
|
|
354
|
+
Equatorial minus polar salinity at the first time [g/kg].
|
|
355
|
+
|
|
356
|
+
Returns
|
|
357
|
+
-------
|
|
358
|
+
TimeSeries
|
|
359
|
+
Salinity difference [g/kg] and overturning [m^3/s], positive when polar water sinks.
|
|
360
|
+
|
|
361
|
+
Notes
|
|
362
|
+
-----
|
|
363
|
+
``volume * dS/dt = salt_forcing - abs(q) * dS`` with ``q = thermal_flow - haline_coefficient * dS``.
|
|
364
|
+
Temperatures are held fixed by fast restoring. Steady states satisfy
|
|
365
|
+
:func:`thermohaline_forcing`; below the fold forcing
|
|
366
|
+
thermal_flow**2 / (4 * haline_coefficient) the initial state decides
|
|
367
|
+
between the thermal (q > 0) and haline (q < 0) regimes.
|
|
368
|
+
"""
|
|
369
|
+
from earthsciencekit._validation import time_grid
|
|
370
|
+
from earthsciencekit.integrators import integrate_ode
|
|
371
|
+
from earthsciencekit.oceanography.core.base import TimeSeries
|
|
372
|
+
|
|
373
|
+
thermal_flow = quantity("thermal_flow", thermal_flow, lower=0, strict_lower=True)
|
|
374
|
+
haline_coefficient = quantity("haline_coefficient", haline_coefficient, lower=0, strict_lower=True)
|
|
375
|
+
salt_forcing = quantity("salt_forcing", salt_forcing, lower=0)
|
|
376
|
+
volume = quantity("volume", volume, lower=0, strict_lower=True)
|
|
377
|
+
initial_salinity_difference = quantity("initial_salinity_difference", initial_salinity_difference)
|
|
378
|
+
scalars = (thermal_flow, haline_coefficient, salt_forcing, volume, initial_salinity_difference)
|
|
379
|
+
require(all(v.ndim == 0 for v in scalars), "box parameters must be scalars")
|
|
380
|
+
time = time_grid(time)
|
|
381
|
+
|
|
382
|
+
def rhs(t, state):
|
|
383
|
+
flow = thermal_flow - haline_coefficient * state[0]
|
|
384
|
+
return [(salt_forcing - np.abs(flow) * state[0]) / volume]
|
|
385
|
+
|
|
386
|
+
salinity = integrate_ode(rhs, [initial_salinity_difference], time).values[:, 0]
|
|
387
|
+
values = np.column_stack([salinity, thermal_flow - haline_coefficient * salinity])
|
|
388
|
+
return TimeSeries(time, values, ("salinity_difference", "overturning"), ("g/kg", "m^3/s"))
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Hydrography: hydrostatic pressure, equations of state, stratification and the abyssal temperature profile.
|
|
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
|
+
"hydrostatic_pressure",
|
|
12
|
+
"linear_density",
|
|
13
|
+
"buoyancy_frequency_squared",
|
|
14
|
+
"abyssal_temperature",
|
|
15
|
+
"simplified_seawater_density",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@model
|
|
20
|
+
def hydrostatic_pressure(density, depth, gravity):
|
|
21
|
+
"""Ocean hydrostatic pressure.
|
|
22
|
+
|
|
23
|
+
Water weight connects pressure and depth.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
density : float or array_like
|
|
28
|
+
kg/m^3; constraint: positive.
|
|
29
|
+
depth : float or array_like
|
|
30
|
+
m; constraint: nonnegative.
|
|
31
|
+
gravity : float or array_like
|
|
32
|
+
m/s^2; constraint: positive.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
numpy scalar or ndarray
|
|
37
|
+
Model value [Pa]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
38
|
+
|
|
39
|
+
Notes
|
|
40
|
+
-----
|
|
41
|
+
Gauge pressure with constant density; atmospheric pressure is excluded.
|
|
42
|
+
Equation: ``density * gravity * depth``.
|
|
43
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
44
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
45
|
+
"""
|
|
46
|
+
density = quantity("density", density, lower=0, strict_lower=True)
|
|
47
|
+
depth = quantity("depth", depth, lower=0)
|
|
48
|
+
gravity = quantity("gravity", gravity, lower=0, strict_lower=True)
|
|
49
|
+
return density * gravity * depth
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@model
|
|
53
|
+
def linear_density(reference_density, temperature_anomaly, salinity_anomaly, thermal_expansion, haline_contraction):
|
|
54
|
+
"""Linear seawater equation of state.
|
|
55
|
+
|
|
56
|
+
Temperature and salinity both change buoyancy.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
reference_density : float or array_like
|
|
61
|
+
kg/m^3; constraint: positive.
|
|
62
|
+
temperature_anomaly : float or array_like
|
|
63
|
+
K; constraint: real.
|
|
64
|
+
salinity_anomaly : float or array_like
|
|
65
|
+
g/kg; constraint: real.
|
|
66
|
+
thermal_expansion : float or array_like
|
|
67
|
+
1/K; constraint: nonnegative.
|
|
68
|
+
haline_contraction : float or array_like
|
|
69
|
+
kg/g; constraint: nonnegative.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
numpy scalar or ndarray
|
|
74
|
+
Model value [Pa]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
75
|
+
|
|
76
|
+
Notes
|
|
77
|
+
-----
|
|
78
|
+
Local linearization around a supplied reference; not TEOS-10 and no pressure dependence.
|
|
79
|
+
Equation: ``reference_density * (1 - thermal_expansion * temperature_anomaly + haline_contraction * salinity_anomaly)``.
|
|
80
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
81
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
82
|
+
"""
|
|
83
|
+
reference_density = quantity("reference_density", reference_density, lower=0, strict_lower=True)
|
|
84
|
+
temperature_anomaly = quantity("temperature_anomaly", temperature_anomaly)
|
|
85
|
+
salinity_anomaly = quantity("salinity_anomaly", salinity_anomaly)
|
|
86
|
+
thermal_expansion = quantity("thermal_expansion", thermal_expansion, lower=0)
|
|
87
|
+
haline_contraction = quantity("haline_contraction", haline_contraction, lower=0)
|
|
88
|
+
require(
|
|
89
|
+
1 - thermal_expansion * temperature_anomaly + haline_contraction * salinity_anomaly > 0, "linearization implies nonpositive density"
|
|
90
|
+
)
|
|
91
|
+
return reference_density * (1 - thermal_expansion * temperature_anomaly + haline_contraction * salinity_anomaly)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@model
|
|
95
|
+
def simplified_seawater_density(conservative_temperature, absolute_salinity, depth):
|
|
96
|
+
"""Simplified nonlinear seawater density fitted to TEOS-10.
|
|
97
|
+
|
|
98
|
+
Cabbeling and thermobaricity make seawater density nonlinear in temperature, salinity and pressure.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
conservative_temperature : float or array_like
|
|
103
|
+
K; constraint: positive.
|
|
104
|
+
absolute_salinity : float or array_like
|
|
105
|
+
g/kg; constraint: nonnegative.
|
|
106
|
+
depth : float or array_like
|
|
107
|
+
m; constraint: nonnegative.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
numpy scalar or ndarray
|
|
112
|
+
Model value [kg/m^3]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
113
|
+
|
|
114
|
+
Notes
|
|
115
|
+
-----
|
|
116
|
+
The S-EOS of Roquet et al. (2015) with NEMO's default coefficients: ``a0 = 0.16550``, ``b0 = 0.76554``,
|
|
117
|
+
``lambda1 = 5.9520e-2``, ``lambda2 = 7.4914e-4``, ``mu1 = 1.4970e-4``, ``mu2 = 1.1090e-5``, ``nu = 2.4341e-3``
|
|
118
|
+
and ``rho0 = 1026``, with anomalies ``t = conservative_temperature - 283.15`` and ``s = absolute_salinity - 35``.
|
|
119
|
+
It reproduces the TEOS-10 density gradients over typical ocean conditions; absolute density can be off by about
|
|
120
|
+
1 kg/m^3 and it is not TEOS-10 itself.
|
|
121
|
+
Equation: ``1026 - a0 * (1 + lambda1 * t / 2 + mu1 * depth) * t + b0 * (1 - lambda2 * s / 2 - mu2 * depth) * s - nu * t * s``.
|
|
122
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
123
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
124
|
+
"""
|
|
125
|
+
conservative_temperature = quantity("conservative_temperature", conservative_temperature, lower=0, strict_lower=True)
|
|
126
|
+
absolute_salinity = quantity("absolute_salinity", absolute_salinity, lower=0)
|
|
127
|
+
depth = quantity("depth", depth, lower=0)
|
|
128
|
+
t = conservative_temperature - 283.15
|
|
129
|
+
s = absolute_salinity - 35
|
|
130
|
+
thermal = 0.16550 * (1 + 0.5 * 5.9520e-2 * t + 1.4970e-4 * depth) * t
|
|
131
|
+
haline = 0.76554 * (1 - 0.5 * 7.4914e-4 * s - 1.1090e-5 * depth) * s
|
|
132
|
+
return 1026 - thermal + haline - 2.4341e-3 * t * s
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@model
|
|
136
|
+
def buoyancy_frequency_squared(density, upward_density_gradient, gravity):
|
|
137
|
+
"""Ocean density stratification.
|
|
138
|
+
|
|
139
|
+
Vertical density gradients control internal oscillations and convection.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
density : float or array_like
|
|
144
|
+
kg/m^3; constraint: positive.
|
|
145
|
+
upward_density_gradient : float or array_like
|
|
146
|
+
kg/m^4; constraint: real.
|
|
147
|
+
gravity : float or array_like
|
|
148
|
+
m/s^2; constraint: positive.
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
numpy scalar or ndarray
|
|
153
|
+
Model value [Pa]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
154
|
+
|
|
155
|
+
Notes
|
|
156
|
+
-----
|
|
157
|
+
Coordinate increases upward; negative output denotes unstable stratification.
|
|
158
|
+
Equation: ``-gravity * upward_density_gradient / density``.
|
|
159
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
160
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
161
|
+
"""
|
|
162
|
+
density = quantity("density", density, lower=0, strict_lower=True)
|
|
163
|
+
upward_density_gradient = quantity("upward_density_gradient", upward_density_gradient)
|
|
164
|
+
gravity = quantity("gravity", gravity, lower=0, strict_lower=True)
|
|
165
|
+
return -gravity * upward_density_gradient / density
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@model
|
|
169
|
+
def abyssal_temperature(height, bottom_temperature, top_temperature, column_height, diffusivity, upwelling):
|
|
170
|
+
"""Munk's advective-diffusive abyssal temperature profile.
|
|
171
|
+
|
|
172
|
+
Slow upwelling balanced by downward mixing of heat sets an exponential thermocline.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
height : float or array_like
|
|
177
|
+
m; constraint: 0 <= height <= column_height. Height above the bottom of the column.
|
|
178
|
+
bottom_temperature : float or array_like
|
|
179
|
+
K; constraint: positive.
|
|
180
|
+
top_temperature : float or array_like
|
|
181
|
+
K; constraint: positive.
|
|
182
|
+
column_height : float or array_like
|
|
183
|
+
m; constraint: positive.
|
|
184
|
+
diffusivity : float or array_like
|
|
185
|
+
m^2/s; constraint: positive. Vertical eddy diffusivity.
|
|
186
|
+
upwelling : float or array_like
|
|
187
|
+
m/s; constraint: positive.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
numpy scalar or ndarray
|
|
192
|
+
Model value [K]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
193
|
+
|
|
194
|
+
Notes
|
|
195
|
+
-----
|
|
196
|
+
Steady one-dimensional balance ``upwelling * dT/dz = diffusivity * d2T/dz2`` with fixed end temperatures;
|
|
197
|
+
the scale height is ``h = diffusivity / upwelling`` (about 1 km in Munk's recipe).
|
|
198
|
+
Equation: ``bottom_temperature + (top_temperature - bottom_temperature)
|
|
199
|
+
* (np.exp((height - column_height) / h) - np.exp(-column_height / h)) / (1 - np.exp(-column_height / h))``.
|
|
200
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
201
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
202
|
+
"""
|
|
203
|
+
height = quantity("height", height, lower=0)
|
|
204
|
+
bottom_temperature = quantity("bottom_temperature", bottom_temperature, lower=0, strict_lower=True)
|
|
205
|
+
top_temperature = quantity("top_temperature", top_temperature, lower=0, strict_lower=True)
|
|
206
|
+
column_height = quantity("column_height", column_height, lower=0, strict_lower=True)
|
|
207
|
+
diffusivity = quantity("diffusivity", diffusivity, lower=0, strict_lower=True)
|
|
208
|
+
upwelling = quantity("upwelling", upwelling, lower=0, strict_lower=True)
|
|
209
|
+
require(height <= column_height, "height must lie inside the column")
|
|
210
|
+
scale = diffusivity / upwelling
|
|
211
|
+
shape = np.exp((height - column_height) / scale) * -np.expm1(-height / scale) / -np.expm1(-column_height / scale)
|
|
212
|
+
return bottom_temperature + (top_temperature - bottom_temperature) * shape
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Inertial motion: the inertial period and damped inertial currents.
|
|
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
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"inertial_period",
|
|
12
|
+
"simulate_inertial_current",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@model
|
|
17
|
+
def inertial_period(coriolis):
|
|
18
|
+
"""Inertial oscillation period.
|
|
19
|
+
|
|
20
|
+
Rotation supplies a natural timescale for ocean motion.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
coriolis : float or array_like
|
|
25
|
+
1/s; constraint: nonzero.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
numpy scalar or ndarray
|
|
30
|
+
Model value [Pa]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
31
|
+
|
|
32
|
+
Notes
|
|
33
|
+
-----
|
|
34
|
+
No pressure gradient or friction; invalid at zero Coriolis parameter.
|
|
35
|
+
Equation: ``2 * np.pi / np.abs(coriolis)``.
|
|
36
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
37
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
38
|
+
"""
|
|
39
|
+
coriolis = quantity("coriolis", coriolis)
|
|
40
|
+
require(coriolis != 0, "coriolis must be nonzero")
|
|
41
|
+
return 2 * np.pi / np.abs(coriolis)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@safe_arithmetic
|
|
45
|
+
def simulate_inertial_current(coriolis, time, *, east_speed=1.0, north_speed=0.0, damping=0.0):
|
|
46
|
+
"""Integrate rotating, optionally damped horizontal ocean currents.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
coriolis : float
|
|
51
|
+
Finite nonzero scalar Coriolis parameter [1/s], signed by hemisphere.
|
|
52
|
+
time : array_like
|
|
53
|
+
Nonnegative strictly increasing sampled times [s].
|
|
54
|
+
east_speed, north_speed : float
|
|
55
|
+
Initial signed horizontal velocity components [m/s].
|
|
56
|
+
damping : float
|
|
57
|
+
Nonnegative scalar linear decay rate [1/s].
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
TimeSeries
|
|
62
|
+
Eastward and northward velocity [m/s].
|
|
63
|
+
|
|
64
|
+
Notes
|
|
65
|
+
-----
|
|
66
|
+
du/dt = f v - r u; dv/dt = -f u - r v. With no damping, speed is
|
|
67
|
+
conserved and northern-hemisphere motion rotates clockwise. Pressure
|
|
68
|
+
gradients and wind forcing are omitted.
|
|
69
|
+
"""
|
|
70
|
+
from earthsciencekit._validation import time_grid
|
|
71
|
+
from earthsciencekit.integrators import integrate_ode
|
|
72
|
+
|
|
73
|
+
coriolis = quantity("coriolis", coriolis)
|
|
74
|
+
damping = quantity("damping", damping, lower=0)
|
|
75
|
+
east_speed = quantity("east_speed", east_speed)
|
|
76
|
+
north_speed = quantity("north_speed", north_speed)
|
|
77
|
+
require(all(v.ndim == 0 for v in (coriolis, damping, east_speed, north_speed)), "current parameters must be scalars")
|
|
78
|
+
require(coriolis != 0, "coriolis must be nonzero")
|
|
79
|
+
time = time_grid(time)
|
|
80
|
+
return integrate_ode(
|
|
81
|
+
lambda t, y: [coriolis * y[1] - damping * y[0], -coriolis * y[0] - damping * y[1]],
|
|
82
|
+
[east_speed, north_speed],
|
|
83
|
+
time,
|
|
84
|
+
names=("east_velocity", "north_velocity"),
|
|
85
|
+
units=("m/s", "m/s"),
|
|
86
|
+
)
|