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,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,5 @@
1
+ """Result containers for earthsciencekit.climate."""
2
+
3
+ from earthsciencekit.climate.core.base import TimeSeries
4
+
5
+ __all__ = ["TimeSeries"]
@@ -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)