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,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
+ )