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,161 @@
|
|
|
1
|
+
"""Eruptions: buoyant plume rise, eruption-column height, the Volcanic Explosivity Index and a rising column parcel.
|
|
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.results import TimeSeries
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"plume_rise_height",
|
|
13
|
+
"column_height",
|
|
14
|
+
"volcanic_explosivity_index",
|
|
15
|
+
"simulate_eruption_column",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@model
|
|
20
|
+
def plume_rise_height(buoyancy_flux, buoyancy_frequency):
|
|
21
|
+
"""Maximum height of a buoyant plume in a stratified fluid.
|
|
22
|
+
|
|
23
|
+
Turbulent entrainment dilutes a plume until the stratification removes its buoyancy, which caps its height at a value
|
|
24
|
+
set by only the source buoyancy flux and the stratification.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
buoyancy_flux : float or array_like
|
|
29
|
+
m^4/s^3, source ``reduced_gravity * velocity * radius**2`` (the factor pi omitted); constraint: positive.
|
|
30
|
+
buoyancy_frequency : float or array_like
|
|
31
|
+
1/s; constraint: positive.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
numpy scalar or ndarray
|
|
36
|
+
Model value [m]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
37
|
+
|
|
38
|
+
Notes
|
|
39
|
+
-----
|
|
40
|
+
Morton, Taylor and Turner (1956) for a steady point source of buoyancy in uniform stratification, with Briggs's
|
|
41
|
+
constant 5.0 (entrainment about 0.1); source momentum, wind and moisture are neglected.
|
|
42
|
+
Equation: ``5.0 * buoyancy_flux**0.25 * buoyancy_frequency**-0.75``.
|
|
43
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
44
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
45
|
+
"""
|
|
46
|
+
buoyancy_flux = quantity("buoyancy_flux", buoyancy_flux, lower=0, strict_lower=True)
|
|
47
|
+
buoyancy_frequency = quantity("buoyancy_frequency", buoyancy_frequency, lower=0, strict_lower=True)
|
|
48
|
+
return 5.0 * buoyancy_flux**0.25 * buoyancy_frequency**-0.75
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@model
|
|
52
|
+
def column_height(heat_flux):
|
|
53
|
+
"""Height of a Plinian eruption column from its heat flux.
|
|
54
|
+
|
|
55
|
+
The thermal energy released at the vent, not the eruption velocity, controls how high a volcanic column rises.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
heat_flux : float or array_like
|
|
60
|
+
W, rate of heat release ``mass_eruption_rate * specific_heat * temperature_excess``; constraint: positive.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
numpy scalar or ndarray
|
|
65
|
+
Model value [m]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
66
|
+
|
|
67
|
+
Notes
|
|
68
|
+
-----
|
|
69
|
+
Wilson, Sparks, Huang and Watkins (1978), JGR 83: 1829-1836, for a standard atmosphere and a strong steady column;
|
|
70
|
+
wind bends weak plumes lower.
|
|
71
|
+
Equation: ``8.2 * heat_flux**0.25``.
|
|
72
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
73
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
74
|
+
"""
|
|
75
|
+
heat_flux = quantity("heat_flux", heat_flux, lower=0, strict_lower=True)
|
|
76
|
+
return 8.2 * heat_flux**0.25
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@model
|
|
80
|
+
def volcanic_explosivity_index(erupted_volume):
|
|
81
|
+
"""Volcanic Explosivity Index from the erupted tephra volume.
|
|
82
|
+
|
|
83
|
+
A single logarithmic number lets eruptions of very different style and size be compared and catalogued.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
erupted_volume : float or array_like
|
|
88
|
+
m^3, bulk tephra volume; constraint: nonnegative.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
numpy scalar or ndarray
|
|
93
|
+
Model value [1], an integer from 0 to 8. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
94
|
+
|
|
95
|
+
Notes
|
|
96
|
+
-----
|
|
97
|
+
Newhall and Self (1982), volume criterion only: VEI 0 below 1e4 m^3, 1 below 1e6 m^3, then one step per factor of
|
|
98
|
+
ten up to VEI 8 at 1e12 m^3 and above. Column height and other criteria are not used.
|
|
99
|
+
Equation: ``np.where(erupted_volume < 1e4, 0, np.where(erupted_volume < 1e6, 1,
|
|
100
|
+
np.clip(np.floor(np.log10(erupted_volume)) - 4, 2, 8)))``.
|
|
101
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
102
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
103
|
+
"""
|
|
104
|
+
erupted_volume = quantity("erupted_volume", erupted_volume, lower=0)
|
|
105
|
+
steps = np.clip(np.floor(np.log10(np.maximum(erupted_volume, 1e6))) - 4, 2, 8)
|
|
106
|
+
return np.where(erupted_volume < 1e4, 0.0, np.where(erupted_volume < 1e6, 1.0, steps))[()]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@safe_arithmetic
|
|
110
|
+
def simulate_eruption_column(vent_radius, vent_velocity, reduced_gravity, buoyancy_frequency, time, *, entrainment=0.12):
|
|
111
|
+
"""Follow a parcel up the axis of a steady buoyant eruption column.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
vent_radius : float
|
|
116
|
+
Positive scalar source radius [m].
|
|
117
|
+
vent_velocity : float
|
|
118
|
+
Positive scalar source velocity [m/s].
|
|
119
|
+
reduced_gravity : float
|
|
120
|
+
Positive scalar source buoyancy ``g (rho_air - rho_plume) / rho_air`` [m/s^2].
|
|
121
|
+
buoyancy_frequency : float
|
|
122
|
+
Nonnegative scalar Brunt-Vaisala frequency of the atmosphere [1/s].
|
|
123
|
+
time : array_like
|
|
124
|
+
Nonnegative strictly increasing travel times since the parcel left the vent [s].
|
|
125
|
+
entrainment : float
|
|
126
|
+
Positive scalar top-hat entrainment coefficient [1]; 0.12 reproduces the constant 5.0 of plume_rise_height.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
TimeSeries
|
|
131
|
+
Parcel height [m], vertical velocity [m/s] and reduced gravity [m/s^2].
|
|
132
|
+
|
|
133
|
+
Notes
|
|
134
|
+
-----
|
|
135
|
+
The Morton, Taylor and Turner (1956) top-hat plume equations for the fluxes Q = b**2 w, M = b**2 w**2 and
|
|
136
|
+
F = b**2 w g' (pi omitted), carried by a parcel moving at w = M / Q: dz/dt = M / Q, dQ/dt = 2 alpha abs(M)**1.5 / Q,
|
|
137
|
+
dM/dt = F and dF/dt = -N**2 M. In a uniform atmosphere the parcel stops near plume_rise_height. Past that height the
|
|
138
|
+
steady theory ends; the parcel is continued as a fountain that falls back with the same entrainment law and oscillates
|
|
139
|
+
about the level of neutral buoyancy, near three quarters of the maximum height, where the umbrella cloud spreads.
|
|
140
|
+
This illustrative closure omits wind, the dense gas-thrust region, particle fallout and moisture.
|
|
141
|
+
"""
|
|
142
|
+
from earthsciencekit._validation import time_grid
|
|
143
|
+
from earthsciencekit.integrators import integrate_ode
|
|
144
|
+
|
|
145
|
+
radius = quantity("vent_radius", vent_radius, lower=0, strict_lower=True)
|
|
146
|
+
velocity = quantity("vent_velocity", vent_velocity, lower=0, strict_lower=True)
|
|
147
|
+
buoyancy = quantity("reduced_gravity", reduced_gravity, lower=0, strict_lower=True)
|
|
148
|
+
frequency = quantity("buoyancy_frequency", buoyancy_frequency, lower=0)
|
|
149
|
+
alpha = quantity("entrainment", entrainment, lower=0, strict_lower=True)
|
|
150
|
+
require(all(v.ndim == 0 for v in (radius, velocity, buoyancy, frequency, alpha)), "eruption column parameters must be scalars")
|
|
151
|
+
time = time_grid(time)
|
|
152
|
+
|
|
153
|
+
def rhs(t, state):
|
|
154
|
+
_, volume, momentum, flux = state
|
|
155
|
+
return [momentum / volume, 2 * alpha * np.abs(momentum) ** 1.5 / volume, flux, -(frequency**2) * momentum]
|
|
156
|
+
|
|
157
|
+
initial = [0.0, radius**2 * velocity, radius**2 * velocity**2, radius**2 * velocity * buoyancy]
|
|
158
|
+
fluxes = integrate_ode(rhs, initial, time - time[0], names=("height", "Q", "M", "F"), units=("m", "m^3/s", "m^4/s^2", "m^4/s^3"))
|
|
159
|
+
_, volume, momentum, flux = fluxes.values.T
|
|
160
|
+
values = np.column_stack([fluxes.values[:, 0], momentum / volume, flux / volume])
|
|
161
|
+
return TimeSeries(time, values, ("height", "vertical_velocity", "reduced_gravity"), ("m", "m/s", "m/s^2"))
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Geochemistry: Rayleigh fractionation, delta notation, melt crystallization, lattice strain and the oxygen-isotope thermometer.
|
|
2
|
+
|
|
3
|
+
SI units unless stated otherwise; isotope delta values are per mil.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from earthsciencekit._validation import model, quantity
|
|
9
|
+
from earthsciencekit.constants import AVOGADRO, R_GAS
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"rayleigh_fractionation",
|
|
13
|
+
"delta_value",
|
|
14
|
+
"fractional_crystallization_melt",
|
|
15
|
+
"equilibrium_crystallization_melt",
|
|
16
|
+
"lattice_strain_partition",
|
|
17
|
+
"isotope_temperature",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@model
|
|
22
|
+
def rayleigh_fractionation(initial_ratio, remaining_fraction, fractionation_factor):
|
|
23
|
+
"""Isotope ratio of a reservoir depleted by Rayleigh distillation.
|
|
24
|
+
|
|
25
|
+
Removing a product that is always slightly enriched in one isotope drives the remaining reservoir further and further
|
|
26
|
+
from its starting composition.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
initial_ratio : float or array_like
|
|
31
|
+
1, heavy-to-light isotope ratio of the reservoir at the start; constraint: positive.
|
|
32
|
+
remaining_fraction : float or array_like
|
|
33
|
+
1, fraction of the reservoir left; constraint: fraction above zero.
|
|
34
|
+
fractionation_factor : float or array_like
|
|
35
|
+
1, product ratio over reservoir ratio at each instant; constraint: positive.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
numpy scalar or ndarray
|
|
40
|
+
Model value [1]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
41
|
+
|
|
42
|
+
Notes
|
|
43
|
+
-----
|
|
44
|
+
Rayleigh (1896): the product leaves as soon as it forms, the reservoir stays well mixed and the factor is constant.
|
|
45
|
+
Equation: ``initial_ratio * remaining_fraction**(fractionation_factor - 1)``.
|
|
46
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
47
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
48
|
+
"""
|
|
49
|
+
initial_ratio = quantity("initial_ratio", initial_ratio, lower=0, strict_lower=True)
|
|
50
|
+
remaining_fraction = quantity("remaining_fraction", remaining_fraction, lower=0, upper=1, strict_lower=True)
|
|
51
|
+
fractionation_factor = quantity("fractionation_factor", fractionation_factor, lower=0, strict_lower=True)
|
|
52
|
+
return initial_ratio * remaining_fraction ** (fractionation_factor - 1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@model
|
|
56
|
+
def delta_value(ratio, standard_ratio):
|
|
57
|
+
"""Isotope delta value relative to a standard.
|
|
58
|
+
|
|
59
|
+
Isotope ratios vary in the fourth decimal place, so geochemists report deviations from a standard in per mil.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
ratio : float or array_like
|
|
64
|
+
1, heavy-to-light isotope ratio of the sample; constraint: positive.
|
|
65
|
+
standard_ratio : float or array_like
|
|
66
|
+
1, the same ratio in the standard (e.g. VSMOW, VPDB); constraint: positive.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
numpy scalar or ndarray
|
|
71
|
+
Model value [per mil]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
72
|
+
|
|
73
|
+
Notes
|
|
74
|
+
-----
|
|
75
|
+
Linear in the ratio; delta values of different standards must be converted before they are compared.
|
|
76
|
+
Equation: ``(ratio / standard_ratio - 1) * 1000``.
|
|
77
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
78
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
79
|
+
"""
|
|
80
|
+
ratio = quantity("ratio", ratio, lower=0, strict_lower=True)
|
|
81
|
+
standard_ratio = quantity("standard_ratio", standard_ratio, lower=0, strict_lower=True)
|
|
82
|
+
return (ratio / standard_ratio - 1) * 1000
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@model
|
|
86
|
+
def fractional_crystallization_melt(initial_concentration, melt_fraction, partition_coefficient):
|
|
87
|
+
"""Trace-element concentration in a melt during fractional crystallization.
|
|
88
|
+
|
|
89
|
+
Crystals removed as soon as they form leave the residual melt enriched in the elements they reject.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
initial_concentration : float or array_like
|
|
94
|
+
Concentration in the parent melt (any unit, e.g. ppm); constraint: nonnegative.
|
|
95
|
+
melt_fraction : float or array_like
|
|
96
|
+
1, fraction of melt remaining; constraint: fraction above zero.
|
|
97
|
+
partition_coefficient : float or array_like
|
|
98
|
+
1, bulk solid/melt concentration ratio; constraint: nonnegative.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
numpy scalar or ndarray
|
|
103
|
+
Model value [unit of initial_concentration]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
104
|
+
|
|
105
|
+
Notes
|
|
106
|
+
-----
|
|
107
|
+
Rayleigh fractional crystallization (Bowen 1928; Gast 1968): crystals do not re-equilibrate and the bulk partition
|
|
108
|
+
coefficient is constant.
|
|
109
|
+
Equation: ``initial_concentration * melt_fraction**(partition_coefficient - 1)``.
|
|
110
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
111
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
112
|
+
"""
|
|
113
|
+
initial_concentration = quantity("initial_concentration", initial_concentration, lower=0)
|
|
114
|
+
melt_fraction = quantity("melt_fraction", melt_fraction, lower=0, upper=1, strict_lower=True)
|
|
115
|
+
partition_coefficient = quantity("partition_coefficient", partition_coefficient, lower=0)
|
|
116
|
+
return initial_concentration * melt_fraction ** (partition_coefficient - 1)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@model
|
|
120
|
+
def equilibrium_crystallization_melt(initial_concentration, melt_fraction, partition_coefficient):
|
|
121
|
+
"""Trace-element concentration in a melt during equilibrium crystallization.
|
|
122
|
+
|
|
123
|
+
Crystals that keep reacting with the melt, as in Bowen's continuous series, limit how far the melt can evolve.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
initial_concentration : float or array_like
|
|
128
|
+
Concentration in the parent melt (any unit, e.g. ppm); constraint: nonnegative.
|
|
129
|
+
melt_fraction : float or array_like
|
|
130
|
+
1, fraction of melt remaining; constraint: fraction.
|
|
131
|
+
partition_coefficient : float or array_like
|
|
132
|
+
1, bulk solid/melt concentration ratio; constraint: positive.
|
|
133
|
+
|
|
134
|
+
Returns
|
|
135
|
+
-------
|
|
136
|
+
numpy scalar or ndarray
|
|
137
|
+
Model value [unit of initial_concentration]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
138
|
+
|
|
139
|
+
Notes
|
|
140
|
+
-----
|
|
141
|
+
Batch mass balance (Shaw 1970): solid and melt stay in equilibrium, so an incompatible element is enriched at most by
|
|
142
|
+
1 / partition_coefficient.
|
|
143
|
+
Equation: ``initial_concentration / (partition_coefficient + melt_fraction * (1 - partition_coefficient))``.
|
|
144
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
145
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
146
|
+
"""
|
|
147
|
+
initial_concentration = quantity("initial_concentration", initial_concentration, lower=0)
|
|
148
|
+
melt_fraction = quantity("melt_fraction", melt_fraction, lower=0, upper=1)
|
|
149
|
+
partition_coefficient = quantity("partition_coefficient", partition_coefficient, lower=0, strict_lower=True)
|
|
150
|
+
return initial_concentration / (partition_coefficient + melt_fraction * (1 - partition_coefficient))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@model
|
|
154
|
+
def lattice_strain_partition(ionic_radius, optimal_radius, peak_partition, youngs_modulus, temperature):
|
|
155
|
+
"""Mineral-melt partition coefficient from the strain of the crystal lattice.
|
|
156
|
+
|
|
157
|
+
Ions close in size to the site they replace enter a crystal easily, which is Goldschmidt's first rule of substitution.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
ionic_radius : float or array_like
|
|
162
|
+
m; constraint: positive.
|
|
163
|
+
optimal_radius : float or array_like
|
|
164
|
+
m, radius that fits the site without strain; constraint: positive.
|
|
165
|
+
peak_partition : float or array_like
|
|
166
|
+
1, partition coefficient of an ion of the optimal radius; constraint: positive.
|
|
167
|
+
youngs_modulus : float or array_like
|
|
168
|
+
Pa, apparent Young's modulus of the site; constraint: positive.
|
|
169
|
+
temperature : float or array_like
|
|
170
|
+
K; constraint: positive.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
numpy scalar or ndarray
|
|
175
|
+
Model value [1]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
176
|
+
|
|
177
|
+
Notes
|
|
178
|
+
-----
|
|
179
|
+
Blundy and Wood (1994) quantification of Goldschmidt (1937) for isovalent ions on one site; charge differences
|
|
180
|
+
need a separate parabola.
|
|
181
|
+
Equation: ``peak_partition * np.exp(-4 * np.pi * youngs_modulus * AVOGADRO * (optimal_radius / 2 * (ionic_radius - optimal_radius)**2
|
|
182
|
+
+ (ionic_radius - optimal_radius)**3 / 3) / (R_GAS * temperature))``.
|
|
183
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
184
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
185
|
+
"""
|
|
186
|
+
ionic_radius = quantity("ionic_radius", ionic_radius, lower=0, strict_lower=True)
|
|
187
|
+
optimal_radius = quantity("optimal_radius", optimal_radius, lower=0, strict_lower=True)
|
|
188
|
+
peak_partition = quantity("peak_partition", peak_partition, lower=0, strict_lower=True)
|
|
189
|
+
youngs_modulus = quantity("youngs_modulus", youngs_modulus, lower=0, strict_lower=True)
|
|
190
|
+
temperature = quantity("temperature", temperature, lower=0, strict_lower=True)
|
|
191
|
+
misfit = ionic_radius - optimal_radius
|
|
192
|
+
strain = 4 * np.pi * youngs_modulus * AVOGADRO * (optimal_radius / 2 * misfit**2 + misfit**3 / 3)
|
|
193
|
+
return peak_partition * np.exp(-strain / (R_GAS * temperature))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@model
|
|
197
|
+
def isotope_temperature(delta_calcite, delta_water):
|
|
198
|
+
"""Calcite-water oxygen-isotope paleotemperature.
|
|
199
|
+
|
|
200
|
+
The oxygen-isotope fractionation between a shell and the water it grew in falls with temperature, so fossil shells
|
|
201
|
+
record ancient ocean temperatures.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
delta_calcite : float or array_like
|
|
206
|
+
per mil, delta18O of the carbonate (CO2 from the calcite, PDB scale); constraint: real.
|
|
207
|
+
delta_water : float or array_like
|
|
208
|
+
per mil, delta18O of the water on the same scale; constraint: real.
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
numpy scalar or ndarray
|
|
213
|
+
Model value [K]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
214
|
+
|
|
215
|
+
Notes
|
|
216
|
+
-----
|
|
217
|
+
Urey (1947) predicted the effect; this is the empirical calibration of Epstein, Buchsbaum, Lowenstam and Urey (1953),
|
|
218
|
+
valid for about 7-30 degC and equilibrium calcite.
|
|
219
|
+
Equation: ``273.15 + 16.5 - 4.3 * d + 0.14 * d**2`` with ``d = delta_calcite - delta_water``.
|
|
220
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
221
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
222
|
+
"""
|
|
223
|
+
delta_calcite = quantity("delta_calcite", delta_calcite)
|
|
224
|
+
delta_water = quantity("delta_water", delta_water)
|
|
225
|
+
difference = delta_calcite - delta_water
|
|
226
|
+
return 273.15 + 16.5 - 4.3 * difference + 0.14 * difference**2
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Geochronology: radioactive decay, the age equation, radiocarbon, lead-lead ages, the concordia and the isochron.
|
|
2
|
+
|
|
3
|
+
SI units unless stated otherwise; ages and half-lives are seconds and decay constants 1/s.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy.optimize import brentq
|
|
8
|
+
|
|
9
|
+
from earthsciencekit._validation import model, quantity, require
|
|
10
|
+
from earthsciencekit.constants import LAMBDA_U235, LAMBDA_U238, LIBBY_MEAN_LIFE, SECONDS_PER_YEAR, U238_U235
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"decay_constant",
|
|
14
|
+
"parent_fraction",
|
|
15
|
+
"radiometric_age",
|
|
16
|
+
"radiogenic_daughter",
|
|
17
|
+
"radiocarbon_age",
|
|
18
|
+
"radiogenic_lead_ratio",
|
|
19
|
+
"lead_lead_age",
|
|
20
|
+
"concordia_ratios",
|
|
21
|
+
"isochron_fit",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
_YOUNGEST, _OLDEST = SECONDS_PER_YEAR, 1e10 * SECONDS_PER_YEAR # s, the bracket of lead_lead_age
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@model
|
|
28
|
+
def decay_constant(half_life):
|
|
29
|
+
"""Decay constant from a half-life.
|
|
30
|
+
|
|
31
|
+
Every radioactive nuclide has a fixed probability per unit time of decaying, set by its half-life.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
half_life : float or array_like
|
|
36
|
+
s; constraint: positive.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
numpy scalar or ndarray
|
|
41
|
+
Model value [1/s]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
42
|
+
|
|
43
|
+
Notes
|
|
44
|
+
-----
|
|
45
|
+
Rutherford and Soddy (1902): first-order decay, independent of temperature, pressure and chemistry.
|
|
46
|
+
Equation: ``np.log(2) / half_life``.
|
|
47
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
48
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
49
|
+
"""
|
|
50
|
+
half_life = quantity("half_life", half_life, lower=0, strict_lower=True)
|
|
51
|
+
return np.log(2) / half_life
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@model
|
|
55
|
+
def parent_fraction(decay_constant, time):
|
|
56
|
+
"""Fraction of radioactive parent atoms remaining.
|
|
57
|
+
|
|
58
|
+
Exponential decay is the clock behind every radiometric age.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
decay_constant : float or array_like
|
|
63
|
+
1/s; constraint: positive.
|
|
64
|
+
time : float or array_like
|
|
65
|
+
s; constraint: nonnegative.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
numpy scalar or ndarray
|
|
70
|
+
Model value [1]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
71
|
+
|
|
72
|
+
Notes
|
|
73
|
+
-----
|
|
74
|
+
Rutherford and Soddy (1902): a closed system with no parent added after time zero.
|
|
75
|
+
Equation: ``np.exp(-decay_constant * time)``.
|
|
76
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
77
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
78
|
+
"""
|
|
79
|
+
decay_constant = quantity("decay_constant", decay_constant, lower=0, strict_lower=True)
|
|
80
|
+
time = quantity("time", time, lower=0)
|
|
81
|
+
return np.exp(-decay_constant * time)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@model
|
|
85
|
+
def radiometric_age(daughter_parent_ratio, decay_constant):
|
|
86
|
+
"""Age from a radiogenic daughter-to-parent ratio.
|
|
87
|
+
|
|
88
|
+
The ratio of accumulated daughter atoms to surviving parent atoms grows with time, so measuring it dates the rock.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
daughter_parent_ratio : float or array_like
|
|
93
|
+
1, radiogenic daughter atoms per parent atom; constraint: nonnegative.
|
|
94
|
+
decay_constant : float or array_like
|
|
95
|
+
1/s; constraint: positive.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
numpy scalar or ndarray
|
|
100
|
+
Model value [s]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
101
|
+
|
|
102
|
+
Notes
|
|
103
|
+
-----
|
|
104
|
+
Closed system with no initial daughter; Boltwood (1907) applied it to lead in uranium minerals.
|
|
105
|
+
Equation: ``np.log1p(daughter_parent_ratio) / decay_constant``.
|
|
106
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
107
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
108
|
+
"""
|
|
109
|
+
daughter_parent_ratio = quantity("daughter_parent_ratio", daughter_parent_ratio, lower=0)
|
|
110
|
+
decay_constant = quantity("decay_constant", decay_constant, lower=0, strict_lower=True)
|
|
111
|
+
return np.log1p(daughter_parent_ratio) / decay_constant
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@model
|
|
115
|
+
def radiogenic_daughter(parent, decay_constant, time):
|
|
116
|
+
"""Radiogenic daughter accumulated beside the surviving parent.
|
|
117
|
+
|
|
118
|
+
Forward accumulation shows how the daughter-to-parent ratio of a rock records its age.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
parent : float or array_like
|
|
123
|
+
Surviving parent amount today (atoms, moles or any consistent unit); constraint: nonnegative.
|
|
124
|
+
decay_constant : float or array_like
|
|
125
|
+
1/s; constraint: positive.
|
|
126
|
+
time : float or array_like
|
|
127
|
+
s; constraint: nonnegative.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
numpy scalar or ndarray
|
|
132
|
+
Model value [unit of parent]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
133
|
+
|
|
134
|
+
Notes
|
|
135
|
+
-----
|
|
136
|
+
Closed system with no initial daughter; the inverse of radiometric_age. Holmes (1913) used it to build the first
|
|
137
|
+
radiometric time scale.
|
|
138
|
+
Equation: ``parent * np.expm1(decay_constant * time)``.
|
|
139
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
140
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
141
|
+
"""
|
|
142
|
+
parent = quantity("parent", parent, lower=0)
|
|
143
|
+
decay_constant = quantity("decay_constant", decay_constant, lower=0, strict_lower=True)
|
|
144
|
+
time = quantity("time", time, lower=0)
|
|
145
|
+
return parent * np.expm1(decay_constant * time)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@model
|
|
149
|
+
def radiocarbon_age(fraction_modern):
|
|
150
|
+
"""Conventional radiocarbon age.
|
|
151
|
+
|
|
152
|
+
Living matter exchanges carbon-14 with the atmosphere; after death the carbon-14 decays, dating organic remains.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
fraction_modern : float or array_like
|
|
157
|
+
1, sample 14C/12C relative to the modern standard; constraint: positive.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
numpy scalar or ndarray
|
|
162
|
+
Model value [s]; negative for bomb carbon above the standard. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
163
|
+
|
|
164
|
+
Notes
|
|
165
|
+
-----
|
|
166
|
+
Libby et al. (1949) with the conventional 5568-year (Libby) half-life, mean life LIBBY_MEAN_LIFE; it is a radiocarbon
|
|
167
|
+
age, not a calendar age, until calibrated.
|
|
168
|
+
Equation: ``-LIBBY_MEAN_LIFE * np.log(fraction_modern)``.
|
|
169
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
170
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
171
|
+
"""
|
|
172
|
+
fraction_modern = quantity("fraction_modern", fraction_modern, lower=0, strict_lower=True)
|
|
173
|
+
return -LIBBY_MEAN_LIFE * np.log(fraction_modern)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@model
|
|
177
|
+
def radiogenic_lead_ratio(time):
|
|
178
|
+
"""Radiogenic 207Pb/206Pb ratio produced over a given age.
|
|
179
|
+
|
|
180
|
+
Two uranium isotopes decaying at different rates make the ratio of their lead daughters a clock that needs no uranium
|
|
181
|
+
measurement.
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
time : float or array_like
|
|
186
|
+
s; constraint: positive.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
numpy scalar or ndarray
|
|
191
|
+
Model value [1]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
192
|
+
|
|
193
|
+
Notes
|
|
194
|
+
-----
|
|
195
|
+
Patterson (1956): closed system with today's 238U/235U = U238_U235 and the Jaffey et al. (1971) decay constants. The
|
|
196
|
+
ratio tends to LAMBDA_U235 / (U238_U235 * LAMBDA_U238) at zero age.
|
|
197
|
+
Equation: ``np.expm1(LAMBDA_U235 * time) / (U238_U235 * np.expm1(LAMBDA_U238 * time))``.
|
|
198
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
199
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
200
|
+
"""
|
|
201
|
+
time = quantity("time", time, lower=0, strict_lower=True)
|
|
202
|
+
return np.expm1(LAMBDA_U235 * time) / (U238_U235 * np.expm1(LAMBDA_U238 * time))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@model
|
|
206
|
+
def lead_lead_age(lead_ratio):
|
|
207
|
+
"""Age from a radiogenic 207Pb/206Pb ratio.
|
|
208
|
+
|
|
209
|
+
Patterson dated the Earth from the slope of a meteorite lead isochron, which equals this ratio.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
lead_ratio : float or array_like
|
|
214
|
+
1, radiogenic 207Pb*/206Pb* or the slope of a Pb-Pb isochron; constraint: between the ratios at one year
|
|
215
|
+
(about 0.0461) and at 10 Ga.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
numpy scalar or ndarray
|
|
220
|
+
Model value [s]. Scalar input returns a NumPy scalar; arrays broadcast.
|
|
221
|
+
|
|
222
|
+
Notes
|
|
223
|
+
-----
|
|
224
|
+
Inverts radiogenic_lead_ratio by Brent's method between one year and 10 Ga; same closed-system assumptions.
|
|
225
|
+
Equation: ``radiogenic_lead_ratio(age) = lead_ratio``.
|
|
226
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
227
|
+
"""
|
|
228
|
+
lead_ratio = quantity("lead_ratio", lead_ratio)
|
|
229
|
+
require(lead_ratio >= radiogenic_lead_ratio(_YOUNGEST), "lead_ratio is below the ratio at one year")
|
|
230
|
+
require(lead_ratio <= radiogenic_lead_ratio(_OLDEST), "lead_ratio exceeds the ratio at 10 Ga")
|
|
231
|
+
|
|
232
|
+
def solve(ratio):
|
|
233
|
+
return brentq(lambda age: radiogenic_lead_ratio(age) - ratio, _YOUNGEST, _OLDEST, xtol=1e-6 * SECONDS_PER_YEAR, rtol=1e-14)
|
|
234
|
+
|
|
235
|
+
return np.vectorize(solve, otypes=[float])(lead_ratio)[()]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@model
|
|
239
|
+
def concordia_ratios(time):
|
|
240
|
+
"""Point on the Wetherill U-Pb concordia.
|
|
241
|
+
|
|
242
|
+
Both uranium-lead clocks agree only on this curve, so a zircon off it has lost lead and its chord dates the loss.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
time : float or array_like
|
|
247
|
+
s; constraint: nonnegative.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
ndarray, shape (..., 2)
|
|
252
|
+
Model value [1]: ``[207Pb*/235U, 206Pb*/238U]``, the horizontal and vertical concordia coordinates.
|
|
253
|
+
|
|
254
|
+
Notes
|
|
255
|
+
-----
|
|
256
|
+
Wetherill (1956): closed system with the Jaffey et al. (1971) decay constants. Episodic lead loss puts samples on a
|
|
257
|
+
straight chord (discordia) between the concordia points of formation and loss.
|
|
258
|
+
Equation: ``[np.expm1(LAMBDA_U235 * time), np.expm1(LAMBDA_U238 * time)]``.
|
|
259
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
260
|
+
NumPy broadcasting applies; singular and overflowing outputs raise ValueError.
|
|
261
|
+
"""
|
|
262
|
+
time = quantity("time", time, lower=0)
|
|
263
|
+
return np.stack([np.expm1(LAMBDA_U235 * time), np.expm1(LAMBDA_U238 * time)], axis=-1)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@model
|
|
267
|
+
def isochron_fit(parent_ratio, daughter_ratio, decay_constant):
|
|
268
|
+
"""Age and initial ratio from a whole-rock or mineral isochron.
|
|
269
|
+
|
|
270
|
+
Cogenetic minerals with different parent contents line up on a straight line whose slope gives the age without knowing
|
|
271
|
+
the initial daughter.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
parent_ratio : array_like, shape (n,), n >= 2
|
|
276
|
+
1, parent over a stable reference isotope (e.g. 87Rb/86Sr); constraint: nonnegative, not all equal.
|
|
277
|
+
daughter_ratio : array_like, shape (n,)
|
|
278
|
+
1, daughter over the same reference isotope (e.g. 87Sr/86Sr); constraint: nonnegative.
|
|
279
|
+
decay_constant : float
|
|
280
|
+
1/s; constraint: positive scalar.
|
|
281
|
+
|
|
282
|
+
Returns
|
|
283
|
+
-------
|
|
284
|
+
numpy.ndarray, shape (2,)
|
|
285
|
+
``[age, initial_ratio]``: the age [s] from the least-squares slope and the intercept [1].
|
|
286
|
+
|
|
287
|
+
Notes
|
|
288
|
+
-----
|
|
289
|
+
Nicolaysen (1961): every sample formed at the same time with the same initial ratio and stayed closed; ordinary least
|
|
290
|
+
squares without measurement-error weighting (a York fit would weight them). The slope must be positive.
|
|
291
|
+
Equation: ``daughter_ratio = initial_ratio + parent_ratio * np.expm1(decay_constant * age)``.
|
|
292
|
+
Invalid, empty, nonreal or nonfinite inputs raise ValueError.
|
|
293
|
+
"""
|
|
294
|
+
parent_ratio = quantity("parent_ratio", parent_ratio, lower=0)
|
|
295
|
+
daughter_ratio = quantity("daughter_ratio", daughter_ratio, lower=0)
|
|
296
|
+
decay_constant = quantity("decay_constant", decay_constant, lower=0, strict_lower=True)
|
|
297
|
+
require(parent_ratio.ndim == 1 and parent_ratio.size >= 2, "parent_ratio must be a vector of at least two samples")
|
|
298
|
+
require(daughter_ratio.shape == parent_ratio.shape, "daughter_ratio must match parent_ratio")
|
|
299
|
+
require(decay_constant.ndim == 0, "decay_constant must be a scalar")
|
|
300
|
+
anomaly = parent_ratio - parent_ratio.mean()
|
|
301
|
+
require(np.any(anomaly != 0), "parent_ratio must vary")
|
|
302
|
+
slope = np.dot(anomaly, daughter_ratio - daughter_ratio.mean()) / np.dot(anomaly, anomaly)
|
|
303
|
+
require(slope > 0, "isochron slope must be positive")
|
|
304
|
+
return np.array([np.log1p(slope) / decay_constant, daughter_ratio.mean() - slope * parent_ratio.mean()])
|