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,6 @@
|
|
|
1
|
+
"""Educational Earth-system computations with explicit units and model limits."""
|
|
2
|
+
|
|
3
|
+
from . import atmosphere, climate, geophysics, oceanography, plate_tectonics, seismology, volcanology
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.0"
|
|
6
|
+
__all__ = ["plate_tectonics", "seismology", "atmosphere", "climate", "oceanography", "volcanology", "geophysics"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Central contracts for real, finite, nonempty numerical quantities."""
|
|
2
|
+
|
|
3
|
+
from functools import wraps
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def quantity(name, value, *, lower=None, upper=None, strict_lower=False, strict_upper=False):
|
|
9
|
+
"""Return a floating NumPy quantity after validating physical bounds."""
|
|
10
|
+
array = np.asarray(value)
|
|
11
|
+
if array.dtype.kind not in "iuf" or not array.size or not np.all(np.isfinite(array)):
|
|
12
|
+
raise ValueError(f"{name} must contain finite real numbers, excluding booleans")
|
|
13
|
+
array = array.astype(float)
|
|
14
|
+
if lower is not None:
|
|
15
|
+
require(array > lower if strict_lower else array >= lower, f"{name} violates its lower bound")
|
|
16
|
+
if upper is not None:
|
|
17
|
+
require(array < upper if strict_upper else array <= upper, f"{name} violates its upper bound")
|
|
18
|
+
return array
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def require(condition, message):
|
|
22
|
+
"""Reject any violated relation, including array-valued physical constraints."""
|
|
23
|
+
if not np.all(condition):
|
|
24
|
+
raise ValueError(message)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def model(function):
|
|
28
|
+
"""Preserve signatures and turn arithmetic singularities into ValueError."""
|
|
29
|
+
|
|
30
|
+
@wraps(function)
|
|
31
|
+
def wrapped(*args, **kwargs):
|
|
32
|
+
try:
|
|
33
|
+
with np.errstate(over="raise", divide="raise", invalid="raise"):
|
|
34
|
+
result = function(*args, **kwargs)
|
|
35
|
+
except FloatingPointError as error:
|
|
36
|
+
raise ValueError(f"{function.__name__}: singular or overflowing arithmetic") from error
|
|
37
|
+
require(np.all(np.isfinite(result)), f"{function.__name__}: nonfinite result")
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
return wrapped
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def time_grid(value):
|
|
44
|
+
"""Validate a nonnegative strictly increasing vector of sampled seconds."""
|
|
45
|
+
time = quantity("time", value, lower=0)
|
|
46
|
+
require(time.ndim == 1 and time.size >= 2, "time must be a vector with at least two samples")
|
|
47
|
+
require(np.all(np.diff(time) > 0), "time must strictly increase")
|
|
48
|
+
return time
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def safe_arithmetic(function):
|
|
52
|
+
"""Translate floating arithmetic failures in structured numerical models."""
|
|
53
|
+
|
|
54
|
+
@wraps(function)
|
|
55
|
+
def wrapped(*args, **kwargs):
|
|
56
|
+
try:
|
|
57
|
+
with np.errstate(over="raise", divide="raise", invalid="raise"):
|
|
58
|
+
return function(*args, **kwargs)
|
|
59
|
+
except FloatingPointError as error:
|
|
60
|
+
raise ValueError(f"{function.__name__}: singular or overflowing arithmetic") from error
|
|
61
|
+
|
|
62
|
+
return wrapped
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Inspectable atmosphere models; SI units unless stated otherwise."""
|
|
2
|
+
|
|
3
|
+
from earthsciencekit.atmosphere.systems.dynamics import (
|
|
4
|
+
coriolis_parameter,
|
|
5
|
+
geostrophic_speed,
|
|
6
|
+
rossby_number,
|
|
7
|
+
rossby_wave_speed,
|
|
8
|
+
simulate_barotropic_vorticity,
|
|
9
|
+
)
|
|
10
|
+
from earthsciencekit.atmosphere.systems.hydrostatics import (
|
|
11
|
+
barometer_column_height,
|
|
12
|
+
barometric_pressure,
|
|
13
|
+
dry_air_density,
|
|
14
|
+
scale_height,
|
|
15
|
+
surface_pressure_tendency,
|
|
16
|
+
two_layer_temperature,
|
|
17
|
+
)
|
|
18
|
+
from earthsciencekit.atmosphere.systems.moisture import (
|
|
19
|
+
mixing_ratio,
|
|
20
|
+
relative_humidity,
|
|
21
|
+
saturation_vapor_pressure,
|
|
22
|
+
specific_humidity,
|
|
23
|
+
virtual_temperature,
|
|
24
|
+
)
|
|
25
|
+
from earthsciencekit.atmosphere.systems.thermodynamics import (
|
|
26
|
+
brunt_vaisala_squared,
|
|
27
|
+
dry_adiabatic_lapse_rate,
|
|
28
|
+
potential_temperature,
|
|
29
|
+
richardson_number,
|
|
30
|
+
simulate_dry_ascent,
|
|
31
|
+
temperature_from_potential,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"dry_air_density",
|
|
36
|
+
"scale_height",
|
|
37
|
+
"barometric_pressure",
|
|
38
|
+
"potential_temperature",
|
|
39
|
+
"temperature_from_potential",
|
|
40
|
+
"dry_adiabatic_lapse_rate",
|
|
41
|
+
"saturation_vapor_pressure",
|
|
42
|
+
"mixing_ratio",
|
|
43
|
+
"specific_humidity",
|
|
44
|
+
"virtual_temperature",
|
|
45
|
+
"relative_humidity",
|
|
46
|
+
"coriolis_parameter",
|
|
47
|
+
"brunt_vaisala_squared",
|
|
48
|
+
"geostrophic_speed",
|
|
49
|
+
"rossby_number",
|
|
50
|
+
"barometer_column_height",
|
|
51
|
+
"two_layer_temperature",
|
|
52
|
+
"richardson_number",
|
|
53
|
+
"surface_pressure_tendency",
|
|
54
|
+
"rossby_wave_speed",
|
|
55
|
+
"simulate_dry_ascent",
|
|
56
|
+
"simulate_barotropic_vorticity",
|
|
57
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Result containers for earthsciencekit.atmosphere.
|
|
2
|
+
|
|
3
|
+
Algebraic models return NumPy scalars or arrays. The sampled dynamic models
|
|
4
|
+
return the package-wide immutable :class:`~earthsciencekit.results.TimeSeries`,
|
|
5
|
+
which is re-exported here as this domain's result type.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from earthsciencekit.results import TimeSeries
|
|
9
|
+
|
|
10
|
+
__all__ = ["TimeSeries"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Models of earthsciencekit.atmosphere, grouped by topic."""
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Rotating dynamics: the Coriolis parameter, geostrophy, the Rossby number, Rossby waves and barotropic vorticity.
|
|
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 OMEGA_EARTH
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"coriolis_parameter",
|
|
13
|
+
"geostrophic_speed",
|
|
14
|
+
"rossby_number",
|
|
15
|
+
"rossby_wave_speed",
|
|
16
|
+
"simulate_barotropic_vorticity",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@model
|
|
21
|
+
def coriolis_parameter(latitude):
|
|
22
|
+
"""Rotating-Earth Coriolis parameter.
|
|
23
|
+
|
|
24
|
+
Latitude controls rotational deflection and geophysical balance.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
latitude : float or array_like
|
|
29
|
+
rad; constraint: latitude.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
numpy scalar or ndarray
|
|
34
|
+
Model value [kg/m^3]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
35
|
+
|
|
36
|
+
Notes
|
|
37
|
+
-----
|
|
38
|
+
Spherical rotating-Earth approximation; input is radians.
|
|
39
|
+
Equation: ``2 * OMEGA_EARTH * np.sin(latitude)``.
|
|
40
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
41
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
42
|
+
"""
|
|
43
|
+
latitude = quantity("latitude", latitude, lower=-np.pi / 2, upper=np.pi / 2)
|
|
44
|
+
return 2 * OMEGA_EARTH * np.sin(latitude)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@model
|
|
48
|
+
def geostrophic_speed(pressure_gradient, density, coriolis):
|
|
49
|
+
"""Pressure-gradient geostrophic component.
|
|
50
|
+
|
|
51
|
+
Pressure gradients and rotation can balance in large-scale flow.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
pressure_gradient : float or array_like
|
|
56
|
+
Pa/m; constraint: real.
|
|
57
|
+
density : float or array_like
|
|
58
|
+
kg/m^3; constraint: positive.
|
|
59
|
+
coriolis : float or array_like
|
|
60
|
+
1/s; constraint: nonzero.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
numpy scalar or ndarray
|
|
65
|
+
Model value [kg/m^3]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
66
|
+
|
|
67
|
+
Notes
|
|
68
|
+
-----
|
|
69
|
+
Signed cross-gradient component; invalid at the equator where coriolis vanishes.
|
|
70
|
+
Equation: ``pressure_gradient / (density * coriolis)``.
|
|
71
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
72
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
73
|
+
"""
|
|
74
|
+
pressure_gradient = quantity("pressure_gradient", pressure_gradient)
|
|
75
|
+
density = quantity("density", density, lower=0, strict_lower=True)
|
|
76
|
+
coriolis = quantity("coriolis", coriolis)
|
|
77
|
+
require(coriolis != 0, "coriolis must be nonzero")
|
|
78
|
+
return pressure_gradient / (density * coriolis)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@model
|
|
82
|
+
def rossby_number(speed, length, coriolis):
|
|
83
|
+
"""Rossby dynamical scaling.
|
|
84
|
+
|
|
85
|
+
Rossby number measures inertia relative to rotational effects.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
speed : float or array_like
|
|
90
|
+
m/s; constraint: nonnegative.
|
|
91
|
+
length : float or array_like
|
|
92
|
+
m; constraint: positive.
|
|
93
|
+
coriolis : float or array_like
|
|
94
|
+
1/s; constraint: nonzero.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
numpy scalar or ndarray
|
|
99
|
+
Model value [kg/m^3]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
100
|
+
|
|
101
|
+
Notes
|
|
102
|
+
-----
|
|
103
|
+
Magnitude scaling only; small values support but do not prove geostrophic balance.
|
|
104
|
+
Equation: ``speed / (np.abs(coriolis) * length)``.
|
|
105
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
106
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
107
|
+
"""
|
|
108
|
+
speed = quantity("speed", speed, lower=0)
|
|
109
|
+
length = quantity("length", length, lower=0, strict_lower=True)
|
|
110
|
+
coriolis = quantity("coriolis", coriolis)
|
|
111
|
+
require(coriolis != 0, "coriolis must be nonzero")
|
|
112
|
+
return speed / (np.abs(coriolis) * length)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@model
|
|
116
|
+
def rossby_wave_speed(zonal_wind, beta, wavenumber):
|
|
117
|
+
"""Barotropic Rossby-wave zonal phase speed.
|
|
118
|
+
|
|
119
|
+
The latitude gradient of the Coriolis parameter makes vorticity waves drift westward.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
zonal_wind : float or array_like
|
|
124
|
+
m/s; constraint: real. Uniform background eastward wind.
|
|
125
|
+
beta : float or array_like
|
|
126
|
+
1/(m s); constraint: nonnegative. Northward gradient of the Coriolis parameter.
|
|
127
|
+
wavenumber : float or array_like
|
|
128
|
+
rad/m; constraint: positive. Total horizontal wavenumber.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
numpy scalar or ndarray
|
|
133
|
+
Model value [m/s]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
134
|
+
|
|
135
|
+
Notes
|
|
136
|
+
-----
|
|
137
|
+
Nondivergent barotropic flow on a beta plane; positive values are eastward.
|
|
138
|
+
Equation: ``zonal_wind - beta / wavenumber**2``.
|
|
139
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
140
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
141
|
+
"""
|
|
142
|
+
zonal_wind = quantity("zonal_wind", zonal_wind)
|
|
143
|
+
beta = quantity("beta", beta, lower=0)
|
|
144
|
+
wavenumber = quantity("wavenumber", wavenumber, lower=0, strict_lower=True)
|
|
145
|
+
return zonal_wind - beta / wavenumber**2
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@safe_arithmetic
|
|
149
|
+
def simulate_barotropic_vorticity(initial_vorticity, time, beta, spacing):
|
|
150
|
+
"""Integrate the barotropic vorticity equation on a periodic beta plane.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
initial_vorticity : array_like, shape (ny, nx), ny, nx >= 4
|
|
155
|
+
Relative vorticity [1/s] on an equally spaced doubly periodic grid,
|
|
156
|
+
rows northward and columns eastward.
|
|
157
|
+
time : array_like
|
|
158
|
+
Strictly increasing nonnegative output times [s]. Initial state belongs
|
|
159
|
+
to the first supplied time; values need not start at zero.
|
|
160
|
+
beta : float
|
|
161
|
+
Nonnegative scalar northward gradient of the Coriolis parameter [1/(m s)].
|
|
162
|
+
spacing : float
|
|
163
|
+
Positive scalar grid spacing [m], the same in both directions.
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
TimeSeries
|
|
168
|
+
Relative vorticity at each grid point [1/s], flattened row by row.
|
|
169
|
+
Columns are labeled cell_<row>_<column>.
|
|
170
|
+
|
|
171
|
+
Notes
|
|
172
|
+
-----
|
|
173
|
+
Solves ``d(zeta)/dt + J(psi, zeta) + beta * d(psi)/dx = 0`` with
|
|
174
|
+
``laplacian(psi) = zeta``. Derivatives are pseudo-spectral (FFT) with the
|
|
175
|
+
two-thirds dealiasing rule, and ``integrators.integrate_ode`` steps in time.
|
|
176
|
+
The domain mean of vorticity is removed and conserved; energy and enstrophy
|
|
177
|
+
are conserved up to the time-stepping error. A single Fourier mode is an
|
|
178
|
+
exact Rossby wave. There is no forcing, friction, divergence or sphere.
|
|
179
|
+
"""
|
|
180
|
+
from earthsciencekit._validation import time_grid
|
|
181
|
+
from earthsciencekit.integrators import integrate_ode
|
|
182
|
+
|
|
183
|
+
zeta = quantity("initial_vorticity", initial_vorticity)
|
|
184
|
+
require(zeta.ndim == 2 and min(zeta.shape) >= 4, "initial_vorticity must be a grid of at least 4 x 4 cells")
|
|
185
|
+
time = time_grid(time)
|
|
186
|
+
beta = quantity("beta", beta, lower=0)
|
|
187
|
+
spacing = quantity("spacing", spacing, lower=0, strict_lower=True)
|
|
188
|
+
require(beta.ndim == spacing.ndim == 0, "beta and spacing must be scalars")
|
|
189
|
+
shape = zeta.shape
|
|
190
|
+
ky = 2 * np.pi * np.fft.fftfreq(shape[0], spacing)[:, None]
|
|
191
|
+
kx = 2 * np.pi * np.fft.rfftfreq(shape[1], spacing)[None, :]
|
|
192
|
+
k2 = kx**2 + ky**2
|
|
193
|
+
inverse = np.divide(-1, k2, out=np.zeros_like(k2), where=k2 > 0)
|
|
194
|
+
keep = (np.abs(ky) < 2 / 3 * np.abs(ky).max()) & (np.abs(kx) < 2 / 3 * kx.max())
|
|
195
|
+
|
|
196
|
+
def rhs(t, state):
|
|
197
|
+
spectrum = np.fft.rfft2(state.reshape(shape)) * keep
|
|
198
|
+
psi = spectrum * inverse
|
|
199
|
+
u = np.fft.irfft2(-1j * ky * psi, s=shape)
|
|
200
|
+
v = np.fft.irfft2(1j * kx * psi, s=shape)
|
|
201
|
+
zeta_x = np.fft.irfft2(1j * kx * spectrum, s=shape)
|
|
202
|
+
zeta_y = np.fft.irfft2(1j * ky * spectrum, s=shape)
|
|
203
|
+
advection = np.fft.rfft2(u * zeta_x + v * zeta_y) * keep
|
|
204
|
+
return np.fft.irfft2(-advection - beta * 1j * kx * psi, s=shape).ravel()
|
|
205
|
+
|
|
206
|
+
initial = np.fft.irfft2(np.fft.rfft2(zeta) * keep * (k2 > 0), s=shape).ravel()
|
|
207
|
+
names = tuple(f"cell_{i}_{j}" for i in range(shape[0]) for j in range(shape[1]))
|
|
208
|
+
return integrate_ode(rhs, initial, time, names=names, units=("1/s",) * initial.size)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Hydrostatics: air density, scale height, barometric pressure, the barometer, a layered profile and pressure tendency.
|
|
2
|
+
|
|
3
|
+
SI units unless stated otherwise.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from earthsciencekit._validation import model, quantity, require
|
|
9
|
+
from earthsciencekit.constants import R_DRY_AIR
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"dry_air_density",
|
|
13
|
+
"scale_height",
|
|
14
|
+
"barometric_pressure",
|
|
15
|
+
"barometer_column_height",
|
|
16
|
+
"two_layer_temperature",
|
|
17
|
+
"surface_pressure_tendency",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@model
|
|
22
|
+
def dry_air_density(pressure, temperature):
|
|
23
|
+
"""Ideal-gas atmospheric density.
|
|
24
|
+
|
|
25
|
+
An equation of state connects atmospheric pressure, temperature and density.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
pressure : float or array_like
|
|
30
|
+
Pa; constraint: positive.
|
|
31
|
+
temperature : float or array_like
|
|
32
|
+
K; constraint: positive.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
numpy scalar or ndarray
|
|
37
|
+
Model value [kg/m^3]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
38
|
+
|
|
39
|
+
Notes
|
|
40
|
+
-----
|
|
41
|
+
Dry ideal air with fixed specific gas constant.
|
|
42
|
+
Equation: ``pressure / (R_DRY_AIR * temperature)``.
|
|
43
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
44
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
45
|
+
"""
|
|
46
|
+
pressure = quantity("pressure", pressure, lower=0, strict_lower=True)
|
|
47
|
+
temperature = quantity("temperature", temperature, lower=0, strict_lower=True)
|
|
48
|
+
return pressure / (R_DRY_AIR * temperature)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@model
|
|
52
|
+
def scale_height(temperature, gravity):
|
|
53
|
+
"""Isothermal scale height.
|
|
54
|
+
|
|
55
|
+
Hydrostatic balance predicts an exponential pressure scale.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
temperature : float or array_like
|
|
60
|
+
K; constraint: positive.
|
|
61
|
+
gravity : float or array_like
|
|
62
|
+
m/s^2; constraint: positive.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
numpy scalar or ndarray
|
|
67
|
+
Model value [kg/m^3]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
68
|
+
|
|
69
|
+
Notes
|
|
70
|
+
-----
|
|
71
|
+
Dry isothermal atmosphere with constant gravity.
|
|
72
|
+
Equation: ``R_DRY_AIR * temperature / gravity``.
|
|
73
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
74
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
75
|
+
"""
|
|
76
|
+
temperature = quantity("temperature", temperature, lower=0, strict_lower=True)
|
|
77
|
+
gravity = quantity("gravity", gravity, lower=0, strict_lower=True)
|
|
78
|
+
return R_DRY_AIR * temperature / gravity
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@model
|
|
82
|
+
def barometric_pressure(surface_pressure, height, temperature, gravity):
|
|
83
|
+
"""Isothermal barometric profile.
|
|
84
|
+
|
|
85
|
+
Vertical pressure structure follows from hydrostatic balance and the gas law.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
surface_pressure : float or array_like
|
|
90
|
+
Pa; constraint: positive.
|
|
91
|
+
height : float or array_like
|
|
92
|
+
m; constraint: real.
|
|
93
|
+
temperature : float or array_like
|
|
94
|
+
K; constraint: positive.
|
|
95
|
+
gravity : float or array_like
|
|
96
|
+
m/s^2; constraint: positive.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
numpy scalar or ndarray
|
|
101
|
+
Model value [kg/m^3]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
102
|
+
|
|
103
|
+
Notes
|
|
104
|
+
-----
|
|
105
|
+
Isothermal column; signed height is relative to the reference level.
|
|
106
|
+
Equation: ``surface_pressure * np.exp(-gravity * height / (R_DRY_AIR * temperature))``.
|
|
107
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
108
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
109
|
+
"""
|
|
110
|
+
surface_pressure = quantity("surface_pressure", surface_pressure, lower=0, strict_lower=True)
|
|
111
|
+
height = quantity("height", height)
|
|
112
|
+
temperature = quantity("temperature", temperature, lower=0, strict_lower=True)
|
|
113
|
+
gravity = quantity("gravity", gravity, lower=0, strict_lower=True)
|
|
114
|
+
return surface_pressure * np.exp(-gravity * height / (R_DRY_AIR * temperature))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@model
|
|
118
|
+
def barometer_column_height(pressure, liquid_density, gravity):
|
|
119
|
+
"""Liquid barometer column height.
|
|
120
|
+
|
|
121
|
+
The weight of a liquid column balances the pressure of the air above it.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
pressure : float or array_like
|
|
126
|
+
Pa; constraint: nonnegative.
|
|
127
|
+
liquid_density : float or array_like
|
|
128
|
+
kg/m^3; constraint: positive.
|
|
129
|
+
gravity : float or array_like
|
|
130
|
+
m/s^2; constraint: positive.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
numpy scalar or ndarray
|
|
135
|
+
Model value [m]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
136
|
+
|
|
137
|
+
Notes
|
|
138
|
+
-----
|
|
139
|
+
Incompressible liquid with a vacuum above the column; vapour pressure and capillarity are neglected.
|
|
140
|
+
Equation: ``pressure / (liquid_density * gravity)``.
|
|
141
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
142
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
143
|
+
"""
|
|
144
|
+
pressure = quantity("pressure", pressure, lower=0)
|
|
145
|
+
liquid_density = quantity("liquid_density", liquid_density, lower=0, strict_lower=True)
|
|
146
|
+
gravity = quantity("gravity", gravity, lower=0, strict_lower=True)
|
|
147
|
+
return pressure / (liquid_density * gravity)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@model
|
|
151
|
+
def two_layer_temperature(height, surface_temperature, lapse_rate, tropopause_height):
|
|
152
|
+
"""Troposphere-stratosphere temperature profile.
|
|
153
|
+
|
|
154
|
+
Temperature falls with height up to the tropopause and stays constant above it.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
height : float or array_like
|
|
159
|
+
m; constraint: nonnegative.
|
|
160
|
+
surface_temperature : float or array_like
|
|
161
|
+
K; constraint: positive.
|
|
162
|
+
lapse_rate : float or array_like
|
|
163
|
+
K/m; constraint: nonnegative.
|
|
164
|
+
tropopause_height : float or array_like
|
|
165
|
+
m; constraint: positive.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
numpy scalar or ndarray
|
|
170
|
+
Model value [K]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
171
|
+
|
|
172
|
+
Notes
|
|
173
|
+
-----
|
|
174
|
+
Constant-lapse troposphere under an isothermal lower stratosphere; the stratospheric warming above about 20 km is omitted.
|
|
175
|
+
Equation: ``surface_temperature - lapse_rate * np.minimum(height, tropopause_height)``.
|
|
176
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
177
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
178
|
+
"""
|
|
179
|
+
height = quantity("height", height, lower=0)
|
|
180
|
+
surface_temperature = quantity("surface_temperature", surface_temperature, lower=0, strict_lower=True)
|
|
181
|
+
lapse_rate = quantity("lapse_rate", lapse_rate, lower=0)
|
|
182
|
+
tropopause_height = quantity("tropopause_height", tropopause_height, lower=0, strict_lower=True)
|
|
183
|
+
require(surface_temperature > lapse_rate * tropopause_height, "tropopause temperature must be positive")
|
|
184
|
+
return surface_temperature - lapse_rate * np.minimum(height, tropopause_height)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@model
|
|
188
|
+
def surface_pressure_tendency(mean_divergence, surface_pressure):
|
|
189
|
+
"""Surface pressure tendency from column divergence.
|
|
190
|
+
|
|
191
|
+
Mass leaving the column through net horizontal divergence lowers the surface pressure.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
mean_divergence : float or array_like
|
|
196
|
+
1/s; constraint: real. Pressure-weighted column mean of the horizontal divergence.
|
|
197
|
+
surface_pressure : float or array_like
|
|
198
|
+
Pa; constraint: positive.
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
numpy scalar or ndarray
|
|
203
|
+
Model value [Pa/s]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
204
|
+
|
|
205
|
+
Notes
|
|
206
|
+
-----
|
|
207
|
+
Hydrostatic column with no surface mass flux; divergence at one level is far larger than the column mean.
|
|
208
|
+
Equation: ``-surface_pressure * mean_divergence``.
|
|
209
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
210
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
211
|
+
"""
|
|
212
|
+
mean_divergence = quantity("mean_divergence", mean_divergence)
|
|
213
|
+
surface_pressure = quantity("surface_pressure", surface_pressure, lower=0, strict_lower=True)
|
|
214
|
+
return -surface_pressure * mean_divergence
|