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,81 @@
|
|
|
1
|
+
"""Physical identities, limits and dynamic checks for earthsciencekit.atmosphere."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from earthsciencekit.atmosphere.systems.dynamics import rossby_wave_speed, simulate_barotropic_vorticity
|
|
7
|
+
from earthsciencekit.atmosphere.systems.hydrostatics import barometer_column_height, surface_pressure_tendency, two_layer_temperature
|
|
8
|
+
from earthsciencekit.atmosphere.systems.thermodynamics import potential_temperature, richardson_number, simulate_dry_ascent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_dry_ascent_conserves_potential_temperature():
|
|
12
|
+
time = np.linspace(0, 1000, 51)
|
|
13
|
+
result = simulate_dry_ascent(300, 1e5, 1, time)
|
|
14
|
+
theta = potential_temperature(result.values[:, 0], result.values[:, 1], 1e5)
|
|
15
|
+
np.testing.assert_allclose(theta, 300, rtol=1e-8)
|
|
16
|
+
assert np.all(np.diff(result.values[:, 1]) < 0)
|
|
17
|
+
with pytest.raises(ValueError):
|
|
18
|
+
simulate_dry_ascent([300, 310], 1e5, 1, time)
|
|
19
|
+
with pytest.raises(ValueError):
|
|
20
|
+
simulate_dry_ascent(300, 1e5, 100, time)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _barotropic_grid(n=16, spacing=6e5):
|
|
24
|
+
x = np.arange(n) * spacing
|
|
25
|
+
return np.meshgrid(x, x), n * spacing
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_barotropic_single_mode_is_rossby_wave():
|
|
29
|
+
beta, spacing = 1.6e-11, 6e5
|
|
30
|
+
(X, Y), length = _barotropic_grid(spacing=spacing)
|
|
31
|
+
k, l = 2 * np.pi * 2 / length, 2 * np.pi / length
|
|
32
|
+
time = np.linspace(3600, 2 * 86400, 7)
|
|
33
|
+
result = simulate_barotropic_vorticity(1e-5 * np.cos(k * X + l * Y), time, beta, spacing)
|
|
34
|
+
c = rossby_wave_speed(0, beta, np.hypot(k, l))
|
|
35
|
+
expected = 1e-5 * np.cos(k * (X[None] - c * (time - time[0])[:, None, None]) + l * Y[None])
|
|
36
|
+
np.testing.assert_allclose(result.values, expected.reshape(time.size, -1), atol=1e-10)
|
|
37
|
+
assert result.names[17] == "cell_1_1" and result.units[0] == "1/s"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_barotropic_conserves_mean_energy_and_enstrophy():
|
|
41
|
+
spacing = 6e5
|
|
42
|
+
(X, Y), length = _barotropic_grid(spacing=spacing)
|
|
43
|
+
rng = np.random.default_rng(1)
|
|
44
|
+
modes = [(i, j) for i in range(1, 4) for j in range(-3, 4)]
|
|
45
|
+
zeta = 2e-6 + sum(1e-5 * rng.normal() * np.cos(2 * np.pi * (i * X + j * Y) / length + rng.uniform(0, 2 * np.pi)) for i, j in modes)
|
|
46
|
+
result = simulate_barotropic_vorticity(zeta, np.linspace(0, 86400, 5), 1.6e-11, spacing)
|
|
47
|
+
fields = result.values.reshape(-1, *X.shape)
|
|
48
|
+
np.testing.assert_allclose(fields.mean(axis=(1, 2)), 0, atol=1e-20)
|
|
49
|
+
spectra = np.abs(np.fft.fft2(fields)) ** 2
|
|
50
|
+
k2 = (2 * np.pi * np.fft.fftfreq(X.shape[0], spacing)[:, None]) ** 2 + (2 * np.pi * np.fft.fftfreq(X.shape[1], spacing)[None, :]) ** 2
|
|
51
|
+
energy = np.sum(np.divide(spectra, k2, out=np.zeros_like(spectra), where=k2 > 0), axis=(1, 2))
|
|
52
|
+
np.testing.assert_allclose(spectra.sum(axis=(1, 2)), spectra[0].sum(), rtol=1e-5)
|
|
53
|
+
np.testing.assert_allclose(energy, energy[0], rtol=1e-5)
|
|
54
|
+
assert not np.allclose(fields[-1], fields[0])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize(
|
|
58
|
+
"zeta,beta,spacing",
|
|
59
|
+
[(np.zeros((3, 8)), 1e-11, 1e5), (np.zeros(16), 1e-11, 1e5), (np.zeros((4, 4)), [1e-11, 1e-11], 1e5), (np.zeros((4, 4)), 1e-11, 0)],
|
|
60
|
+
)
|
|
61
|
+
def test_barotropic_rejects_bad_setup(zeta, beta, spacing):
|
|
62
|
+
with pytest.raises(ValueError):
|
|
63
|
+
simulate_barotropic_vorticity(zeta, [0, 1], beta, spacing)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_two_layer_profile_hydrostatic_pressure_matches_standard_atmosphere():
|
|
67
|
+
height = np.linspace(0, 11000, 2001)
|
|
68
|
+
temperature = two_layer_temperature(height, 288.15, 0.0065, 11000)
|
|
69
|
+
integrand = 9.80665 / (287.05 * temperature)
|
|
70
|
+
log_ratio = np.sum((integrand[1:] + integrand[:-1]) / 2 * np.diff(height))
|
|
71
|
+
assert np.isclose(101325 * np.exp(-log_ratio), 101325 * (216.65 / 288.15) ** (9.80665 / (287.05 * 0.0065)), rtol=1e-7)
|
|
72
|
+
assert two_layer_temperature(20000, 288.15, 0.0065, 11000) == two_layer_temperature(11000, 288.15, 0.0065, 11000)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_richardson_and_rossby_limits():
|
|
76
|
+
assert richardson_number(-1e-4, 0.01) < 0 < richardson_number(1e-4, -0.01)
|
|
77
|
+
beta, wind = 1.6e-11, 15.0
|
|
78
|
+
assert np.isclose(rossby_wave_speed(wind, beta, np.sqrt(beta / wind)), 0, atol=1e-12)
|
|
79
|
+
assert rossby_wave_speed(wind, 0, 1e-6) == wind
|
|
80
|
+
assert np.isclose(surface_pressure_tendency(-1e-6, 1e5) * 21600, 2160)
|
|
81
|
+
assert np.isclose(barometer_column_height(101325, 13595.1, 9.80665), 0.76, rtol=1e-4)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Plots for earthsciencekit.atmosphere.
|
|
2
|
+
|
|
3
|
+
The dynamic models return a TimeSeries, whose ``plot`` and ``animate`` methods
|
|
4
|
+
call these package-wide helpers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from earthsciencekit.visualizers import animate_series, plot_series
|
|
8
|
+
|
|
9
|
+
__all__ = ["plot_series", "animate_series"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Inspectable climate models; SI units unless stated otherwise."""
|
|
2
|
+
|
|
3
|
+
from earthsciencekit.climate.systems.carbon import (
|
|
4
|
+
carbon_airborne_fraction,
|
|
5
|
+
carbon_decay,
|
|
6
|
+
)
|
|
7
|
+
from earthsciencekit.climate.systems.forcing import (
|
|
8
|
+
co2_forcing,
|
|
9
|
+
methane_forcing,
|
|
10
|
+
nitrous_oxide_forcing,
|
|
11
|
+
)
|
|
12
|
+
from earthsciencekit.climate.systems.radiation import (
|
|
13
|
+
absorbed_solar,
|
|
14
|
+
daily_insolation,
|
|
15
|
+
effective_temperature,
|
|
16
|
+
greenhouse_surface_temperature,
|
|
17
|
+
outgoing_longwave,
|
|
18
|
+
radiative_convective_temperature,
|
|
19
|
+
)
|
|
20
|
+
from earthsciencekit.climate.systems.response import (
|
|
21
|
+
equilibrium_solar_constant,
|
|
22
|
+
equilibrium_warming,
|
|
23
|
+
gregory_regression,
|
|
24
|
+
heat_capacity_column,
|
|
25
|
+
heat_uptake,
|
|
26
|
+
ice_albedo,
|
|
27
|
+
planck_feedback,
|
|
28
|
+
red_noise_spectrum,
|
|
29
|
+
response_timescale,
|
|
30
|
+
simulate_one_box,
|
|
31
|
+
step_temperature,
|
|
32
|
+
)
|
|
33
|
+
from earthsciencekit.climate.systems.sea_level import (
|
|
34
|
+
thermal_expansion_sea_level,
|
|
35
|
+
water_equivalent_sea_level,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"absorbed_solar",
|
|
40
|
+
"effective_temperature",
|
|
41
|
+
"outgoing_longwave",
|
|
42
|
+
"planck_feedback",
|
|
43
|
+
"co2_forcing",
|
|
44
|
+
"equilibrium_warming",
|
|
45
|
+
"heat_capacity_column",
|
|
46
|
+
"response_timescale",
|
|
47
|
+
"step_temperature",
|
|
48
|
+
"heat_uptake",
|
|
49
|
+
"ice_albedo",
|
|
50
|
+
"carbon_airborne_fraction",
|
|
51
|
+
"carbon_decay",
|
|
52
|
+
"thermal_expansion_sea_level",
|
|
53
|
+
"water_equivalent_sea_level",
|
|
54
|
+
"greenhouse_surface_temperature",
|
|
55
|
+
"daily_insolation",
|
|
56
|
+
"radiative_convective_temperature",
|
|
57
|
+
"equilibrium_solar_constant",
|
|
58
|
+
"red_noise_spectrum",
|
|
59
|
+
"methane_forcing",
|
|
60
|
+
"nitrous_oxide_forcing",
|
|
61
|
+
"gregory_regression",
|
|
62
|
+
"simulate_one_box",
|
|
63
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Result containers for earthsciencekit.climate.
|
|
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.climate, grouped by topic."""
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Carbon budget: airborne fraction and single-reservoir decay.
|
|
2
|
+
|
|
3
|
+
SI units unless stated otherwise.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from earthsciencekit._validation import model, quantity
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"carbon_airborne_fraction",
|
|
12
|
+
"carbon_decay",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@model
|
|
17
|
+
def carbon_airborne_fraction(atmospheric_increase, emissions):
|
|
18
|
+
"""Carbon budget fraction.
|
|
19
|
+
|
|
20
|
+
A carbon mass budget partitions emissions and atmospheric accumulation.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
atmospheric_increase : float or array_like
|
|
25
|
+
kg C; constraint: nonnegative.
|
|
26
|
+
emissions : float or array_like
|
|
27
|
+
kg C; constraint: positive.
|
|
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
|
+
Same time interval and carbon mass units; feedback-driven values above one are not clipped.
|
|
37
|
+
Equation: ``atmospheric_increase / emissions``.
|
|
38
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
39
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
40
|
+
"""
|
|
41
|
+
atmospheric_increase = quantity("atmospheric_increase", atmospheric_increase, lower=0)
|
|
42
|
+
emissions = quantity("emissions", emissions, lower=0, strict_lower=True)
|
|
43
|
+
return atmospheric_increase / emissions
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@model
|
|
47
|
+
def carbon_decay(initial_excess, timescale, time):
|
|
48
|
+
"""Single-reservoir carbon relaxation.
|
|
49
|
+
|
|
50
|
+
A reservoir model illustrates first-order removal and residence time.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
initial_excess : float or array_like
|
|
55
|
+
kg C; constraint: nonnegative.
|
|
56
|
+
timescale : float or array_like
|
|
57
|
+
s; constraint: positive.
|
|
58
|
+
time : float or array_like
|
|
59
|
+
s; constraint: nonnegative.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
numpy scalar or ndarray
|
|
64
|
+
Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
65
|
+
|
|
66
|
+
Notes
|
|
67
|
+
-----
|
|
68
|
+
Not a realistic complete atmospheric CO2 impulse response, which has multiple timescales.
|
|
69
|
+
Equation: ``initial_excess * np.exp(-time / timescale)``.
|
|
70
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
71
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
72
|
+
"""
|
|
73
|
+
initial_excess = quantity("initial_excess", initial_excess, lower=0)
|
|
74
|
+
timescale = quantity("timescale", timescale, lower=0, strict_lower=True)
|
|
75
|
+
time = quantity("time", time, lower=0)
|
|
76
|
+
return initial_excess * np.exp(-time / timescale)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Radiative forcing of the well-mixed greenhouse gases.
|
|
2
|
+
|
|
3
|
+
SI units unless stated otherwise.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from earthsciencekit._validation import model, quantity
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"co2_forcing",
|
|
12
|
+
"methane_forcing",
|
|
13
|
+
"nitrous_oxide_forcing",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@model
|
|
18
|
+
def co2_forcing(concentration, reference_concentration, coefficient):
|
|
19
|
+
"""Logarithmic CO2 forcing approximation.
|
|
20
|
+
|
|
21
|
+
A logarithmic approximation connects concentration changes to a forcing perturbation.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
concentration : float or array_like
|
|
26
|
+
ppm; constraint: positive.
|
|
27
|
+
reference_concentration : float or array_like
|
|
28
|
+
ppm; constraint: positive.
|
|
29
|
+
coefficient : float or array_like
|
|
30
|
+
W/m^2; constraint: positive.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
numpy scalar or ndarray
|
|
35
|
+
Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
36
|
+
|
|
37
|
+
Notes
|
|
38
|
+
-----
|
|
39
|
+
User-supplied coefficient; an educational parameterization, not a current assessed forcing product.
|
|
40
|
+
Equation: ``coefficient * np.log(concentration / reference_concentration)``.
|
|
41
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
42
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
43
|
+
"""
|
|
44
|
+
concentration = quantity("concentration", concentration, lower=0, strict_lower=True)
|
|
45
|
+
reference_concentration = quantity("reference_concentration", reference_concentration, lower=0, strict_lower=True)
|
|
46
|
+
coefficient = quantity("coefficient", coefficient, lower=0, strict_lower=True)
|
|
47
|
+
return coefficient * np.log(concentration / reference_concentration)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _methane_nitrous_overlap(methane, nitrous_oxide):
|
|
51
|
+
"""Myhre et al. (1998) CH4-N2O band overlap in W/m^2; concentrations in ppb."""
|
|
52
|
+
product = methane * nitrous_oxide
|
|
53
|
+
return 0.47 * np.log(1 + 2.01e-5 * product**0.75 + 5.31e-15 * methane * product**1.52)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@model
|
|
57
|
+
def methane_forcing(concentration, reference_concentration, nitrous_oxide):
|
|
58
|
+
"""Methane radiative forcing (Myhre et al. 1998).
|
|
59
|
+
|
|
60
|
+
Methane forcing grows as a square root of concentration, less the band it shares with nitrous oxide.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
concentration : float or array_like
|
|
65
|
+
ppb; constraint: positive.
|
|
66
|
+
reference_concentration : float or array_like
|
|
67
|
+
ppb; constraint: positive.
|
|
68
|
+
nitrous_oxide : float or array_like
|
|
69
|
+
ppb, reference N2O concentration; constraint: positive.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
numpy scalar or ndarray
|
|
74
|
+
Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
75
|
+
|
|
76
|
+
Notes
|
|
77
|
+
-----
|
|
78
|
+
Simplified fit to line-by-line calculations, valid near present-day concentrations; with
|
|
79
|
+
``f(M, N) = 0.47 * log(1 + 2.01e-5 * (M * N)**0.75 + 5.31e-15 * M * (M * N)**1.52)``.
|
|
80
|
+
Equation: ``0.036 * (sqrt(M) - sqrt(M0)) - (f(M, N0) - f(M0, N0))``, with ``M = concentration``,
|
|
81
|
+
``M0 = reference_concentration`` and ``N0 = nitrous_oxide``.
|
|
82
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
83
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
84
|
+
"""
|
|
85
|
+
concentration = quantity("concentration", concentration, lower=0, strict_lower=True)
|
|
86
|
+
reference_concentration = quantity("reference_concentration", reference_concentration, lower=0, strict_lower=True)
|
|
87
|
+
nitrous_oxide = quantity("nitrous_oxide", nitrous_oxide, lower=0, strict_lower=True)
|
|
88
|
+
overlap = _methane_nitrous_overlap(concentration, nitrous_oxide) - _methane_nitrous_overlap(reference_concentration, nitrous_oxide)
|
|
89
|
+
return 0.036 * (np.sqrt(concentration) - np.sqrt(reference_concentration)) - overlap
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@model
|
|
93
|
+
def nitrous_oxide_forcing(concentration, reference_concentration, methane):
|
|
94
|
+
"""Nitrous oxide radiative forcing (Myhre et al. 1998).
|
|
95
|
+
|
|
96
|
+
Nitrous oxide forcing grows as a square root of concentration, less the band it shares with methane.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
concentration : float or array_like
|
|
101
|
+
ppb; constraint: positive.
|
|
102
|
+
reference_concentration : float or array_like
|
|
103
|
+
ppb; constraint: positive.
|
|
104
|
+
methane : float or array_like
|
|
105
|
+
ppb, reference CH4 concentration; constraint: positive.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
numpy scalar or ndarray
|
|
110
|
+
Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
111
|
+
|
|
112
|
+
Notes
|
|
113
|
+
-----
|
|
114
|
+
Simplified fit to line-by-line calculations, valid near present-day concentrations; f is the overlap term of methane_forcing.
|
|
115
|
+
Equation: ``0.12 * (sqrt(N) - sqrt(N0)) - (f(M0, N) - f(M0, N0))``, with ``N = concentration``,
|
|
116
|
+
``N0 = reference_concentration`` and ``M0 = methane``.
|
|
117
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
118
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
119
|
+
"""
|
|
120
|
+
concentration = quantity("concentration", concentration, lower=0, strict_lower=True)
|
|
121
|
+
reference_concentration = quantity("reference_concentration", reference_concentration, lower=0, strict_lower=True)
|
|
122
|
+
methane = quantity("methane", methane, lower=0, strict_lower=True)
|
|
123
|
+
overlap = _methane_nitrous_overlap(methane, concentration) - _methane_nitrous_overlap(methane, reference_concentration)
|
|
124
|
+
return 0.12 * (np.sqrt(concentration) - np.sqrt(reference_concentration)) - overlap
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Radiation: absorbed sunlight, emission, greenhouse layers, insolation and radiative-convective profiles.
|
|
2
|
+
|
|
3
|
+
SI units unless stated otherwise.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from earthsciencekit._validation import model, quantity
|
|
9
|
+
from earthsciencekit.constants import SIGMA_SB
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"absorbed_solar",
|
|
13
|
+
"effective_temperature",
|
|
14
|
+
"outgoing_longwave",
|
|
15
|
+
"greenhouse_surface_temperature",
|
|
16
|
+
"daily_insolation",
|
|
17
|
+
"radiative_convective_temperature",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@model
|
|
22
|
+
def absorbed_solar(solar_constant, albedo):
|
|
23
|
+
"""Planetary mean absorbed sunlight.
|
|
24
|
+
|
|
25
|
+
Sphere geometry and reflection set the global mean solar input.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
solar_constant : float or array_like
|
|
30
|
+
W/m^2; constraint: nonnegative.
|
|
31
|
+
albedo : float or array_like
|
|
32
|
+
1; constraint: fraction.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
numpy scalar or ndarray
|
|
37
|
+
Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
38
|
+
|
|
39
|
+
Notes
|
|
40
|
+
-----
|
|
41
|
+
Global mean rather than local insolation; fixed bolometric albedo.
|
|
42
|
+
Equation: ``solar_constant * (1 - albedo) / 4``.
|
|
43
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
44
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
45
|
+
"""
|
|
46
|
+
solar_constant = quantity("solar_constant", solar_constant, lower=0)
|
|
47
|
+
albedo = quantity("albedo", albedo, lower=0, upper=1)
|
|
48
|
+
return solar_constant * (1 - albedo) / 4
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@model
|
|
52
|
+
def effective_temperature(absorbed_flux, emissivity):
|
|
53
|
+
"""Radiative equilibrium temperature.
|
|
54
|
+
|
|
55
|
+
Stefan-Boltzmann emission links an energy balance to temperature.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
absorbed_flux : float or array_like
|
|
60
|
+
W/m^2; constraint: nonnegative.
|
|
61
|
+
emissivity : float or array_like
|
|
62
|
+
1; constraint: open fraction.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
numpy scalar or ndarray
|
|
67
|
+
Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
68
|
+
|
|
69
|
+
Notes
|
|
70
|
+
-----
|
|
71
|
+
Single emitting level in equilibrium; not the surface temperature of a greenhouse atmosphere.
|
|
72
|
+
Equation: ``(absorbed_flux / (emissivity * SIGMA_SB))**0.25``.
|
|
73
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
74
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
75
|
+
"""
|
|
76
|
+
absorbed_flux = quantity("absorbed_flux", absorbed_flux, lower=0)
|
|
77
|
+
emissivity = quantity("emissivity", emissivity, lower=0, upper=1, strict_lower=True)
|
|
78
|
+
return (absorbed_flux / (emissivity * SIGMA_SB)) ** 0.25
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@model
|
|
82
|
+
def outgoing_longwave(temperature, emissivity):
|
|
83
|
+
"""Greybody infrared emission.
|
|
84
|
+
|
|
85
|
+
A greybody approximation maps temperature to outgoing radiant flux.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
temperature : float or array_like
|
|
90
|
+
K; constraint: positive.
|
|
91
|
+
emissivity : float or array_like
|
|
92
|
+
1; constraint: fraction.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
numpy scalar or ndarray
|
|
97
|
+
Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
98
|
+
|
|
99
|
+
Notes
|
|
100
|
+
-----
|
|
101
|
+
Constant emissivity; not a spectral atmospheric radiative-transfer solver.
|
|
102
|
+
Equation: ``emissivity * SIGMA_SB * temperature**4``.
|
|
103
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
104
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
105
|
+
"""
|
|
106
|
+
temperature = quantity("temperature", temperature, lower=0, strict_lower=True)
|
|
107
|
+
emissivity = quantity("emissivity", emissivity, lower=0, upper=1)
|
|
108
|
+
return emissivity * SIGMA_SB * temperature**4
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@model
|
|
112
|
+
def greenhouse_surface_temperature(absorbed_flux, layer_emissivity):
|
|
113
|
+
"""Surface temperature under one absorbing atmospheric layer.
|
|
114
|
+
|
|
115
|
+
A layer that absorbs and re-emits infrared returns part of the surface emission and warms the ground.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
absorbed_flux : float or array_like
|
|
120
|
+
W/m^2; constraint: nonnegative.
|
|
121
|
+
layer_emissivity : float or array_like
|
|
122
|
+
1; constraint: fraction.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
numpy scalar or ndarray
|
|
127
|
+
Model value [K]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
128
|
+
|
|
129
|
+
Notes
|
|
130
|
+
-----
|
|
131
|
+
Blackbody surface, a grey isothermal layer transparent to sunlight; the layer temperature is the surface temperature over 2**0.25.
|
|
132
|
+
Equation: ``(2 * absorbed_flux / ((2 - layer_emissivity) * SIGMA_SB))**0.25``.
|
|
133
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
134
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
135
|
+
"""
|
|
136
|
+
absorbed_flux = quantity("absorbed_flux", absorbed_flux, lower=0)
|
|
137
|
+
layer_emissivity = quantity("layer_emissivity", layer_emissivity, lower=0, upper=1)
|
|
138
|
+
return (2 * absorbed_flux / ((2 - layer_emissivity) * SIGMA_SB)) ** 0.25
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@model
|
|
142
|
+
def daily_insolation(solar_constant, latitude, declination, distance_ratio):
|
|
143
|
+
"""Daily-mean top-of-atmosphere insolation.
|
|
144
|
+
|
|
145
|
+
Latitude, solar declination and Earth-Sun distance set the sunlight a latitude receives over a day.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
solar_constant : float or array_like
|
|
150
|
+
W/m^2 at the mean Earth-Sun distance; constraint: nonnegative.
|
|
151
|
+
latitude : float or array_like
|
|
152
|
+
rad; constraint: closed interval [-pi/2, pi/2].
|
|
153
|
+
declination : float or array_like
|
|
154
|
+
rad; constraint: closed interval [-pi/2, pi/2].
|
|
155
|
+
distance_ratio : float or array_like
|
|
156
|
+
1, mean over actual Earth-Sun distance; constraint: positive.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
numpy scalar or ndarray
|
|
161
|
+
Model value [W/m^2]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
162
|
+
|
|
163
|
+
Notes
|
|
164
|
+
-----
|
|
165
|
+
Declination fixed over the day, no atmosphere; the sunset hour angle h0 is clipped for polar day and night.
|
|
166
|
+
Equation: ``solar_constant * distance_ratio**2 / pi * (h0 * sin(phi) * sin(delta) + cos(phi) * cos(delta) * sin(h0))``
|
|
167
|
+
with ``phi = latitude``, ``delta = declination`` and ``h0 = arccos(clip(-tan(phi) * tan(delta), -1, 1))``.
|
|
168
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
169
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
170
|
+
"""
|
|
171
|
+
solar_constant = quantity("solar_constant", solar_constant, lower=0)
|
|
172
|
+
latitude = quantity("latitude", latitude, lower=-np.pi / 2, upper=np.pi / 2)
|
|
173
|
+
declination = quantity("declination", declination, lower=-np.pi / 2, upper=np.pi / 2)
|
|
174
|
+
distance_ratio = quantity("distance_ratio", distance_ratio, lower=0, strict_lower=True)
|
|
175
|
+
hour_angle = np.arccos(np.clip(-np.tan(latitude) * np.tan(declination), -1, 1))
|
|
176
|
+
geometry = hour_angle * np.sin(latitude) * np.sin(declination) + np.cos(latitude) * np.cos(declination) * np.sin(hour_angle)
|
|
177
|
+
return solar_constant * distance_ratio**2 / np.pi * geometry
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@model
|
|
181
|
+
def radiative_convective_temperature(height, effective_temperature, lapse_rate, emission_height):
|
|
182
|
+
"""Radiative-convective equilibrium temperature profile.
|
|
183
|
+
|
|
184
|
+
Convection holds the troposphere at a fixed lapse rate below a radiatively balanced stratosphere.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
height : float or array_like
|
|
189
|
+
m above the surface; constraint: nonnegative.
|
|
190
|
+
effective_temperature : float or array_like
|
|
191
|
+
K; constraint: positive.
|
|
192
|
+
lapse_rate : float or array_like
|
|
193
|
+
K/m; constraint: positive.
|
|
194
|
+
emission_height : float or array_like
|
|
195
|
+
m; constraint: nonnegative.
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
numpy scalar or ndarray
|
|
200
|
+
Model value [K]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
201
|
+
|
|
202
|
+
Notes
|
|
203
|
+
-----
|
|
204
|
+
Grey optically thin stratosphere at the skin temperature effective_temperature / 2**0.25; the troposphere
|
|
205
|
+
passes through effective_temperature at emission_height, so the surface is effective_temperature + lapse_rate * emission_height.
|
|
206
|
+
Equation: ``np.maximum(effective_temperature + lapse_rate * (emission_height - height), effective_temperature / 2**0.25)``.
|
|
207
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
208
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
209
|
+
"""
|
|
210
|
+
height = quantity("height", height, lower=0)
|
|
211
|
+
effective_temperature = quantity("effective_temperature", effective_temperature, lower=0, strict_lower=True)
|
|
212
|
+
lapse_rate = quantity("lapse_rate", lapse_rate, lower=0, strict_lower=True)
|
|
213
|
+
emission_height = quantity("emission_height", emission_height, lower=0)
|
|
214
|
+
return np.maximum(effective_temperature + lapse_rate * (emission_height - height), effective_temperature / 2**0.25)
|