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,402 @@
1
+ """Climate response: feedbacks, heat capacity, transient and stochastic one-box response, ice-albedo equilibria and the Gregory regression.
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 SIGMA_SB
10
+
11
+ __all__ = [
12
+ "planck_feedback",
13
+ "equilibrium_warming",
14
+ "heat_capacity_column",
15
+ "response_timescale",
16
+ "step_temperature",
17
+ "heat_uptake",
18
+ "ice_albedo",
19
+ "equilibrium_solar_constant",
20
+ "red_noise_spectrum",
21
+ "gregory_regression",
22
+ "simulate_one_box",
23
+ ]
24
+
25
+
26
+ @model
27
+ def planck_feedback(temperature, emissivity):
28
+ """Blackbody restoring derivative.
29
+
30
+ The temperature derivative of emission supplies a radiative restoring coefficient.
31
+
32
+ Parameters
33
+ ----------
34
+ temperature : float or array_like
35
+ K; constraint: positive.
36
+ emissivity : float or array_like
37
+ 1; constraint: open fraction.
38
+
39
+ Returns
40
+ -------
41
+ numpy scalar or ndarray
42
+ Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
43
+
44
+ Notes
45
+ -----
46
+ Positive restoring magnitude in W per square metre per kelvin; other feedbacks are separate.
47
+ Equation: ``4 * emissivity * SIGMA_SB * temperature**3``.
48
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
49
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
50
+ """
51
+ temperature = quantity("temperature", temperature, lower=0, strict_lower=True)
52
+ emissivity = quantity("emissivity", emissivity, lower=0, upper=1, strict_lower=True)
53
+ return 4 * emissivity * SIGMA_SB * temperature**3
54
+
55
+
56
+ @model
57
+ def equilibrium_warming(forcing, restoring):
58
+ """Linear climate response.
59
+
60
+ A restoring feedback maps steady forcing to equilibrium temperature change.
61
+
62
+ Parameters
63
+ ----------
64
+ forcing : float or array_like
65
+ W/m^2; constraint: real.
66
+ restoring : float or array_like
67
+ W/(m^2 K); constraint: positive.
68
+
69
+ Returns
70
+ -------
71
+ numpy scalar or ndarray
72
+ Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
73
+
74
+ Notes
75
+ -----
76
+ Stable linear response with a positive net restoring coefficient.
77
+ Equation: ``forcing / restoring``.
78
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
79
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
80
+ """
81
+ forcing = quantity("forcing", forcing)
82
+ restoring = quantity("restoring", restoring, lower=0, strict_lower=True)
83
+ return forcing / restoring
84
+
85
+
86
+ @model
87
+ def heat_capacity_column(density, heat_capacity, depth):
88
+ """Mixed-layer areal heat capacity.
89
+
90
+ Mixed-layer depth determines how much energy is needed for warming.
91
+
92
+ Parameters
93
+ ----------
94
+ density : float or array_like
95
+ kg/m^3; constraint: positive.
96
+ heat_capacity : float or array_like
97
+ J/(kg K); constraint: positive.
98
+ depth : float or array_like
99
+ m; constraint: positive.
100
+
101
+ Returns
102
+ -------
103
+ numpy scalar or ndarray
104
+ Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
105
+
106
+ Notes
107
+ -----
108
+ Uniform slab with constant material properties.
109
+ Equation: ``density * heat_capacity * depth``.
110
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
111
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
112
+ """
113
+ density = quantity("density", density, lower=0, strict_lower=True)
114
+ heat_capacity = quantity("heat_capacity", heat_capacity, lower=0, strict_lower=True)
115
+ depth = quantity("depth", depth, lower=0, strict_lower=True)
116
+ return density * heat_capacity * depth
117
+
118
+
119
+ @model
120
+ def response_timescale(areal_heat_capacity, restoring):
121
+ """Climate relaxation time.
122
+
123
+ Heat storage delays the equilibrium response to forcing.
124
+
125
+ Parameters
126
+ ----------
127
+ areal_heat_capacity : float or array_like
128
+ J/(m^2 K); constraint: positive.
129
+ restoring : float or array_like
130
+ W/(m^2 K); constraint: positive.
131
+
132
+ Returns
133
+ -------
134
+ numpy scalar or ndarray
135
+ Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
136
+
137
+ Notes
138
+ -----
139
+ One-box linear model; output is seconds.
140
+ Equation: ``areal_heat_capacity / restoring``.
141
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
142
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
143
+ """
144
+ areal_heat_capacity = quantity("areal_heat_capacity", areal_heat_capacity, lower=0, strict_lower=True)
145
+ restoring = quantity("restoring", restoring, lower=0, strict_lower=True)
146
+ return areal_heat_capacity / restoring
147
+
148
+
149
+ @model
150
+ def step_temperature(forcing, restoring, areal_heat_capacity, time):
151
+ """Transient one-box warming.
152
+
153
+ A step forcing exposes the lag between forcing and temperature adjustment.
154
+
155
+ Parameters
156
+ ----------
157
+ forcing : float or array_like
158
+ W/m^2; constraint: real.
159
+ restoring : float or array_like
160
+ W/(m^2 K); constraint: positive.
161
+ areal_heat_capacity : float or array_like
162
+ J/(m^2 K); constraint: positive.
163
+ time : float or array_like
164
+ s; constraint: nonnegative.
165
+
166
+ Returns
167
+ -------
168
+ numpy scalar or ndarray
169
+ Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
170
+
171
+ Notes
172
+ -----
173
+ Zero initial anomaly and constant parameters; no deep-ocean exchange.
174
+ Equation: ``forcing / restoring * (-np.expm1(-restoring * time / areal_heat_capacity))``.
175
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
176
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
177
+ """
178
+ forcing = quantity("forcing", forcing)
179
+ restoring = quantity("restoring", restoring, lower=0, strict_lower=True)
180
+ areal_heat_capacity = quantity("areal_heat_capacity", areal_heat_capacity, lower=0, strict_lower=True)
181
+ time = quantity("time", time, lower=0)
182
+ return forcing / restoring * (-np.expm1(-restoring * time / areal_heat_capacity))
183
+
184
+
185
+ @model
186
+ def heat_uptake(areal_heat_capacity, temperature_change):
187
+ """Areal heat-content change.
188
+
189
+ Energy accounting connects warming to ocean heat storage.
190
+
191
+ Parameters
192
+ ----------
193
+ areal_heat_capacity : float or array_like
194
+ J/(m^2 K); constraint: positive.
195
+ temperature_change : float or array_like
196
+ K; constraint: real.
197
+
198
+ Returns
199
+ -------
200
+ numpy scalar or ndarray
201
+ Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
202
+
203
+ Notes
204
+ -----
205
+ One uniform layer; temperature_change is an anomaly.
206
+ Equation: ``areal_heat_capacity * temperature_change``.
207
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
208
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
209
+ """
210
+ areal_heat_capacity = quantity("areal_heat_capacity", areal_heat_capacity, lower=0, strict_lower=True)
211
+ temperature_change = quantity("temperature_change", temperature_change)
212
+ return areal_heat_capacity * temperature_change
213
+
214
+
215
+ @model
216
+ def ice_albedo(temperature, cold_albedo, warm_albedo, transition_temperature, width):
217
+ """Smooth illustrative ice-albedo transition.
218
+
219
+ Temperature-dependent reflection illustrates ice-albedo feedback.
220
+
221
+ Parameters
222
+ ----------
223
+ temperature : float or array_like
224
+ K; constraint: positive.
225
+ cold_albedo : float or array_like
226
+ 1; constraint: fraction.
227
+ warm_albedo : float or array_like
228
+ 1; constraint: fraction.
229
+ transition_temperature : float or array_like
230
+ K; constraint: positive.
231
+ width : float or array_like
232
+ K; constraint: positive.
233
+
234
+ Returns
235
+ -------
236
+ numpy scalar or ndarray
237
+ Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
238
+
239
+ Notes
240
+ -----
241
+ Illustrative smooth closure, not a calibrated satellite albedo retrieval.
242
+ Equation: ``warm_albedo + (cold_albedo - warm_albedo) * (1 - np.tanh((temperature - transition_temperature) / width)) / 2``.
243
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
244
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
245
+ """
246
+ temperature = quantity("temperature", temperature, lower=0, strict_lower=True)
247
+ cold_albedo = quantity("cold_albedo", cold_albedo, lower=0, upper=1)
248
+ warm_albedo = quantity("warm_albedo", warm_albedo, lower=0, upper=1)
249
+ transition_temperature = quantity("transition_temperature", transition_temperature, lower=0, strict_lower=True)
250
+ width = quantity("width", width, lower=0, strict_lower=True)
251
+ require(cold_albedo >= warm_albedo, "cold albedo must be at least warm albedo")
252
+ return warm_albedo + (cold_albedo - warm_albedo) * (1 - np.tanh((temperature - transition_temperature) / width)) / 2
253
+
254
+
255
+ @model
256
+ def equilibrium_solar_constant(temperature, emissivity, cold_albedo, warm_albedo, transition_temperature, width):
257
+ """Solar constant that holds a planet with ice-albedo feedback at a given temperature.
258
+
259
+ Inverting the energy balance traces every equilibrium, including the unstable branch between a warm and an ice-covered state.
260
+
261
+ Parameters
262
+ ----------
263
+ temperature : float or array_like
264
+ K; constraint: positive.
265
+ emissivity : float or array_like
266
+ 1; constraint: open fraction.
267
+ cold_albedo : float or array_like
268
+ 1; constraint: fraction below one.
269
+ warm_albedo : float or array_like
270
+ 1; constraint: fraction.
271
+ transition_temperature : float or array_like
272
+ K; constraint: positive.
273
+ width : float or array_like
274
+ K; constraint: positive.
275
+
276
+ Returns
277
+ -------
278
+ numpy scalar or ndarray
279
+ Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
280
+
281
+ Notes
282
+ -----
283
+ Zero-dimensional greybody balance with the ice_albedo closure; equilibria with a negative slope dS/dT are unstable.
284
+ Equation: ``4 * emissivity * SIGMA_SB * temperature**4 / (1 - albedo)``, with ``albedo`` from ice_albedo.
285
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
286
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
287
+ """
288
+ emissivity = quantity("emissivity", emissivity, lower=0, upper=1, strict_lower=True)
289
+ require(quantity("cold_albedo", cold_albedo) < 1, "cold albedo must be below one")
290
+ albedo = ice_albedo(temperature, cold_albedo, warm_albedo, transition_temperature, width)
291
+ return 4 * emissivity * SIGMA_SB * quantity("temperature", temperature) ** 4 / (1 - albedo)
292
+
293
+
294
+ @model
295
+ def red_noise_spectrum(frequency, forcing_spectrum, restoring, areal_heat_capacity):
296
+ """Temperature spectrum of a one-box climate driven by white noise.
297
+
298
+ Heat capacity integrates random weather forcing into slow red-noise climate variability.
299
+
300
+ Parameters
301
+ ----------
302
+ frequency : float or array_like
303
+ Hz; constraint: real.
304
+ forcing_spectrum : float or array_like
305
+ W^2/(m^4 Hz), two-sided; constraint: nonnegative.
306
+ restoring : float or array_like
307
+ W/(m^2 K); constraint: positive.
308
+ areal_heat_capacity : float or array_like
309
+ J/(m^2 K); constraint: positive.
310
+
311
+ Returns
312
+ -------
313
+ numpy scalar or ndarray
314
+ Model value [K^2/Hz], two-sided. Scalar input returns a NumPy scalar; arrays broadcast.
315
+
316
+ Notes
317
+ -----
318
+ Linear C dT/dt = F - lambda T with white-noise F; integrating over all frequencies gives the
319
+ variance forcing_spectrum / (2 * restoring * areal_heat_capacity).
320
+ Equation: ``forcing_spectrum / (restoring**2 + (2 * np.pi * frequency * areal_heat_capacity)**2)``.
321
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
322
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
323
+ """
324
+ frequency = quantity("frequency", frequency)
325
+ forcing_spectrum = quantity("forcing_spectrum", forcing_spectrum, lower=0)
326
+ restoring = quantity("restoring", restoring, lower=0, strict_lower=True)
327
+ areal_heat_capacity = quantity("areal_heat_capacity", areal_heat_capacity, lower=0, strict_lower=True)
328
+ return forcing_spectrum / (restoring**2 + (2 * np.pi * frequency * areal_heat_capacity) ** 2)
329
+
330
+
331
+ @model
332
+ def gregory_regression(temperature_change, imbalance):
333
+ """Forcing and feedback from a regression of imbalance on warming.
334
+
335
+ In a run with constant forcing, the top-of-atmosphere imbalance falls linearly with warming, so a straight-line fit gives both.
336
+
337
+ Parameters
338
+ ----------
339
+ temperature_change : array_like, shape (n,), n >= 2
340
+ K; constraint: real, not all equal.
341
+ imbalance : array_like, shape (n,)
342
+ W/m^2, net downward top-of-atmosphere flux; constraint: real.
343
+
344
+ Returns
345
+ -------
346
+ numpy.ndarray, shape (2,)
347
+ ``[forcing, restoring]``: the intercept [W/m^2] and minus the slope [W/(m^2 K)] of the least-squares line.
348
+
349
+ Notes
350
+ -----
351
+ Assumes N = F - lambda T with constant F and lambda; equilibrium warming is forcing / restoring.
352
+ Equation: ``imbalance = forcing - restoring * temperature_change`` (ordinary least squares).
353
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
354
+ """
355
+ temperature_change = quantity("temperature_change", temperature_change)
356
+ imbalance = quantity("imbalance", imbalance)
357
+ require(temperature_change.ndim == 1 and temperature_change.size >= 2, "temperature_change must be a vector of at least two samples")
358
+ require(imbalance.shape == temperature_change.shape, "imbalance must match temperature_change")
359
+ anomaly = temperature_change - temperature_change.mean()
360
+ require(np.any(anomaly != 0), "temperature_change must vary")
361
+ slope = np.dot(anomaly, imbalance - imbalance.mean()) / np.dot(anomaly, anomaly)
362
+ return np.array([imbalance.mean() - slope * temperature_change.mean(), -slope])
363
+
364
+
365
+ @safe_arithmetic
366
+ def simulate_one_box(forcing, restoring, areal_heat_capacity, time, *, initial_anomaly=0.0):
367
+ """Integrate a linear one-box climate energy balance.
368
+
369
+ Parameters
370
+ ----------
371
+ forcing : float
372
+ Constant signed radiative perturbation [W/m^2].
373
+ restoring : float
374
+ Positive scalar net restoring coefficient [W/(m^2 K)].
375
+ areal_heat_capacity : float
376
+ Positive scalar effective heat capacity [J/(m^2 K)].
377
+ time : array_like
378
+ Nonnegative strictly increasing sampled times [s].
379
+ initial_anomaly : float
380
+ Finite scalar temperature anomaly [K] at the first supplied time.
381
+
382
+ Returns
383
+ -------
384
+ TimeSeries
385
+ Temperature anomaly [K].
386
+
387
+ Notes
388
+ -----
389
+ C dT/dt = F - lambda T. Compare with step_temperature for zero initial
390
+ anomaly, using elapsed time since the first sample. This educational model
391
+ omits deep-ocean exchange, nonlinear feedbacks and geographic structure.
392
+ """
393
+ from earthsciencekit._validation import time_grid
394
+ from earthsciencekit.integrators import integrate_ode
395
+
396
+ forcing = quantity("forcing", forcing)
397
+ restoring = quantity("restoring", restoring, lower=0, strict_lower=True)
398
+ capacity = quantity("areal_heat_capacity", areal_heat_capacity, lower=0, strict_lower=True)
399
+ initial = quantity("initial_anomaly", initial_anomaly)
400
+ require(all(v.ndim == 0 for v in (forcing, restoring, capacity, initial)), "climate parameters must be scalars")
401
+ time = time_grid(time)
402
+ return integrate_ode(lambda t, y: (forcing - restoring * y) / capacity, [initial], time, names=("temperature_anomaly",), units=("K",))
@@ -0,0 +1,77 @@
1
+ """Sea level: thermal expansion and the mass equivalent of added water.
2
+
3
+ SI units unless stated otherwise.
4
+ """
5
+
6
+ from earthsciencekit._validation import model, quantity
7
+
8
+ __all__ = [
9
+ "thermal_expansion_sea_level",
10
+ "water_equivalent_sea_level",
11
+ ]
12
+
13
+
14
+ @model
15
+ def thermal_expansion_sea_level(expansion_coefficient, depth, temperature_change):
16
+ """Steric sea-level slab approximation.
17
+
18
+ Ocean expansion connects heat uptake to a sea-level contribution.
19
+
20
+ Parameters
21
+ ----------
22
+ expansion_coefficient : float or array_like
23
+ 1/K; constraint: positive.
24
+ depth : float or array_like
25
+ m; constraint: positive.
26
+ temperature_change : float or array_like
27
+ K; constraint: real.
28
+
29
+ Returns
30
+ -------
31
+ numpy scalar or ndarray
32
+ Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
33
+
34
+ Notes
35
+ -----
36
+ Constant coefficient and uniform warming; salinity and pressure dependence are omitted.
37
+ Equation: ``expansion_coefficient * depth * temperature_change``.
38
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
39
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
40
+ """
41
+ expansion_coefficient = quantity("expansion_coefficient", expansion_coefficient, lower=0, strict_lower=True)
42
+ depth = quantity("depth", depth, lower=0, strict_lower=True)
43
+ temperature_change = quantity("temperature_change", temperature_change)
44
+ return expansion_coefficient * depth * temperature_change
45
+
46
+
47
+ @model
48
+ def water_equivalent_sea_level(water_mass, ocean_area, water_density):
49
+ """Mass-equivalent sea-level contribution.
50
+
51
+ Added ocean water changes mean level through mass conservation.
52
+
53
+ Parameters
54
+ ----------
55
+ water_mass : float or array_like
56
+ kg; constraint: nonnegative.
57
+ ocean_area : float or array_like
58
+ m^2; constraint: positive.
59
+ water_density : float or array_like
60
+ kg/m^3; constraint: positive.
61
+
62
+ Returns
63
+ -------
64
+ numpy scalar or ndarray
65
+ Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
66
+
67
+ Notes
68
+ -----
69
+ Uniform ocean area and density; gravitational fingerprints and land motion are omitted.
70
+ Equation: ``water_mass / (ocean_area * water_density)``.
71
+ Invalid, empty, nonreal or nonfinite inputs raise ValueError.
72
+ NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
73
+ """
74
+ water_mass = quantity("water_mass", water_mass, lower=0)
75
+ ocean_area = quantity("ocean_area", ocean_area, lower=0, strict_lower=True)
76
+ water_density = quantity("water_density", water_density, lower=0, strict_lower=True)
77
+ return water_mass / (ocean_area * water_density)
File without changes