gomd 2.1.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.
- gomd/__init__.py +380 -0
- gomd/__main__.py +7 -0
- gomd/cli.py +76 -0
- gomd/client.py +1372 -0
- gomd/dynamics.py +629 -0
- gomd/maritime.py +952 -0
- gomd/ocean.py +1646 -0
- gomd/spatial.py +222 -0
- gomd/stats.py +1086 -0
- gomd-2.1.0.dist-info/METADATA +277 -0
- gomd-2.1.0.dist-info/RECORD +15 -0
- gomd-2.1.0.dist-info/WHEEL +5 -0
- gomd-2.1.0.dist-info/entry_points.txt +2 -0
- gomd-2.1.0.dist-info/licenses/LICENSE +21 -0
- gomd-2.1.0.dist-info/top_level.txt +1 -0
gomd/ocean.py
ADDED
|
@@ -0,0 +1,1646 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gomd.ocean — Physical Oceanography, Seawater Thermodynamics, Carbonate Chemistry,
|
|
3
|
+
Eutrophication (TRIX / TSI), Mucilage Hazard, and Marine Heatwave (MHW) Analytics
|
|
4
|
+
=================================================================================
|
|
5
|
+
Implements peer-reviewed oceanographic algorithms for the GOMD Python SDK:
|
|
6
|
+
1. UNESCO EOS-80 Seawater Equation of State: density rho(S, T, p) & sigma_t
|
|
7
|
+
2. Garcia & Gordon (1992) Dissolved Oxygen Solubility (DO_sat) & AOU
|
|
8
|
+
3. Surface Ocean Carbonate Equilibrium Solver (pCO2, DIC, HCO3-, CO32-, Omega_arag, Omega_calc)
|
|
9
|
+
4. Hobday et al. (2016, 2018) Marine Heatwave (MHW) Detection & Categorization (Cat I-IV)
|
|
10
|
+
5. Vollenweider et al. (1998) TRIX Coastal Eutrophication & Carlson (1977) TSI
|
|
11
|
+
6. Multi-Factor Marine Mucilage Hazard Index (Marmara / Mediterranean calibration)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def seawater_density_eos80(
|
|
21
|
+
salinity_psu: float,
|
|
22
|
+
temp_c: float,
|
|
23
|
+
pressure_dbar: float = 0.0,
|
|
24
|
+
) -> Dict[str, float]:
|
|
25
|
+
"""
|
|
26
|
+
Compute in-situ seawater density (kg/m^3), sigma_t (kg/m^3), and specific volume
|
|
27
|
+
anomaly using the UNESCO 1983 (EOS-80) International Equation of State of Seawater
|
|
28
|
+
(Fofonoff & Millard, 1983).
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
salinity_psu : float
|
|
33
|
+
Practical Salinity (PSU, 0 to 42).
|
|
34
|
+
temp_c : float
|
|
35
|
+
In-situ temperature in degrees Celsius (-2 to 40).
|
|
36
|
+
pressure_dbar : float, default 0.0
|
|
37
|
+
Sea pressure in decibars (1 dbar ~ 1 m depth; converted to bars internally).
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
dict
|
|
42
|
+
{'rho_kg_m3', 'sigma_0_kg_m3', 'rho_pure_water_kg_m3', 'secant_bulk_modulus_bar'}
|
|
43
|
+
"""
|
|
44
|
+
S = float(salinity_psu)
|
|
45
|
+
T = float(temp_c)
|
|
46
|
+
P = float(pressure_dbar) / 10.0 # dbar -> bar
|
|
47
|
+
|
|
48
|
+
# Pure water density rho_w(T) (Bigg, 1967 / UNESCO 1983)
|
|
49
|
+
rho_w = (
|
|
50
|
+
999.842594
|
|
51
|
+
+ 6.793952e-2 * T
|
|
52
|
+
- 9.095290e-3 * (T ** 2)
|
|
53
|
+
+ 1.001685e-4 * (T ** 3)
|
|
54
|
+
- 1.120083e-6 * (T ** 4)
|
|
55
|
+
+ 6.536332e-9 * (T ** 5)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# One-atmosphere density rho(S, T, 0)
|
|
59
|
+
A = (
|
|
60
|
+
8.24493e-1
|
|
61
|
+
- 4.0899e-3 * T
|
|
62
|
+
+ 7.6438e-5 * (T ** 2)
|
|
63
|
+
- 8.2467e-7 * (T ** 3)
|
|
64
|
+
+ 5.3875e-9 * (T ** 4)
|
|
65
|
+
)
|
|
66
|
+
B = -5.72466e-3 + 1.0227e-4 * T - 1.6546e-6 * (T ** 2)
|
|
67
|
+
C = 4.8314e-4
|
|
68
|
+
|
|
69
|
+
rho_0 = rho_w + A * S + B * (S ** 1.5) + C * (S ** 2)
|
|
70
|
+
sigma_0 = rho_0 - 1000.0
|
|
71
|
+
|
|
72
|
+
# Secant bulk modulus K(S, T, P) for high-pressure correction
|
|
73
|
+
Kw = (
|
|
74
|
+
19652.21
|
|
75
|
+
+ 148.4206 * T
|
|
76
|
+
- 2.327105 * (T ** 2)
|
|
77
|
+
+ 1.360477e-2 * (T ** 3)
|
|
78
|
+
- 5.155288e-5 * (T ** 4)
|
|
79
|
+
)
|
|
80
|
+
Aw = 3.239908 + 1.43713e-3 * T + 1.16092e-4 * (T ** 2) - 5.77905e-7 * (T ** 3)
|
|
81
|
+
Bw = 8.50935e-5 - 6.12293e-6 * T + 5.2787e-8 * (T ** 2)
|
|
82
|
+
|
|
83
|
+
K0 = (
|
|
84
|
+
Kw
|
|
85
|
+
+ (54.6746 - 0.603459 * T + 1.09987e-2 * (T ** 2) - 6.1670e-5 * (T ** 3)) * S
|
|
86
|
+
+ (7.944e-2 + 1.6483e-2 * T - 5.3009e-4 * (T ** 2)) * (S ** 1.5)
|
|
87
|
+
)
|
|
88
|
+
A_p = Aw + (2.2838e-3 - 1.0981e-5 * T - 1.6078e-6 * (T ** 2)) * S + 1.91075e-4 * (S ** 1.5)
|
|
89
|
+
B_p = Bw + (-9.9348e-7 + 2.0816e-8 * T + 9.1697e-10 * (T ** 2)) * S
|
|
90
|
+
|
|
91
|
+
K = K0 + A_p * P + B_p * (P ** 2)
|
|
92
|
+
rho_p = rho_0 / (1.0 - P / K) if P > 0 else rho_0
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
"rho_kg_m3": round(rho_p, 4),
|
|
96
|
+
"density_kg_m3": round(rho_p, 4),
|
|
97
|
+
"sigma_0_kg_m3": round(sigma_0, 4),
|
|
98
|
+
"sigma_t_kg_m3": round(sigma_0, 4),
|
|
99
|
+
"rho_pure_water_kg_m3": round(rho_w, 4),
|
|
100
|
+
"secant_bulk_modulus_bar": round(K, 2),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def oxygen_solubility(
|
|
105
|
+
temp_c: float,
|
|
106
|
+
salinity_psu: float,
|
|
107
|
+
observed_do_umol_kg: Optional[float] = None,
|
|
108
|
+
) -> Dict[str, Optional[float]]:
|
|
109
|
+
"""
|
|
110
|
+
Compute Dissolved Oxygen saturation concentration (DO_sat) in seawater using the
|
|
111
|
+
Garcia & Gordon (1992) Benson-Krause refit, plus Apparent Oxygen Utilization (AOU)
|
|
112
|
+
and hypoxia status if `observed_do_umol_kg` is supplied.
|
|
113
|
+
|
|
114
|
+
Reference: Garcia, H. E., & Gordon, L. I. (1992). Oxygen solubility in seawater:
|
|
115
|
+
Better fitting equations. Limnology and Oceanography, 37(6), 1307-1312.
|
|
116
|
+
"""
|
|
117
|
+
T = float(temp_c)
|
|
118
|
+
S = float(salinity_psu)
|
|
119
|
+
T_s = math.log((298.15 - T) / (273.15 + T))
|
|
120
|
+
|
|
121
|
+
# Garcia & Gordon (1992) Table 1 constants for umol/kg
|
|
122
|
+
A0, A1, A2, A3, A4, A5 = 5.80871, 3.20291, 4.17887, 5.10006, -9.86643e-2, 3.80369
|
|
123
|
+
B0, B1, B2, B3 = -7.01577e-3, -7.70028e-3, -1.13864e-2, -9.51519e-3
|
|
124
|
+
C0 = -2.75915e-7
|
|
125
|
+
|
|
126
|
+
ln_C = (
|
|
127
|
+
A0
|
|
128
|
+
+ A1 * T_s
|
|
129
|
+
+ A2 * (T_s ** 2)
|
|
130
|
+
+ A3 * (T_s ** 3)
|
|
131
|
+
+ A4 * (T_s ** 4)
|
|
132
|
+
+ A5 * (T_s ** 5)
|
|
133
|
+
+ S * (B0 + B1 * T_s + B2 * (T_s ** 2) + B3 * (T_s ** 3))
|
|
134
|
+
+ C0 * (S ** 2)
|
|
135
|
+
)
|
|
136
|
+
do_sat_umol_kg = math.exp(ln_C)
|
|
137
|
+
# Approximate conversion: 1 mg/L ~ 31.25 umol/L, adjusted by seawater density
|
|
138
|
+
rho_kg_l = seawater_density_eos80(S, T, 0.0)["rho_kg_m3"] / 1000.0
|
|
139
|
+
do_sat_mg_l = (do_sat_umol_kg * rho_kg_l * 31.9988) / 1000.0
|
|
140
|
+
|
|
141
|
+
aou = None
|
|
142
|
+
pct_sat = None
|
|
143
|
+
hypoxia_class = None
|
|
144
|
+
if observed_do_umol_kg is not None:
|
|
145
|
+
obs = float(observed_do_umol_kg)
|
|
146
|
+
aou = round(do_sat_umol_kg - obs, 3)
|
|
147
|
+
pct_sat = round((obs / do_sat_umol_kg) * 100.0, 2)
|
|
148
|
+
obs_mg_l = (obs * rho_kg_l * 31.9988) / 1000.0
|
|
149
|
+
if obs_mg_l < 0.2:
|
|
150
|
+
hypoxia_class = "anoxic (<0.2 mg/L)"
|
|
151
|
+
elif obs_mg_l < 2.0:
|
|
152
|
+
hypoxia_class = "hypoxic (<2.0 mg/L)"
|
|
153
|
+
elif obs_mg_l < 4.0:
|
|
154
|
+
hypoxia_class = "suboxic_stress (2.0-4.0 mg/L)"
|
|
155
|
+
else:
|
|
156
|
+
hypoxia_class = "normoxic (>=4.0 mg/L)"
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"do_sat_umol_kg": round(do_sat_umol_kg, 3),
|
|
160
|
+
"do_sat_mg_l": round(do_sat_mg_l, 3),
|
|
161
|
+
"aou_umol_kg": aou,
|
|
162
|
+
"oxygen_saturation_pct": pct_sat,
|
|
163
|
+
"hypoxia_status": hypoxia_class,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def carbonate_system(
|
|
168
|
+
ph_total: float,
|
|
169
|
+
temp_c: float,
|
|
170
|
+
salinity_psu: float = 35.0,
|
|
171
|
+
ta_umol_kg: float = 2320.0,
|
|
172
|
+
) -> Dict[str, Any]:
|
|
173
|
+
"""
|
|
174
|
+
Solve the surface ocean carbonate chemistry system given Total-scale pH,
|
|
175
|
+
temperature (C), salinity (PSU), and Total Alkalinity (TA, umol/kg).
|
|
176
|
+
|
|
177
|
+
Uses Mehrbach et al. (1973) refit by Dickson & Millero (1987) for K1 and K2,
|
|
178
|
+
Weiss (1974) for CO2 solubility K0, and Mucci (1983) for Ksp(aragonite/calcite).
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
dict
|
|
183
|
+
{'pCO2_uatm', 'dic_umol_kg', 'hco3_umol_kg', 'co3_umol_kg',
|
|
184
|
+
'omega_aragonite', 'omega_calcite', 'ecological_status'}
|
|
185
|
+
"""
|
|
186
|
+
T_k = float(temp_c) + 273.15
|
|
187
|
+
S = float(salinity_psu)
|
|
188
|
+
ph = float(ph_total)
|
|
189
|
+
ta = float(ta_umol_kg) * 1e-6 # mol/kg
|
|
190
|
+
|
|
191
|
+
# Weiss (1974) CO2 solubility K0 (mol / (kg * atm))
|
|
192
|
+
ln_K0 = (
|
|
193
|
+
-60.2409
|
|
194
|
+
+ 93.4517 * (100.0 / T_k)
|
|
195
|
+
+ 23.3585 * math.log(T_k / 100.0)
|
|
196
|
+
+ S * (0.023517 - 0.023656 * (T_k / 100.0) + 0.0047036 * ((T_k / 100.0) ** 2))
|
|
197
|
+
)
|
|
198
|
+
K0 = math.exp(ln_K0)
|
|
199
|
+
|
|
200
|
+
# Lueker et al. (2000) / Dickson & Millero (1987) pK1 and pK2
|
|
201
|
+
pK1 = 3633.86 / T_k - 61.2172 + 9.67770 * math.log(T_k) - 0.011555 * S + 0.0001152 * (S ** 2)
|
|
202
|
+
pK2 = 471.78 / T_k + 25.9290 - 3.16967 * math.log(T_k) - 0.01781 * S + 0.0001122 * (S ** 2)
|
|
203
|
+
K1 = 10.0 ** (-pK1)
|
|
204
|
+
K2 = 10.0 ** (-pK2)
|
|
205
|
+
|
|
206
|
+
# Borate alkalinity correction (Dickson, 1990)
|
|
207
|
+
B_total = 0.0004157 * (S / 35.0)
|
|
208
|
+
ln_KB = (
|
|
209
|
+
(-8966.90 - 2890.53 * (S ** 0.5) - 77.942 * S + 1.728 * (S ** 1.5) - 0.0996 * (S ** 2)) / T_k
|
|
210
|
+
+ (148.0248 + 137.1942 * (S ** 0.5) + 1.62142 * S)
|
|
211
|
+
+ (-24.4344 - 25.085 * (S ** 0.5) - 0.2474 * S) * math.log(T_k)
|
|
212
|
+
+ 0.053105 * (S ** 0.5) * T_k
|
|
213
|
+
)
|
|
214
|
+
KB = math.exp(ln_KB)
|
|
215
|
+
|
|
216
|
+
H = 10.0 ** (-ph)
|
|
217
|
+
b_alk = B_total * KB / (H + KB)
|
|
218
|
+
carb_alk = max(ta - b_alk, 1e-6)
|
|
219
|
+
|
|
220
|
+
# Carbonate species (mol/kg)
|
|
221
|
+
co3 = carb_alk / (H / K2 + 2.0)
|
|
222
|
+
hco3 = (co3 * H) / K2
|
|
223
|
+
co2_aq = (hco3 * H) / K1
|
|
224
|
+
dic = co2_aq + hco3 + co3
|
|
225
|
+
pco2_uatm = (co2_aq / K0) * 1e6
|
|
226
|
+
|
|
227
|
+
# Calcium concentration [Ca2+] (Riley & Tongudai, 1967) and Mucci (1983) Ksp
|
|
228
|
+
ca_mol_kg = 0.01028 * (S / 35.0)
|
|
229
|
+
log_Ksp_arag = (
|
|
230
|
+
-171.945
|
|
231
|
+
- 0.077993 * T_k
|
|
232
|
+
+ 2903.293 / T_k
|
|
233
|
+
+ 71.595 * math.log10(T_k)
|
|
234
|
+
+ (-0.068393 + 0.0017276 * T_k + 88.135 / T_k) * (S ** 0.5)
|
|
235
|
+
- 0.10018 * S
|
|
236
|
+
+ 0.0059415 * (S ** 1.5)
|
|
237
|
+
)
|
|
238
|
+
log_Ksp_calc = (
|
|
239
|
+
-171.9065
|
|
240
|
+
- 0.077993 * T_k
|
|
241
|
+
+ 2839.319 / T_k
|
|
242
|
+
+ 71.595 * math.log10(T_k)
|
|
243
|
+
+ (-0.77712 + 0.0028426 * T_k + 178.34 / T_k) * (S ** 0.5)
|
|
244
|
+
- 0.07711 * S
|
|
245
|
+
+ 0.0041249 * (S ** 1.5)
|
|
246
|
+
)
|
|
247
|
+
omega_arag = (ca_mol_kg * co3) / (10.0 ** log_Ksp_arag)
|
|
248
|
+
omega_calc = (ca_mol_kg * co3) / (10.0 ** log_Ksp_calc)
|
|
249
|
+
|
|
250
|
+
if omega_arag < 1.0:
|
|
251
|
+
status = "corrosive_undersaturated (Omega_arag < 1.0)"
|
|
252
|
+
elif omega_arag < 3.0:
|
|
253
|
+
status = "marginal_calcification_stress (1.0 <= Omega_arag < 3.0)"
|
|
254
|
+
else:
|
|
255
|
+
status = "optimal_calcification (Omega_arag >= 3.0)"
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
"pCO2_uatm": round(pco2_uatm, 2),
|
|
259
|
+
"dic_umol_kg": round(dic * 1e6, 2),
|
|
260
|
+
"hco3_umol_kg": round(hco3 * 1e6, 2),
|
|
261
|
+
"co3_umol_kg": round(co3 * 1e6, 2),
|
|
262
|
+
"omega_aragonite": round(omega_arag, 3),
|
|
263
|
+
"omega_calcite": round(omega_calc, 3),
|
|
264
|
+
"ecological_status": status,
|
|
265
|
+
"calcification_regime": status,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def marine_heatwave_detect(
|
|
270
|
+
sst_series: Sequence[float],
|
|
271
|
+
dates_or_steps: Optional[Sequence[Any]] = None,
|
|
272
|
+
pct_threshold: float = 90.0,
|
|
273
|
+
min_duration: int = 5,
|
|
274
|
+
max_gap: int = 2,
|
|
275
|
+
) -> Dict[str, Any]:
|
|
276
|
+
"""
|
|
277
|
+
Detect and categorize Marine Heatwave (MHW) events following the hierarchical
|
|
278
|
+
framework of Hobday et al. (2016, Progress in Oceanography) and Hobday et al.
|
|
279
|
+
(2018, Oceanography, Categories I–IV: Moderate, Strong, Severe, Extreme).
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
sst_series : Sequence[float]
|
|
284
|
+
Chronological sea surface temperature observations.
|
|
285
|
+
dates_or_steps : Sequence[Any], optional
|
|
286
|
+
Corresponding date labels or time indices.
|
|
287
|
+
pct_threshold : float, default 90.0
|
|
288
|
+
Percentile threshold for MHW exceedance (default 90th percentile).
|
|
289
|
+
min_duration : int, default 5
|
|
290
|
+
Minimum consecutive steps above threshold to qualify as an MHW event.
|
|
291
|
+
max_gap : int, default 2
|
|
292
|
+
Maximum gap (in steps) between two consecutive exceedances to merge them.
|
|
293
|
+
"""
|
|
294
|
+
vals = [float(v) for v in sst_series if v is not None]
|
|
295
|
+
n = len(vals)
|
|
296
|
+
if n == 0:
|
|
297
|
+
return {"events": [], "event_count": 0}
|
|
298
|
+
|
|
299
|
+
labels = list(dates_or_steps) if dates_or_steps is not None and len(dates_or_steps) == n else list(range(n))
|
|
300
|
+
sorted_vals = sorted(vals)
|
|
301
|
+
clim_mean = sum(vals) / n
|
|
302
|
+
k_idx = min(max(int(math.floor((pct_threshold / 100.0) * (n - 1))), 0), n - 1)
|
|
303
|
+
thresh_val = sorted_vals[k_idx]
|
|
304
|
+
delta_unit = max(thresh_val - clim_mean, 0.25)
|
|
305
|
+
|
|
306
|
+
# Identify contiguous exceedance spans
|
|
307
|
+
raw_spans: List[Tuple[int, int]] = []
|
|
308
|
+
start: Optional[int] = None
|
|
309
|
+
for i, val in enumerate(vals):
|
|
310
|
+
if val > thresh_val:
|
|
311
|
+
if start is None:
|
|
312
|
+
start = i
|
|
313
|
+
else:
|
|
314
|
+
if start is not None:
|
|
315
|
+
if (i - start) >= min_duration:
|
|
316
|
+
raw_spans.append((start, i - 1))
|
|
317
|
+
start = None
|
|
318
|
+
if start is not None and (n - start) >= min_duration:
|
|
319
|
+
raw_spans.append((start, n - 1))
|
|
320
|
+
|
|
321
|
+
# Merge spans separated by <= max_gap
|
|
322
|
+
merged_spans: List[Tuple[int, int]] = []
|
|
323
|
+
for sp in raw_spans:
|
|
324
|
+
if merged_spans and (sp[0] - merged_spans[-1][1] - 1) <= max_gap:
|
|
325
|
+
merged_spans[-1] = (merged_spans[-1][0], sp[1])
|
|
326
|
+
else:
|
|
327
|
+
merged_spans.append(sp)
|
|
328
|
+
|
|
329
|
+
events: List[Dict[str, Any]] = []
|
|
330
|
+
for idx, (s_i, e_i) in enumerate(merged_spans, 1):
|
|
331
|
+
segment = vals[s_i : e_i + 1]
|
|
332
|
+
anomalies = [v - clim_mean for v in segment]
|
|
333
|
+
max_anom = max(anomalies)
|
|
334
|
+
mean_anom = sum(anomalies) / len(anomalies)
|
|
335
|
+
cum_anom = sum(anomalies)
|
|
336
|
+
severity_mult = max_anom / delta_unit
|
|
337
|
+
if severity_mult >= 4.0:
|
|
338
|
+
category = "IV (Extreme)"
|
|
339
|
+
elif severity_mult >= 3.0:
|
|
340
|
+
category = "III (Severe)"
|
|
341
|
+
elif severity_mult >= 2.0:
|
|
342
|
+
category = "II (Strong)"
|
|
343
|
+
else:
|
|
344
|
+
category = "I (Moderate)"
|
|
345
|
+
|
|
346
|
+
events.append(
|
|
347
|
+
{
|
|
348
|
+
"event_id": idx,
|
|
349
|
+
"start_step": labels[s_i],
|
|
350
|
+
"end_step": labels[e_i],
|
|
351
|
+
"duration_steps": e_i - s_i + 1,
|
|
352
|
+
"max_sst_c": round(max(segment), 3),
|
|
353
|
+
"max_intensity_c": round(max_anom, 3),
|
|
354
|
+
"mean_intensity_c": round(mean_anom, 3),
|
|
355
|
+
"cumulative_intensity_c_steps": round(cum_anom, 3),
|
|
356
|
+
"hobday_category": category,
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
"climatology_mean_c": round(clim_mean, 3),
|
|
362
|
+
"threshold_pct": pct_threshold,
|
|
363
|
+
"threshold_sst_c": round(thresh_val, 3),
|
|
364
|
+
"event_count": len(events),
|
|
365
|
+
"events": events,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def trophic_state_index(
|
|
370
|
+
chl_a_ug_l: float,
|
|
371
|
+
din_ug_l: Optional[float] = None,
|
|
372
|
+
tp_ug_l: Optional[float] = None,
|
|
373
|
+
do_saturation_pct: Optional[float] = None,
|
|
374
|
+
secchi_m: Optional[float] = None,
|
|
375
|
+
) -> Dict[str, Any]:
|
|
376
|
+
"""
|
|
377
|
+
Compute Carlson (1977) Trophic State Index (TSI) and Vollenweider et al. (1998)
|
|
378
|
+
TRIX Coastal Eutrophication Index (used by UN SDG 14.1.1 & UNEP/MAP MedPol).
|
|
379
|
+
|
|
380
|
+
Formula (Vollenweider et al., 1998):
|
|
381
|
+
TRIX = (log10(Chl-a * |100 - %DO| * DIN * TP) + 1.5) / 1.2
|
|
382
|
+
"""
|
|
383
|
+
chla = max(float(chl_a_ug_l), 0.01)
|
|
384
|
+
tsi_chla = 9.81 * math.log(chla) + 30.6
|
|
385
|
+
tsi_sd = 60.0 - 14.41 * math.log(max(float(secchi_m), 0.05)) if secchi_m is not None else None
|
|
386
|
+
tsi_tp = 14.42 * math.log(max(float(tp_ug_l), 0.1)) + 4.15 if tp_ug_l is not None else None
|
|
387
|
+
|
|
388
|
+
trix_val = None
|
|
389
|
+
trix_status = None
|
|
390
|
+
if din_ug_l is not None and tp_ug_l is not None and do_saturation_pct is not None:
|
|
391
|
+
dev_o2 = max(abs(100.0 - float(do_saturation_pct)), 1.0)
|
|
392
|
+
prod = max(chla * dev_o2 * max(float(din_ug_l), 0.1) * max(float(tp_ug_l), 0.1), 1e-6)
|
|
393
|
+
trix_val = round((math.log10(prod) + 1.5) / 1.2, 3)
|
|
394
|
+
if trix_val < 4.0:
|
|
395
|
+
trix_status = "oligotrophic_high_quality (TRIX < 4)"
|
|
396
|
+
elif trix_val < 5.0:
|
|
397
|
+
trix_status = "mesotrophic_good (4 <= TRIX < 5)"
|
|
398
|
+
elif trix_val < 6.0:
|
|
399
|
+
trix_status = "eutrophic_poor (5 <= TRIX < 6)"
|
|
400
|
+
else:
|
|
401
|
+
trix_status = "hypertrophic_degraded (TRIX >= 6)"
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
"carlson_tsi_chla": round(tsi_chla, 2),
|
|
405
|
+
"carlson_tsi_secchi": round(tsi_sd, 2) if tsi_sd is not None else None,
|
|
406
|
+
"carlson_tsi_tp": round(tsi_tp, 2) if tsi_tp is not None else None,
|
|
407
|
+
"vollenweider_trix": trix_val,
|
|
408
|
+
"trix_water_quality": trix_status,
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def mucilage_risk_index(
|
|
413
|
+
sst_anomaly_c: float,
|
|
414
|
+
n_p_molar_ratio: float,
|
|
415
|
+
wind_speed_ms: float,
|
|
416
|
+
residence_time_days: float = 30.0,
|
|
417
|
+
chl_a_ug_l: float = 2.5,
|
|
418
|
+
) -> Dict[str, Any]:
|
|
419
|
+
"""
|
|
420
|
+
Calculate the composite Marine Mucilage Hazard Index (0-100) calibrated for
|
|
421
|
+
stratified semi-enclosed basins (e.g., Sea of Marmara, Northern Adriatic).
|
|
422
|
+
|
|
423
|
+
Captures the 4 drivers of phytoplankton extracellular polymeric substance (EPS)
|
|
424
|
+
hyper-production and aggregation:
|
|
425
|
+
1. Thermal stratification & positive SST anomaly
|
|
426
|
+
2. Nutrient stoichiometry imbalance (elevated N:P Redfield deviation > 16:1)
|
|
427
|
+
3. Low wind mixing / water-column stagnation
|
|
428
|
+
4. Phytoplankton biomass precursor (Chl-a)
|
|
429
|
+
"""
|
|
430
|
+
thermal_score = min(max((float(sst_anomaly_c) + 0.5) / 3.5, 0.0), 1.0) * 30.0
|
|
431
|
+
np_dev = abs(float(n_p_molar_ratio) - 16.0)
|
|
432
|
+
nutrient_score = min(np_dev / 24.0, 1.0) * 25.0
|
|
433
|
+
stagnation_score = (
|
|
434
|
+
min(max((8.0 - float(wind_speed_ms)) / 7.0, 0.0), 1.0) * 15.0
|
|
435
|
+
+ min(float(residence_time_days) / 60.0, 1.0) * 10.0
|
|
436
|
+
)
|
|
437
|
+
biomass_score = min(math.log1p(max(float(chl_a_ug_l), 0.0)) / math.log1p(15.0), 1.0) * 20.0
|
|
438
|
+
|
|
439
|
+
total = round(thermal_score + nutrient_score + stagnation_score + biomass_score, 2)
|
|
440
|
+
if total >= 75.0:
|
|
441
|
+
level = "CRITICAL (Imminent Mucilage Aggregation Risk)"
|
|
442
|
+
elif total >= 50.0:
|
|
443
|
+
level = "HIGH (Elevated EPS & Stratification Alert)"
|
|
444
|
+
elif total >= 25.0:
|
|
445
|
+
level = "MODERATE (Seasonal Monitoring Required)"
|
|
446
|
+
else:
|
|
447
|
+
level = "LOW (Well-Mixed / Baseline Conditions)"
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
"mucilage_risk_score": total,
|
|
451
|
+
"mucilage_risk_index_0_100": total,
|
|
452
|
+
"risk_level": level,
|
|
453
|
+
"alert_level": level,
|
|
454
|
+
"components": {
|
|
455
|
+
"thermal_stratification": round(thermal_score, 2),
|
|
456
|
+
"nutrient_stoichiometry": round(nutrient_score, 2),
|
|
457
|
+
"hydrodynamic_stagnation": round(stagnation_score, 2),
|
|
458
|
+
"phytoplankton_biomass": round(biomass_score, 2),
|
|
459
|
+
},
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def compound_mhw_oax_event(
|
|
464
|
+
sst_series: Sequence[float],
|
|
465
|
+
ph_series: Sequence[float],
|
|
466
|
+
do_mg_l_series: Sequence[float],
|
|
467
|
+
labels: Optional[Sequence[Any]] = None,
|
|
468
|
+
sst_pct: float = 85.0,
|
|
469
|
+
ph_pct: float = 15.0,
|
|
470
|
+
do_pct: float = 15.0,
|
|
471
|
+
sst_percentile: Optional[float] = None,
|
|
472
|
+
ph_percentile: Optional[float] = None,
|
|
473
|
+
do_percentile: Optional[float] = None,
|
|
474
|
+
**kwargs: Any,
|
|
475
|
+
) -> Dict[str, Any]:
|
|
476
|
+
"""
|
|
477
|
+
Detect Triple-Threat Compound Marine Extremes (MHW + Ocean Acidification Extreme [OAX]
|
|
478
|
+
+ Low-Oxygen Extreme [LOX]) and compute the Metabolic Habitat Compression / Aerobic
|
|
479
|
+
Growth Index (AGI).
|
|
480
|
+
|
|
481
|
+
Post-2020 Scientific References:
|
|
482
|
+
- Gruber, N., Boyd, P. W., Frölicher, T. L., & Vogt, M. (2021). Biogeochemical
|
|
483
|
+
extremes and compound events in the ocean. Science, 374(6566), eabc9852.
|
|
484
|
+
- Burger, F. A., Terhaar, J., & Frölicher, T. L. (2022). Compound marine heatwaves
|
|
485
|
+
and ocean acidity extremes. Nature, 608, 327-332.
|
|
486
|
+
- Clarke, T. M., Wabnitz, C. C., Striegel, S., Frölicher, T. L., Reygondeau, G.,
|
|
487
|
+
& Cheung, W. W. (2021). Aerobic growth index (AGI): An index to understand the
|
|
488
|
+
impacts of ocean warming and deoxygenation on marine ectotherms. Progress in
|
|
489
|
+
Oceanography / Science Advances.
|
|
490
|
+
"""
|
|
491
|
+
if sst_percentile is not None:
|
|
492
|
+
sst_pct = float(sst_percentile)
|
|
493
|
+
if ph_percentile is not None:
|
|
494
|
+
ph_pct = float(ph_percentile)
|
|
495
|
+
if do_percentile is not None:
|
|
496
|
+
do_pct = float(do_percentile)
|
|
497
|
+
|
|
498
|
+
sst = [float(v) for v in sst_series]
|
|
499
|
+
ph = [float(v) for v in ph_series]
|
|
500
|
+
do_vals = [float(v) for v in do_mg_l_series]
|
|
501
|
+
n = min(len(sst), len(ph), len(do_vals))
|
|
502
|
+
if n < 4:
|
|
503
|
+
raise ValueError("compound_mhw_oax_event requires at least 4 paired observations.")
|
|
504
|
+
|
|
505
|
+
lbls = [str(labels[i]) for i in range(n)] if labels is not None else [str(i + 1) for i in range(n)]
|
|
506
|
+
# Convert pH to hydrogen ion concentration [H+] in nmol/kg for linear extreme quantile evaluation
|
|
507
|
+
h_nmol = [(10.0 ** (-ph[i])) * 1e9 for i in range(n)]
|
|
508
|
+
|
|
509
|
+
def _pct(arr: List[float], q: float) -> float:
|
|
510
|
+
s = sorted(arr)
|
|
511
|
+
pos = (q / 100.0) * (len(s) - 1)
|
|
512
|
+
lo = int(math.floor(pos))
|
|
513
|
+
hi = int(math.ceil(pos))
|
|
514
|
+
return s[lo] + (pos - lo) * (s[hi] - s[lo])
|
|
515
|
+
|
|
516
|
+
sst_thresh = _pct(sst[:n], sst_pct)
|
|
517
|
+
h_thresh = _pct(h_nmol, 100.0 - ph_pct) # High [H+] = low pH extreme
|
|
518
|
+
do_thresh = _pct(do_vals[:n], do_pct)
|
|
519
|
+
|
|
520
|
+
steps_out: List[Dict[str, Any]] = []
|
|
521
|
+
double_count = 0
|
|
522
|
+
triple_count = 0
|
|
523
|
+
agi_values: List[float] = []
|
|
524
|
+
|
|
525
|
+
for i in range(n):
|
|
526
|
+
is_mhw = sst[i] >= sst_thresh
|
|
527
|
+
is_oax = h_nmol[i] >= h_thresh
|
|
528
|
+
is_lox = do_vals[i] <= do_thresh
|
|
529
|
+
multiplicity = int(is_mhw) + int(is_oax) + int(is_lox)
|
|
530
|
+
if multiplicity == 2:
|
|
531
|
+
double_count += 1
|
|
532
|
+
elif multiplicity == 3:
|
|
533
|
+
triple_count += 1
|
|
534
|
+
|
|
535
|
+
# Clarke et al. (2021) Aerobic Growth Index (AGI = pO2_supply / pO2_demand_maintenance)
|
|
536
|
+
# Metabolic O2 demand increases exponentially with temperature via Arrhenius factor exp(-j / T_K)
|
|
537
|
+
t_k = sst[i] + 273.15
|
|
538
|
+
metabolic_demand = math.exp(18.2 - 5400.0 / t_k)
|
|
539
|
+
o2_supply = max(do_vals[i], 0.05) / 8.0
|
|
540
|
+
agi = o2_supply / max(metabolic_demand, 0.05)
|
|
541
|
+
agi_values.append(agi)
|
|
542
|
+
|
|
543
|
+
if multiplicity >= 2:
|
|
544
|
+
steps_out.append(
|
|
545
|
+
{
|
|
546
|
+
"step": lbls[i],
|
|
547
|
+
"sst_c": round(sst[i], 3),
|
|
548
|
+
"ph_total": round(ph[i], 4),
|
|
549
|
+
"do_mg_l": round(do_vals[i], 3),
|
|
550
|
+
"mhw_active": is_mhw,
|
|
551
|
+
"oax_active": is_oax,
|
|
552
|
+
"lox_active": is_lox,
|
|
553
|
+
"multiplicity": multiplicity,
|
|
554
|
+
"compound_class": "Triple Compound (MHW+OAX+LOX)" if multiplicity == 3 else "Dual Compound Extreme",
|
|
555
|
+
"aerobic_growth_index_agi": round(agi, 3),
|
|
556
|
+
}
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# Expected independent triple probability vs observed empirical frequency
|
|
560
|
+
p_ind_triple = ((100.0 - sst_pct) / 100.0) * (ph_pct / 100.0) * (do_pct / 100.0)
|
|
561
|
+
p_obs_triple = triple_count / float(n)
|
|
562
|
+
likelihood_mult = (p_obs_triple / p_ind_triple) if p_ind_triple > 0 else 0.0
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
"n_steps": n,
|
|
566
|
+
"thresholds": {
|
|
567
|
+
"sst_c_high": round(sst_thresh, 3),
|
|
568
|
+
"h_plus_nmol_kg_high": round(h_thresh, 3),
|
|
569
|
+
"do_mg_l_low": round(do_thresh, 3),
|
|
570
|
+
},
|
|
571
|
+
"dual_compound_steps": double_count,
|
|
572
|
+
"dual_compound_days": double_count,
|
|
573
|
+
"triple_compound_steps": triple_count,
|
|
574
|
+
"triple_compound_days": triple_count,
|
|
575
|
+
"compound_frequency_pct": round(((double_count + triple_count) / float(n)) * 100.0, 2),
|
|
576
|
+
"triple_likelihood_multiplier": round(likelihood_mult, 2),
|
|
577
|
+
"mean_aerobic_growth_index": round(sum(agi_values) / len(agi_values), 3),
|
|
578
|
+
"critical_habitat_compression_steps": sum(1 for a in agi_values if a < 1.0),
|
|
579
|
+
"compound_events": steps_out,
|
|
580
|
+
"reference": "Gruber et al. (Science, 2021); Burger et al. (Nature, 2022); Clarke et al. (2021)",
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def teos10_gsw_stratification(
|
|
585
|
+
salinity_psu: float,
|
|
586
|
+
temp_c: float,
|
|
587
|
+
pressure_dbar: float = 100.0,
|
|
588
|
+
lat: float = 36.0,
|
|
589
|
+
surface_salinity_psu: Optional[float] = None,
|
|
590
|
+
surface_temp_c: Optional[float] = None,
|
|
591
|
+
) -> Dict[str, Any]:
|
|
592
|
+
"""
|
|
593
|
+
Compute TEOS-10 Gibbs Seawater (IOC-SCOR-IAPSO) Absolute Salinity (S_A, g/kg),
|
|
594
|
+
Conservative Temperature (Theta, deg C), Brunt-Vaisala Buoyancy Frequency Squared
|
|
595
|
+
(N^2, s^-2), and Sallée et al. (Nature, 2021) Pycnocline Stratification Barrier Index.
|
|
596
|
+
|
|
597
|
+
Post-2020 Scientific References:
|
|
598
|
+
- Sallée, J.-B., Pellichero, V., Akhoudas, C., Pauthenet, E., Vignes, L.,
|
|
599
|
+
Schmidtko, S., ... & Kuusela, M. (2021). Summertime velocity of the upper-ocean
|
|
600
|
+
stratification over the past half-century. Nature, 591(7851), 592-598.
|
|
601
|
+
- IOC, SCOR and IAPSO (2010 / CMIP6 Standard). The international thermodynamic
|
|
602
|
+
equation of seawater - 2010 (TEOS-10).
|
|
603
|
+
"""
|
|
604
|
+
sp = float(salinity_psu)
|
|
605
|
+
t = float(temp_c)
|
|
606
|
+
p = max(float(pressure_dbar), 1.0)
|
|
607
|
+
# Reference Composition Salinity Scale factor (35.16504 / 35.0) + biogeochemical anomaly delta_SA
|
|
608
|
+
sa = sp * (35.16504 / 35.0) + 0.008 * (p / 1000.0)
|
|
609
|
+
# Potential temperature & TEOS-10 Conservative Temperature polynomial approximation
|
|
610
|
+
theta0 = t - 1.5e-4 * p * (1.0 + 0.02 * (t - 10.0))
|
|
611
|
+
ct = theta0 + 0.0045 * (35.16504 - sa) / 35.16504 * theta0
|
|
612
|
+
|
|
613
|
+
s_surf = float(surface_salinity_psu) if surface_salinity_psu is not None else max(sp - 0.35, 0.0)
|
|
614
|
+
t_surf = float(surface_temp_c) if surface_temp_c is not None else (t + 4.5)
|
|
615
|
+
|
|
616
|
+
rho_deep = seawater_density_eos80(sp, t, p)["density_kg_m3"]
|
|
617
|
+
rho_surf = seawater_density_eos80(s_surf, t_surf, 0.0)["density_kg_m3"]
|
|
618
|
+
sigma_diff = max(rho_deep - rho_surf, 0.0)
|
|
619
|
+
|
|
620
|
+
# Local gravitational acceleration g(lat)
|
|
621
|
+
rad = math.radians(float(lat))
|
|
622
|
+
g_lat = 9.780327 * (1.0 + 5.3024e-3 * (math.sin(rad) ** 2))
|
|
623
|
+
dz = max(p, 1.0) # 1 dbar approx 1 m
|
|
624
|
+
n2 = (g_lat / rho_deep) * (sigma_diff / dz)
|
|
625
|
+
cph = (math.sqrt(max(n2, 0.0)) * 3600.0) / (2.0 * math.pi)
|
|
626
|
+
|
|
627
|
+
if n2 >= 4.0e-4:
|
|
628
|
+
barrier = "STRONG PYCNOCLINE BARRIER (Severe Vertical Ventilation Suppression)"
|
|
629
|
+
elif n2 >= 1.0e-4:
|
|
630
|
+
barrier = "MODERATE STRATIFICATION (Seasonal Thermocline Active)"
|
|
631
|
+
else:
|
|
632
|
+
barrier = "WEAK / CONVECTIVELY MIXED WATER COLUMN"
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
"practical_salinity_sp": round(sp, 4),
|
|
636
|
+
"absolute_salinity_sa_g_kg": round(sa, 4),
|
|
637
|
+
"conservative_temperature_ct_c": round(ct, 4),
|
|
638
|
+
"delta_sigma_kg_m3": round(sigma_diff, 4),
|
|
639
|
+
"brunt_vaisala_n2_s2": round(n2, 7),
|
|
640
|
+
"buoyancy_frequency_cph": round(cph, 3),
|
|
641
|
+
"stratification_regime": barrier,
|
|
642
|
+
"reference": "TEOS-10 (IOC/SCOR/IAPSO) & Sallée et al. (Nature, 2021)",
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def blue_carbon_sequestration_valuation(
|
|
647
|
+
seagrass_ha: float = 0.0,
|
|
648
|
+
saltmarsh_ha: float = 0.0,
|
|
649
|
+
mangrove_ha: float = 0.0,
|
|
650
|
+
degradation_risk_pct: float = 10.0,
|
|
651
|
+
horizon_years: int = 30,
|
|
652
|
+
discount_rate: float = 0.02,
|
|
653
|
+
scc_usd_per_tco2: float = 185.0,
|
|
654
|
+
vcm_price_usd: float = 28.5,
|
|
655
|
+
) -> Dict[str, Any]:
|
|
656
|
+
"""
|
|
657
|
+
Quantify Coastal Blue Carbon Sequestration, Sedimentary Carbon Stock Vulnerability,
|
|
658
|
+
Social Cost of Carbon (SCC) Natural Capital Wealth, and Paris Agreement Article 6
|
|
659
|
+
Voluntary Carbon Market (VCM) Revenue.
|
|
660
|
+
|
|
661
|
+
Post-2020 Scientific References:
|
|
662
|
+
- Bertram, C., Quaas, M., Reusch, T. B., Vafeidis, A. T., Wolff, C., & Rickels, W.
|
|
663
|
+
(2021). The blue carbon wealth of nations. Nature Climate Change, 11(8), 704-709.
|
|
664
|
+
- Macreadie, P. I., Costa, M. D., Atwood, T. B., Friess, D. A., Kelleway, J. J.,
|
|
665
|
+
Kennedy, H., ... & Duarte, C. M. (2021). Blue carbon as a natural climate
|
|
666
|
+
solution. Nature Reviews Earth & Environment, 2(12), 826-839.
|
|
667
|
+
- Rennert, K., Errickson, F., Prest, B. C., Rennels, L., Newell, R. G., Pizer, W.,
|
|
668
|
+
... & Anthoff, D. (2022). Comprehensive evidence implies a higher social cost
|
|
669
|
+
of CO2 ($185/tCO2). Nature, 610(7933), 687-692.
|
|
670
|
+
"""
|
|
671
|
+
# Annual burial rates (tCO2e / ha / yr) & top-1m soil carbon stock (tCO2e / ha)
|
|
672
|
+
# from Macreadie et al. (2021) & IPCC Wetlands Supplement
|
|
673
|
+
rates = {
|
|
674
|
+
"seagrass_posidonia": {"seq_tco2_ha_yr": 5.06, "stock_tco2_ha": 513.0},
|
|
675
|
+
"saltmarsh": {"seq_tco2_ha_yr": 7.99, "stock_tco2_ha": 917.0},
|
|
676
|
+
"mangrove": {"seq_tco2_ha_yr": 8.29, "stock_tco2_ha": 1415.0},
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
sg = max(float(seagrass_ha), 0.0)
|
|
680
|
+
sm = max(float(saltmarsh_ha), 0.0)
|
|
681
|
+
mg = max(float(mangrove_ha), 0.0)
|
|
682
|
+
|
|
683
|
+
annual_seq = (
|
|
684
|
+
sg * rates["seagrass_posidonia"]["seq_tco2_ha_yr"]
|
|
685
|
+
+ sm * rates["saltmarsh"]["seq_tco2_ha_yr"]
|
|
686
|
+
+ mg * rates["mangrove"]["seq_tco2_ha_yr"]
|
|
687
|
+
)
|
|
688
|
+
total_stock = (
|
|
689
|
+
sg * rates["seagrass_posidonia"]["stock_tco2_ha"]
|
|
690
|
+
+ sm * rates["saltmarsh"]["stock_tco2_ha"]
|
|
691
|
+
+ mg * rates["mangrove"]["stock_tco2_ha"]
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# Avoided emissions from protecting top-1m sediment stock against remineralization
|
|
695
|
+
deg_frac = min(max(float(degradation_risk_pct) / 100.0, 0.0), 1.0)
|
|
696
|
+
avoided_stock_loss = total_stock * deg_frac * 0.50 # 50% oxidized upon habitat destruction
|
|
697
|
+
|
|
698
|
+
# Discounted Present Value of sequestration stream over horizon_years (Ramsey r = 2.0% per Rennert et al. 2022)
|
|
699
|
+
r = max(float(discount_rate), 0.0001)
|
|
700
|
+
pv_factor = sum(1.0 / ((1.0 + r) ** t) for t in range(1, int(horizon_years) + 1))
|
|
701
|
+
cum_seq_tco2 = annual_seq * int(horizon_years) + avoided_stock_loss
|
|
702
|
+
npv_scc_usd = (annual_seq * pv_factor + avoided_stock_loss) * float(scc_usd_per_tco2)
|
|
703
|
+
annual_vcm_usd = annual_seq * float(vcm_price_usd)
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
"total_habitat_area_ha": round(sg + sm + mg, 2),
|
|
707
|
+
"annual_sequestration_tco2e_yr": round(annual_seq, 2),
|
|
708
|
+
"top_1m_sediment_stock_tco2e": round(total_stock, 2),
|
|
709
|
+
"avoided_remineralization_tco2e": round(avoided_stock_loss, 2),
|
|
710
|
+
"cumulative_mitigation_tco2e": round(cum_seq_tco2, 2),
|
|
711
|
+
"natural_capital_npv_scc_usd": round(npv_scc_usd, 2),
|
|
712
|
+
"annual_article6_vcm_revenue_usd": round(annual_vcm_usd, 2),
|
|
713
|
+
"scc_benchmark_usd_tco2": float(scc_usd_per_tco2),
|
|
714
|
+
"reference": "Bertram et al. (Nat. Clim. Change, 2021); Macreadie et al. (2021); Rennert et al. (Nature, 2022)",
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def sustainable_insurance_cat_var(
|
|
719
|
+
coastal_exposed_value_usd: float = 500_000_000.0,
|
|
720
|
+
reef_mangrove_seagrass_ha: float = 1200.0,
|
|
721
|
+
observed_wave_hs_m: float = 5.4,
|
|
722
|
+
parametric_trigger_hs_m: float = 4.8,
|
|
723
|
+
storm_return_period_yr: float = 50.0,
|
|
724
|
+
annual_restoration_cost_usd: float = 1_800_000.0,
|
|
725
|
+
) -> Dict[str, Any]:
|
|
726
|
+
"""
|
|
727
|
+
UNEP FI Principles for Sustainable Insurance (PSI) & Parametric Coastal Ecosystem
|
|
728
|
+
Catastrophe Risk-Transfer Model (Coral Reef, Mangrove & Seagrass Wave Attenuation).
|
|
729
|
+
|
|
730
|
+
Post-2020 Scientific & Actuarial References:
|
|
731
|
+
- Reguero, B. G., Beck, M. W., Schmid, D., Stadmueller, D., Raepple, J., Schüssele, S.,
|
|
732
|
+
& Pfliegner, K. (2020). Financing coastal resilience by combining nature-based risk
|
|
733
|
+
reduction with insurance. Ecological Economics / Nature Communications Framework.
|
|
734
|
+
- Beck, M. W., Heck, N., Narayan, S., Menéndez, P., Torres-Ortega, S., Losada, I. J.,
|
|
735
|
+
... & Reguero, B. G. (2022). Return on investment for mangrove and reef flood
|
|
736
|
+
protection. Nature Sustainability, 5(7), 618-626.
|
|
737
|
+
- UNEP Finance Initiative (UNEP FI, 2023). Principles for Sustainable Insurance (PSI):
|
|
738
|
+
Insuring Nature-Positive & Climate-Resilient Coastal Infrastructure.
|
|
739
|
+
"""
|
|
740
|
+
exp_val = max(float(coastal_exposed_value_usd), 100_000.0)
|
|
741
|
+
hab_ha = max(float(reef_mangrove_seagrass_ha), 0.0)
|
|
742
|
+
hs_obs = max(float(observed_wave_hs_m), 0.1)
|
|
743
|
+
hs_trig = max(float(parametric_trigger_hs_m), 0.5)
|
|
744
|
+
ret_yr = max(float(storm_return_period_yr), 2.0)
|
|
745
|
+
rest_cost = max(float(annual_restoration_cost_usd), 1.0)
|
|
746
|
+
|
|
747
|
+
# Baseline Expected Annual Damage (EAD) without nature-based coastal protection (~0.85% of exposed value)
|
|
748
|
+
ead_unprotected_usd = exp_val * 0.0085
|
|
749
|
+
|
|
750
|
+
# Wave energy attenuation from coral reefs / mangroves / Posidonia seagrass (up to 62% flood damage reduction per Beck et al. 2022)
|
|
751
|
+
attenuation_frac = min(0.62, 0.62 * (1.0 - math.exp(-hab_ha / 1500.0)))
|
|
752
|
+
avoided_annual_loss_usd = ead_unprotected_usd * attenuation_frac
|
|
753
|
+
ead_protected_usd = ead_unprotected_usd - avoided_annual_loss_usd
|
|
754
|
+
|
|
755
|
+
# Probable Maximum Loss at 100-year and 250-year return periods (PML_100, PML_250)
|
|
756
|
+
pml_100_usd = exp_val * 0.095 * (1.0 - 0.75 * attenuation_frac)
|
|
757
|
+
pml_250_usd = exp_val * 0.165 * (1.0 - 0.70 * attenuation_frac)
|
|
758
|
+
|
|
759
|
+
# Actuarial technical premium (Pure Risk Premium + 28% risk/capital loading)
|
|
760
|
+
technical_premium_unprotected_usd = ead_unprotected_usd * 1.28
|
|
761
|
+
technical_premium_protected_usd = ead_protected_usd * 1.28
|
|
762
|
+
annual_premium_savings_usd = technical_premium_unprotected_usd - technical_premium_protected_usd
|
|
763
|
+
|
|
764
|
+
# Parametric payout trigger evaluation (stepped payout based on Hs exceedance above trigger)
|
|
765
|
+
triggered = hs_obs >= hs_trig
|
|
766
|
+
if triggered:
|
|
767
|
+
exceed_ratio = min((hs_obs - hs_trig) / max(hs_trig * 0.6, 0.5), 1.0)
|
|
768
|
+
payout_tier_pct = min(100.0, round(40.0 + 60.0 * exceed_ratio, 1))
|
|
769
|
+
parametric_payout_usd = pml_100_usd * (payout_tier_pct / 100.0)
|
|
770
|
+
else:
|
|
771
|
+
payout_tier_pct = 0.0
|
|
772
|
+
parametric_payout_usd = 0.0
|
|
773
|
+
|
|
774
|
+
resilience_bcr = (avoided_annual_loss_usd + annual_premium_savings_usd * 0.5) / rest_cost
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
"coastal_exposed_value_usd": round(exp_val, 2),
|
|
778
|
+
"wave_attenuation_reduction_pct": round(attenuation_frac * 100.0, 2),
|
|
779
|
+
"ead_unprotected_usd": round(ead_unprotected_usd, 2),
|
|
780
|
+
"ead_nature_protected_usd": round(ead_protected_usd, 2),
|
|
781
|
+
"avoided_annual_damage_usd": round(avoided_annual_loss_usd, 2),
|
|
782
|
+
"pml_100yr_usd": round(pml_100_usd, 2),
|
|
783
|
+
"pml_250yr_usd": round(pml_250_usd, 2),
|
|
784
|
+
"annual_insurance_premium_protected_usd": round(technical_premium_protected_usd, 2),
|
|
785
|
+
"annual_insurance_premium_savings_usd": round(annual_premium_savings_usd, 2),
|
|
786
|
+
"parametric_trigger_activated": triggered,
|
|
787
|
+
"parametric_payout_tier_pct": payout_tier_pct,
|
|
788
|
+
"parametric_payout_usd": round(parametric_payout_usd, 2),
|
|
789
|
+
"nature_resilience_benefit_cost_ratio": round(resilience_bcr, 2),
|
|
790
|
+
"reference": "UNEP FI PSI (2023); Reguero et al. (2020); Beck et al. (Nature Sustainability, 2022)",
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def cwon_wealth_accounting(
|
|
795
|
+
gni_usd: float = 1_150_000_000_000.0,
|
|
796
|
+
gross_national_savings_usd: float = 295_000_000_000.0,
|
|
797
|
+
consumption_fixed_capital_usd: float = 135_000_000_000.0,
|
|
798
|
+
education_expenditure_usd: float = 42_000_000_000.0,
|
|
799
|
+
energy_depletion_usd: float = 4_800_000_000.0,
|
|
800
|
+
mineral_depletion_usd: float = 2_900_000_000.0,
|
|
801
|
+
net_forest_depletion_usd: float = 650_000_000.0,
|
|
802
|
+
co2_damage_usd: float = 24_500_000_000.0,
|
|
803
|
+
pm25_damage_usd: float = 6_200_000_000.0,
|
|
804
|
+
blue_natural_capital_net_change_usd: float = 1_450_000_000.0,
|
|
805
|
+
) -> Dict[str, Any]:
|
|
806
|
+
"""
|
|
807
|
+
World Bank Changing Wealth of Nations (CWON 2021/2024), Dasgupta Review (2021) &
|
|
808
|
+
UN SEEA Ecosystem Accounting: Adjusted Net Savings (ANS / Genuine Savings) and
|
|
809
|
+
Blue-Adjusted Inclusive Wealth Accounting.
|
|
810
|
+
|
|
811
|
+
Formula:
|
|
812
|
+
NNS = GNS - CFC
|
|
813
|
+
ANS_CWON = NNS + Edu_Exp - (D_energy + D_mineral + D_forest) - (Damage_CO2 + Damage_PM2.5)
|
|
814
|
+
ANS_Blue_Inclusive = ANS_CWON + Delta_Blue_Natural_Capital
|
|
815
|
+
"""
|
|
816
|
+
gni = max(float(gni_usd), 1.0)
|
|
817
|
+
gns = float(gross_national_savings_usd)
|
|
818
|
+
cfc = float(consumption_fixed_capital_usd)
|
|
819
|
+
edu = float(education_expenditure_usd)
|
|
820
|
+
d_res = float(energy_depletion_usd) + float(mineral_depletion_usd) + float(net_forest_depletion_usd)
|
|
821
|
+
d_poll = float(co2_damage_usd) + float(pm25_damage_usd)
|
|
822
|
+
blue_adj = float(blue_natural_capital_net_change_usd)
|
|
823
|
+
|
|
824
|
+
nns_usd = gns - cfc
|
|
825
|
+
ans_cwon_usd = nns_usd + edu - d_res - d_poll
|
|
826
|
+
ans_blue_inclusive_usd = ans_cwon_usd + blue_adj
|
|
827
|
+
|
|
828
|
+
ans_gni_pct = (ans_cwon_usd / gni) * 100.0
|
|
829
|
+
ans_blue_gni_pct = (ans_blue_inclusive_usd / gni) * 100.0
|
|
830
|
+
depletion_gni_pct = (d_res / gni) * 100.0
|
|
831
|
+
co2_damage_gni_pct = (float(co2_damage_usd) / gni) * 100.0
|
|
832
|
+
|
|
833
|
+
if ans_blue_gni_pct >= 8.0:
|
|
834
|
+
regime = "STRONG INCLUSIVE WEALTH ACCUMULATION (Dasgupta-Sustainable Trajectory)"
|
|
835
|
+
elif ans_blue_gni_pct >= 0.0:
|
|
836
|
+
regime = "MODERATE GENUINE SAVINGS (Positive Net Wealth, High Carbon Drag)"
|
|
837
|
+
else:
|
|
838
|
+
regime = "NATURAL CAPITAL LIQUIDATION ALERT (Negative Genuine Savings)"
|
|
839
|
+
|
|
840
|
+
return {
|
|
841
|
+
"net_national_savings_usd": round(nns_usd, 2),
|
|
842
|
+
"cwon_adjusted_net_savings_usd": round(ans_cwon_usd, 2),
|
|
843
|
+
"blue_inclusive_adjusted_net_savings_usd": round(ans_blue_inclusive_usd, 2),
|
|
844
|
+
"cwon_ans_pct_gni": round(ans_gni_pct, 3),
|
|
845
|
+
"blue_inclusive_ans_pct_gni": round(ans_blue_gni_pct, 3),
|
|
846
|
+
"natural_resource_depletion_pct_gni": round(depletion_gni_pct, 3),
|
|
847
|
+
"co2_damage_pct_gni": round(co2_damage_gni_pct, 3),
|
|
848
|
+
"wealth_sustainability_regime": regime,
|
|
849
|
+
"reference": "World Bank Changing Wealth of Nations (CWON, 2021/2024); Dasgupta Review (2021); UN SEEA (2021)",
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def sst_warming_velocity_profile(
|
|
854
|
+
sst_series: Sequence[float],
|
|
855
|
+
dt_days: Any = 1.0,
|
|
856
|
+
mixed_layer_depth_m: float = 25.0,
|
|
857
|
+
seawater_density_kg_m3: float = 1025.0,
|
|
858
|
+
specific_heat_j_kg_k: float = 3985.0,
|
|
859
|
+
baseline_window: Optional[int] = None,
|
|
860
|
+
**kwargs: Any,
|
|
861
|
+
) -> Dict[str, Any]:
|
|
862
|
+
"""
|
|
863
|
+
SST Rate-of-Change (dSST/dt), Marine Heatwave Onset/Decline Velocity (R_onset, R_decline),
|
|
864
|
+
and Mixed-Layer Heat Budget Net Surface Flux (Q_net) Model.
|
|
865
|
+
|
|
866
|
+
Follows Hobday et al. (2016, Progress in Oceanography), Oliver et al. (2021, Annual Review
|
|
867
|
+
of Marine Science), and Schlegel et al. (2021) for MHW onset/decline rates and upper-ocean
|
|
868
|
+
mixed-layer heat content rate of change:
|
|
869
|
+
Q_net(t) = rho_0 * c_p * h_MLD * (dSST / dt) [W/m^2]
|
|
870
|
+
|
|
871
|
+
Parameters
|
|
872
|
+
----------
|
|
873
|
+
sst_series : Sequence[float]
|
|
874
|
+
Chronological sea surface temperature observations (deg C).
|
|
875
|
+
dt_days : float or Sequence[float], default 1.0
|
|
876
|
+
Time step between consecutive observations in days (1.0 for daily OISST), or a sequence
|
|
877
|
+
of chronological years (e.g., `[2005, ..., 2026]`) from which `dt_days` is inferred.
|
|
878
|
+
mixed_layer_depth_m : float, default 25.0
|
|
879
|
+
Effective upper-ocean mixed layer depth h_MLD in meters.
|
|
880
|
+
"""
|
|
881
|
+
vals = [float(v) for v in sst_series if v is not None and math.isfinite(float(v))]
|
|
882
|
+
n = len(vals)
|
|
883
|
+
if n < 3:
|
|
884
|
+
return {"error": "At least 3 valid SST observations are required.", "n": n}
|
|
885
|
+
|
|
886
|
+
if isinstance(dt_days, (list, tuple)):
|
|
887
|
+
if len(dt_days) >= 2:
|
|
888
|
+
try:
|
|
889
|
+
yr_step = (float(dt_days[-1]) - float(dt_days[0])) / max(len(dt_days) - 1, 1)
|
|
890
|
+
dt = max(yr_step * 365.25 if yr_step <= 10.0 else yr_step, 1e-6)
|
|
891
|
+
except (TypeError, ValueError):
|
|
892
|
+
dt = 365.25
|
|
893
|
+
else:
|
|
894
|
+
dt = 365.25
|
|
895
|
+
else:
|
|
896
|
+
dt = max(float(dt_days), 1e-6)
|
|
897
|
+
dt_sec = dt * 86400.0
|
|
898
|
+
heat_cap_area = float(seawater_density_kg_m3) * float(specific_heat_j_kg_k) * float(mixed_layer_depth_m)
|
|
899
|
+
|
|
900
|
+
# Centered finite-difference dSST/dt (deg C / day) and interior second derivative d2SST/dt2
|
|
901
|
+
dsst_dt: List[float] = []
|
|
902
|
+
d2sst_dt2: List[float] = []
|
|
903
|
+
for i in range(n):
|
|
904
|
+
if i == 0:
|
|
905
|
+
rate = (vals[1] - vals[0]) / dt
|
|
906
|
+
elif i == n - 1:
|
|
907
|
+
rate = (vals[-1] - vals[-2]) / dt
|
|
908
|
+
else:
|
|
909
|
+
rate = (vals[i + 1] - vals[i - 1]) / (2.0 * dt)
|
|
910
|
+
acc = (vals[i + 1] - 2.0 * vals[i] + vals[i - 1]) / (dt * dt)
|
|
911
|
+
d2sst_dt2.append(acc)
|
|
912
|
+
dsst_dt.append(rate)
|
|
913
|
+
|
|
914
|
+
q_net_wm2 = [(r / 86400.0) * heat_cap_area for r in dsst_dt]
|
|
915
|
+
pos_rates = [r for r in dsst_dt if r > 0.0]
|
|
916
|
+
neg_rates = [abs(r) for r in dsst_dt if r < 0.0]
|
|
917
|
+
|
|
918
|
+
# Peak warming event onset & decline velocity (Hobday et al., 2016)
|
|
919
|
+
peak_idx = max(range(n), key=lambda k: vals[k])
|
|
920
|
+
start_idx = min(range(peak_idx + 1), key=lambda k: vals[k])
|
|
921
|
+
end_idx = min(range(peak_idx, n), key=lambda k: vals[k])
|
|
922
|
+
|
|
923
|
+
onset_days = max((peak_idx - start_idx) * dt, dt)
|
|
924
|
+
decline_days = max((end_idx - peak_idx) * dt, dt)
|
|
925
|
+
r_onset = (vals[peak_idx] - vals[start_idx]) / onset_days
|
|
926
|
+
r_decline = (vals[peak_idx] - vals[end_idx]) / decline_days
|
|
927
|
+
|
|
928
|
+
# OLS linear warming rate (deg C / day -> deg C / decade)
|
|
929
|
+
t_mean = (n - 1) * dt / 2.0
|
|
930
|
+
v_mean = sum(vals) / n
|
|
931
|
+
num = sum(((i * dt - t_mean) * (vals[i] - v_mean)) for i in range(n))
|
|
932
|
+
den = sum(((i * dt - t_mean) ** 2) for i in range(n))
|
|
933
|
+
slope_per_day = num / max(den, 1e-12)
|
|
934
|
+
warming_per_decade = slope_per_day * 3652.5
|
|
935
|
+
|
|
936
|
+
# Compare second half vs first half decadal trend for decadal acceleration
|
|
937
|
+
half = max(n // 2, 2)
|
|
938
|
+
slope1 = (vals[half - 1] - vals[0]) / max((half - 1) * dt, 1e-6) * 3652.5
|
|
939
|
+
slope2 = (vals[-1] - vals[n - half]) / max((half - 1) * dt, 1e-6) * 3652.5
|
|
940
|
+
acc_dec2 = slope2 - slope1
|
|
941
|
+
|
|
942
|
+
max_warming_rate = max(dsst_dt)
|
|
943
|
+
max_cooling_rate = min(dsst_dt)
|
|
944
|
+
mean_acc_day2 = sum(d2sst_dt2) / max(len(d2sst_dt2), 1)
|
|
945
|
+
|
|
946
|
+
return {
|
|
947
|
+
"n_observations": n,
|
|
948
|
+
"dt_days": round(dt, 4),
|
|
949
|
+
"mean_dsst_dt_c_per_day": round(sum(dsst_dt) / n, 5),
|
|
950
|
+
"max_warming_rate_c_per_day": round(max_warming_rate, 4),
|
|
951
|
+
"max_cooling_rate_c_per_day": round(max_cooling_rate, 4),
|
|
952
|
+
"mean_warming_phase_rate_c_per_day": round(sum(pos_rates) / max(len(pos_rates), 1), 4),
|
|
953
|
+
"mean_cooling_phase_rate_c_per_day": round(sum(neg_rates) / max(len(neg_rates), 1), 4),
|
|
954
|
+
"mhw_onset_rate_c_per_day": round(r_onset, 4),
|
|
955
|
+
"mhw_decline_rate_c_per_day": round(r_decline, 4),
|
|
956
|
+
"onset_to_decline_asymmetry_ratio": round(r_onset / max(r_decline, 1e-6), 3),
|
|
957
|
+
"decadal_trend_c_per_decade": round(warming_per_decade, 4),
|
|
958
|
+
"mean_thermal_acceleration_c_per_day2": round(mean_acc_day2, 6),
|
|
959
|
+
"warming_acceleration_c_per_decade2": round(acc_dec2, 4),
|
|
960
|
+
"peak_mixed_layer_heat_flux_wm2": round(max(q_net_wm2), 2),
|
|
961
|
+
"min_mixed_layer_heat_flux_wm2": round(min(q_net_wm2), 2),
|
|
962
|
+
"reference": "Hobday et al. (2016, Prog. Oceanogr.); Oliver et al. (2021, Annu. Rev. Mar. Sci.); Schlegel et al. (2021)",
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def ocean_climate_velocity(
|
|
967
|
+
temporal_trend_c_per_year: float = 0.045,
|
|
968
|
+
meridional_grad_c_per_km: float = -0.0042,
|
|
969
|
+
zonal_grad_c_per_km: float = 0.0015,
|
|
970
|
+
species_max_dispersal_km_per_year: float = 8.5,
|
|
971
|
+
) -> Dict[str, Any]:
|
|
972
|
+
"""
|
|
973
|
+
Ocean Isotherm Migration & Climate Velocity Vector Model (V_SST).
|
|
974
|
+
|
|
975
|
+
Implements Loarie et al. (2009, Nature), Burrows et al. (2011, Science),
|
|
976
|
+
Molinos et al. (2016, Nature Climate Change), and Brito-Morales et al. (2020,
|
|
977
|
+
Nature Climate Change):
|
|
978
|
+
||grad_h(SST)|| = sqrt((dSST/dx)^2 + (dSST/dy)^2) [deg C / km]
|
|
979
|
+
V_SST = (dSST/dt) / ||grad_h(SST)|| [km / year]
|
|
980
|
+
|
|
981
|
+
Parameters
|
|
982
|
+
----------
|
|
983
|
+
temporal_trend_c_per_year : float, default 0.045
|
|
984
|
+
Local temporal warming rate dSST/dt in deg C / year (e.g., 0.045 C/yr = 0.45 C/decade).
|
|
985
|
+
meridional_grad_c_per_km : float, default -0.0042
|
|
986
|
+
Northward spatial SST gradient dSST/dy in deg C / km.
|
|
987
|
+
zonal_grad_c_per_km : float, default 0.0015
|
|
988
|
+
Eastward spatial SST gradient dSST/dx in deg C / km.
|
|
989
|
+
species_max_dispersal_km_per_year : float, default 8.5
|
|
990
|
+
Maximum autonomous biogeographic range shift speed of target marine taxon (km/year).
|
|
991
|
+
"""
|
|
992
|
+
dsst_dt = float(temporal_trend_c_per_year)
|
|
993
|
+
gx = float(zonal_grad_c_per_km)
|
|
994
|
+
gy = float(meridional_grad_c_per_km)
|
|
995
|
+
v_disp = max(float(species_max_dispersal_km_per_year), 0.01)
|
|
996
|
+
|
|
997
|
+
grad_mag = math.hypot(gx, gy)
|
|
998
|
+
grad_safe = max(grad_mag, 1e-6)
|
|
999
|
+
velocity_km_yr = dsst_dt / grad_safe
|
|
1000
|
+
velocity_km_decade = velocity_km_yr * 10.0
|
|
1001
|
+
|
|
1002
|
+
# Isotherm migration bearing (direction toward cooler water when warming)
|
|
1003
|
+
bearing_rad = math.atan2(-gx * math.copysign(1.0, dsst_dt), -gy * math.copysign(1.0, dsst_dt))
|
|
1004
|
+
bearing_deg = (math.degrees(bearing_rad) + 360.0) % 360.0
|
|
1005
|
+
|
|
1006
|
+
dispersal_lag_ratio = abs(velocity_km_yr) / v_disp
|
|
1007
|
+
if dispersal_lag_ratio > 1.5:
|
|
1008
|
+
ecological_regime = "HIGH EXTIRPATION / THERMAL ESCAPE DEFICIT (V_SST > 1.5x Dispersal Capacity)"
|
|
1009
|
+
elif dispersal_lag_ratio > 1.0:
|
|
1010
|
+
ecological_regime = "BIOGEOGRAPHIC TRACKING STRESS (Isotherm Velocity Exceeds Species Dispersal)"
|
|
1011
|
+
else:
|
|
1012
|
+
ecological_regime = "CLIMATE REFUGIA / TRACKABLE ISOTHERM SHIFT (V_SST <= Dispersal Capacity)"
|
|
1013
|
+
|
|
1014
|
+
return {
|
|
1015
|
+
"temporal_warming_c_per_year": round(dsst_dt, 5),
|
|
1016
|
+
"temporal_warming_c_per_decade": round(dsst_dt * 10.0, 4),
|
|
1017
|
+
"spatial_gradient_c_per_km": round(grad_mag, 6),
|
|
1018
|
+
"spatial_gradient_c_per_100km": round(grad_mag * 100.0, 4),
|
|
1019
|
+
"climate_velocity_km_per_year": round(velocity_km_yr, 3),
|
|
1020
|
+
"climate_velocity_km_per_decade": round(velocity_km_decade, 2),
|
|
1021
|
+
"isotherm_migration_bearing_deg": round(bearing_deg, 2),
|
|
1022
|
+
"species_dispersal_lag_ratio": round(dispersal_lag_ratio, 3),
|
|
1023
|
+
"biogeographic_regime": ecological_regime,
|
|
1024
|
+
"reference": "Loarie et al. (2009, Nature); Burrows et al. (2011, Science); Brito-Morales et al. (2020, Nat. Clim. Chang.)",
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def ectotherm_thermal_shock_index(
|
|
1029
|
+
sst_series: Sequence[float],
|
|
1030
|
+
acclimation_tau_days: float = 14.0,
|
|
1031
|
+
q10_coefficient: float = 2.3,
|
|
1032
|
+
critical_onset_rate_c_per_day: float = 0.18,
|
|
1033
|
+
dt_days: float = 1.0,
|
|
1034
|
+
) -> Dict[str, Any]:
|
|
1035
|
+
"""
|
|
1036
|
+
Marine Ectotherm Physiological Acclimation Lag & Rapid-Onset Thermal Shock Index (TSI).
|
|
1037
|
+
|
|
1038
|
+
Models dynamic thermal acclimation lag and metabolic oxygen-demand shock triggered by
|
|
1039
|
+
rapid SST rate of change (dSST/dt), following Genin et al. (2020, PNAS, 'Rapid onsets
|
|
1040
|
+
of warming events trigger mass mortality of coral-reef fish') and Pörtner (2021, J. Exp.
|
|
1041
|
+
Biol., Oxygen- and Capacity-Limited Thermal Tolerance — OCLTT):
|
|
1042
|
+
dT_acclim / dt = (SST(t) - T_acclim(t)) / tau_acclim
|
|
1043
|
+
ThermalLag(t) = SST(t) - T_acclim(t)
|
|
1044
|
+
TSI(t) = max(0, dSST/dt) * max(0, ThermalLag(t)) * Q10^(ThermalLag(t)/10)
|
|
1045
|
+
|
|
1046
|
+
Parameters
|
|
1047
|
+
----------
|
|
1048
|
+
sst_series : Sequence[float]
|
|
1049
|
+
Daily or regular SST observations (deg C).
|
|
1050
|
+
acclimation_tau_days : float, default 14.0
|
|
1051
|
+
Physiological thermal acclimation e-folding timescale tau_acclim in days.
|
|
1052
|
+
q10_coefficient : float, default 2.3
|
|
1053
|
+
Van 't Hoff Q10 temperature sensitivity of routine metabolic oxygen demand.
|
|
1054
|
+
critical_onset_rate_c_per_day : float, default 0.18
|
|
1055
|
+
Empirical rapid-warming mortality threshold in deg C / day (Genin et al., 2020).
|
|
1056
|
+
"""
|
|
1057
|
+
vals = [float(v) for v in sst_series if v is not None and math.isfinite(float(v))]
|
|
1058
|
+
n = len(vals)
|
|
1059
|
+
if n < 2:
|
|
1060
|
+
return {"error": "At least 2 valid SST observations are required.", "n": n}
|
|
1061
|
+
|
|
1062
|
+
tau = max(float(acclimation_tau_days), 0.5)
|
|
1063
|
+
q10 = max(float(q10_coefficient), 1.0)
|
|
1064
|
+
dt = max(float(dt_days), 1e-6)
|
|
1065
|
+
alpha = 1.0 - math.exp(-dt / tau)
|
|
1066
|
+
|
|
1067
|
+
t_acclim = vals[0]
|
|
1068
|
+
max_lag = 0.0
|
|
1069
|
+
max_tsi = 0.0
|
|
1070
|
+
max_metabolic_surge = 1.0
|
|
1071
|
+
rapid_shock_days = 0
|
|
1072
|
+
|
|
1073
|
+
for i in range(1, n):
|
|
1074
|
+
rate = (vals[i] - vals[i - 1]) / dt
|
|
1075
|
+
t_acclim = t_acclim + alpha * (vals[i] - t_acclim)
|
|
1076
|
+
lag = max(0.0, vals[i] - t_acclim)
|
|
1077
|
+
q10_factor = q10 ** (lag / 10.0)
|
|
1078
|
+
tsi = max(0.0, rate) * lag * q10_factor
|
|
1079
|
+
if lag > max_lag:
|
|
1080
|
+
max_lag = lag
|
|
1081
|
+
if tsi > max_tsi:
|
|
1082
|
+
max_tsi = tsi
|
|
1083
|
+
if q10_factor > max_metabolic_surge:
|
|
1084
|
+
max_metabolic_surge = q10_factor
|
|
1085
|
+
if rate >= float(critical_onset_rate_c_per_day) and lag >= 0.5:
|
|
1086
|
+
rapid_shock_days += 1
|
|
1087
|
+
|
|
1088
|
+
if max_tsi >= 0.45 or rapid_shock_days >= 3:
|
|
1089
|
+
status = "CRITICAL THERMAL SHOCK (High Mass-Mortality & Hypoxic Scope Collapse Risk)"
|
|
1090
|
+
elif max_tsi >= 0.18 or rapid_shock_days >= 1:
|
|
1091
|
+
status = "MODERATE PHYSIOLOGICAL ACCLIMATION STRESS (Sublethal OCLTT Aerobic Compression)"
|
|
1092
|
+
else:
|
|
1093
|
+
status = "WITHIN ACCLIMATION CAPACITY (Gradual Warming Tracked by Physiology)"
|
|
1094
|
+
|
|
1095
|
+
return {
|
|
1096
|
+
"n_observations": n,
|
|
1097
|
+
"acclimation_tau_days": tau,
|
|
1098
|
+
"q10_coefficient": q10,
|
|
1099
|
+
"max_acclimation_lag_c": round(max_lag, 4),
|
|
1100
|
+
"max_q10_metabolic_demand_multiplier": round(max_metabolic_surge, 4),
|
|
1101
|
+
"max_thermal_shock_index": round(max_tsi, 4),
|
|
1102
|
+
"rapid_onset_shock_days": rapid_shock_days,
|
|
1103
|
+
"physiological_risk_status": status,
|
|
1104
|
+
"reference": "Genin et al. (2020, PNAS); Pörtner (2021, J. Exp. Biol.); Morley et al. (2019)",
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def stochastic_sst_memory_ou(
|
|
1109
|
+
sst_anomaly_series: Sequence[float],
|
|
1110
|
+
dt_days: float = 1.0,
|
|
1111
|
+
mixed_layer_depth_m: float = 25.0,
|
|
1112
|
+
) -> Dict[str, Any]:
|
|
1113
|
+
"""
|
|
1114
|
+
Hasselmann (1976) / Martinez-Villalobos et al. (2022) Stochastic Ornstein-Uhlenbeck (OU)
|
|
1115
|
+
SST Rate-of-Change Damping & Ocean Thermal Memory Model.
|
|
1116
|
+
|
|
1117
|
+
Estimates the stochastic climate feedback damping rate lambda (day^-1), thermal memory
|
|
1118
|
+
e-folding timescale tau_mem = 1 / lambda (days), air-sea turbulent heat feedback
|
|
1119
|
+
alpha_fb (W m^-2 K^-1), and stochastic weather-noise amplitude sigma_xi from an SST
|
|
1120
|
+
anomaly trajectory SST'(t) and its finite-difference rate of change dSST'/dt:
|
|
1121
|
+
dSST'/dt = -lambda * SST'(t) + sigma_xi * xi(t)
|
|
1122
|
+
|
|
1123
|
+
Parameters
|
|
1124
|
+
----------
|
|
1125
|
+
sst_anomaly_series : Sequence[float]
|
|
1126
|
+
Daily or regular SST anomaly series (deg C). If raw SST is passed, the sample mean
|
|
1127
|
+
is automatically removed.
|
|
1128
|
+
dt_days : float, default 1.0
|
|
1129
|
+
Time step in days.
|
|
1130
|
+
mixed_layer_depth_m : float, default 25.0
|
|
1131
|
+
Mixed-layer depth in meters used to convert lambda into air-sea feedback (W m^-2 K^-1).
|
|
1132
|
+
"""
|
|
1133
|
+
raw = [float(v) for v in sst_anomaly_series if v is not None and math.isfinite(float(v))]
|
|
1134
|
+
n = len(raw)
|
|
1135
|
+
if n < 5:
|
|
1136
|
+
return {"error": "At least 5 valid observations are required.", "n": n}
|
|
1137
|
+
|
|
1138
|
+
dt = max(float(dt_days), 1e-6)
|
|
1139
|
+
mean_v = sum(raw) / n
|
|
1140
|
+
anom = [v - mean_v for v in raw]
|
|
1141
|
+
|
|
1142
|
+
# Forward rate dSST'/dt = (SST'_{t+1} - SST'_t) / dt regressed on SST'_t
|
|
1143
|
+
x_vals = anom[:-1]
|
|
1144
|
+
rates = [(anom[i + 1] - anom[i]) / dt for i in range(n - 1)]
|
|
1145
|
+
var_x = sum(x * x for x in x_vals) / max(len(x_vals), 1)
|
|
1146
|
+
cov_xr = sum(x_vals[i] * rates[i] for i in range(len(x_vals))) / max(len(x_vals), 1)
|
|
1147
|
+
|
|
1148
|
+
# Damping rate lambda > 0
|
|
1149
|
+
lambda_day = max(0.002, min(1.5, -cov_xr / max(var_x, 1e-8)))
|
|
1150
|
+
tau_memory_days = 1.0 / lambda_day
|
|
1151
|
+
lag1_autocorr = math.exp(-lambda_day * dt)
|
|
1152
|
+
|
|
1153
|
+
# Residual stochastic forcing standard deviation sigma_xi (deg C / sqrt(day))
|
|
1154
|
+
residuals = [rates[i] + lambda_day * x_vals[i] for i in range(len(x_vals))]
|
|
1155
|
+
sigma_xi = math.sqrt(sum(r * r for r in residuals) / max(len(residuals), 1)) * math.sqrt(dt)
|
|
1156
|
+
|
|
1157
|
+
# Air-sea heat feedback coefficient alpha_fb = rho * c_p * h_MLD * lambda (W m^-2 K^-1)
|
|
1158
|
+
heat_cap = 1025.0 * 3985.0 * float(mixed_layer_depth_m)
|
|
1159
|
+
alpha_feedback_wm2_k = (lambda_day / 86400.0) * heat_cap
|
|
1160
|
+
|
|
1161
|
+
return {
|
|
1162
|
+
"n_observations": n,
|
|
1163
|
+
"damping_rate_lambda_per_day": round(lambda_day, 5),
|
|
1164
|
+
"thermal_memory_tau_days": round(tau_memory_days, 2),
|
|
1165
|
+
"implied_lag1_autocorrelation": round(lag1_autocorr, 4),
|
|
1166
|
+
"stochastic_forcing_sigma_c_sqrt_day": round(sigma_xi, 4),
|
|
1167
|
+
"air_sea_feedback_wm2_per_k": round(alpha_feedback_wm2_k, 2),
|
|
1168
|
+
"predictability_horizon_days_95pct": round(tau_memory_days * math.log(20.0), 1),
|
|
1169
|
+
"reference": "Hasselmann (1976, Tellus); Frankignoul & Hasselmann (1977); Martinez-Villalobos et al. (2022, J. Clim.)",
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def global_hypoxia_deadzone_index(
|
|
1174
|
+
subpycnocline_do_mg_l: float,
|
|
1175
|
+
chlorophyll_a_ug_l: float = 4.5,
|
|
1176
|
+
secchi_depth_m: float = 4.0,
|
|
1177
|
+
din_dip_molar_ratio: float = 20.0,
|
|
1178
|
+
basin_code: str = "OCN_GULF_MEXICO",
|
|
1179
|
+
) -> Dict[str, Any]:
|
|
1180
|
+
"""
|
|
1181
|
+
Compute the Global Coastal Hypoxia, Dead-Zone Severity & Eutrophication Risk Index
|
|
1182
|
+
following Breitburg et al. (Science 2018, IOC-UNESCO GO2NE), Diaz & Rosenberg
|
|
1183
|
+
(Science 2008), and Redfield (1958) stoichiometric imbalance diagnostics.
|
|
1184
|
+
|
|
1185
|
+
Parameters
|
|
1186
|
+
----------
|
|
1187
|
+
subpycnocline_do_mg_l : float
|
|
1188
|
+
Sub-pycnocline / bottom-water dissolved oxygen concentration in mg/L
|
|
1189
|
+
(severe hypoxia threshold = 2.0 mg/L ~ 62.5 umol/L; suboxia = 0.5 mg/L).
|
|
1190
|
+
chlorophyll_a_ug_l : float, default 4.5
|
|
1191
|
+
Surface chlorophyll-a concentration in ug/L.
|
|
1192
|
+
secchi_depth_m : float, default 4.0
|
|
1193
|
+
Secchi disk water transparency depth in meters.
|
|
1194
|
+
din_dip_molar_ratio : float, default 20.0
|
|
1195
|
+
Dissolved inorganic nitrogen to phosphorus molar ratio (Redfield = 16:1).
|
|
1196
|
+
basin_code : str, default 'OCN_GULF_MEXICO'
|
|
1197
|
+
GOMD marine basin code (e.g., 'OCN_GULF_MEXICO', 'SEA_BALTIC', 'OCN_MARMARA').
|
|
1198
|
+
"""
|
|
1199
|
+
do_val = max(0.0, float(subpycnocline_do_mg_l))
|
|
1200
|
+
chla = max(0.01, float(chlorophyll_a_ug_l))
|
|
1201
|
+
secchi = max(0.1, float(secchi_depth_m))
|
|
1202
|
+
np_ratio = max(0.1, float(din_dip_molar_ratio))
|
|
1203
|
+
|
|
1204
|
+
# Oxygen deficit relative to biological hypoxia threshold (2.0 mg/L) and normoxia (6.0 mg/L)
|
|
1205
|
+
hypoxia_deficit_mg_l = max(0.0, 2.0 - do_val)
|
|
1206
|
+
do_stress = max(0.0, min(100.0, ((6.0 - do_val) / 5.5) * 100.0))
|
|
1207
|
+
chla_stress = max(0.0, min(100.0, (math.log10(chla + 1.0) / math.log10(16.0)) * 100.0))
|
|
1208
|
+
turbidity_stress = max(0.0, min(100.0, ((10.0 - min(secchi, 10.0)) / 9.0) * 100.0))
|
|
1209
|
+
redfield_imbalance = abs(math.log2(np_ratio / 16.0))
|
|
1210
|
+
stoich_stress = min(100.0, redfield_imbalance * 45.0)
|
|
1211
|
+
|
|
1212
|
+
composite_score = 0.45 * do_stress + 0.25 * chla_stress + 0.15 * turbidity_stress + 0.15 * stoich_stress
|
|
1213
|
+
|
|
1214
|
+
if do_val < 0.5:
|
|
1215
|
+
regime = "ANOMALOUS_SUBOXIA_DEAD_ZONE"
|
|
1216
|
+
elif do_val < 2.0:
|
|
1217
|
+
regime = "SEVERE_HYPOXIA_DEAD_ZONE"
|
|
1218
|
+
elif do_val < 4.0:
|
|
1219
|
+
regime = "SEASONAL_HYPOXIA_VULNERABLE"
|
|
1220
|
+
else:
|
|
1221
|
+
regime = "NORMOXIC_OXIC"
|
|
1222
|
+
|
|
1223
|
+
return {
|
|
1224
|
+
"basin_code": str(basin_code).upper(),
|
|
1225
|
+
"subpycnocline_do_mg_l": round(do_val, 3),
|
|
1226
|
+
"subpycnocline_do_umol_l": round(do_val * 31.25, 2),
|
|
1227
|
+
"hypoxia_deficit_below_2mgl": round(hypoxia_deficit_mg_l, 3),
|
|
1228
|
+
"chlorophyll_a_ug_l": round(chla, 3),
|
|
1229
|
+
"secchi_depth_m": round(secchi, 2),
|
|
1230
|
+
"din_dip_molar_ratio": round(np_ratio, 2),
|
|
1231
|
+
"redfield_log2_imbalance": round(redfield_imbalance, 3),
|
|
1232
|
+
"composite_deadzone_severity_0_100": round(composite_score, 2),
|
|
1233
|
+
"oxygen_regime": regime,
|
|
1234
|
+
"reference": "Breitburg et al. (Science 2018, IOC-UNESCO GO2NE); Diaz & Rosenberg (Science 2008)",
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def offshore_wind_marine_energy_lcoe(
|
|
1239
|
+
capacity_mw: float = 1000.0,
|
|
1240
|
+
capex_usd_per_kw: float = 3150.0,
|
|
1241
|
+
opex_usd_per_kw_yr: float = 78.0,
|
|
1242
|
+
net_capacity_factor: float = 0.48,
|
|
1243
|
+
wacc: float = 0.065,
|
|
1244
|
+
lifetime_years: int = 25,
|
|
1245
|
+
grid_emission_factor_tco2_mwh: float = 0.42,
|
|
1246
|
+
) -> Dict[str, Any]:
|
|
1247
|
+
"""
|
|
1248
|
+
Compute Offshore Wind & Marine Renewable Energy Levelized Cost of Electricity (LCOE),
|
|
1249
|
+
Capital Recovery Factor (CRF), Annual Net Generation (GWh), Avoided CO2 Emissions,
|
|
1250
|
+
and Co-Located Green Hydrogen / E-Ammonia Production Potential following IRENA
|
|
1251
|
+
(2024, Renewable Power Generation Costs) and GWEC (2024, Global Offshore Wind Report).
|
|
1252
|
+
"""
|
|
1253
|
+
cap_mw = max(0.1, float(capacity_mw))
|
|
1254
|
+
capex_kw = max(100.0, float(capex_usd_per_kw))
|
|
1255
|
+
opex_kw = max(1.0, float(opex_usd_per_kw_yr))
|
|
1256
|
+
cf = max(0.05, min(0.95, float(net_capacity_factor)))
|
|
1257
|
+
r = max(0.001, float(wacc))
|
|
1258
|
+
n = max(5, int(lifetime_years))
|
|
1259
|
+
ef = max(0.0, float(grid_emission_factor_tco2_mwh))
|
|
1260
|
+
|
|
1261
|
+
crf = (r * ((1.0 + r) ** n)) / (((1.0 + r) ** n) - 1.0)
|
|
1262
|
+
annual_mwh_per_kw = 8.76 * cf
|
|
1263
|
+
lcoe_usd_mwh = ((capex_kw * crf) + opex_kw) / annual_mwh_per_kw
|
|
1264
|
+
annual_gwh = cap_mw * 8.76 * cf
|
|
1265
|
+
avoided_tco2_yr = annual_gwh * 1000.0 * ef
|
|
1266
|
+
# PEM / Alkaline electrolyzer specific consumption ~ 52 MWh / tonne H2
|
|
1267
|
+
green_h2_tonnes_yr = (annual_gwh * 1000.0) / 52.0
|
|
1268
|
+
green_ammonia_tonnes_yr = green_h2_tonnes_yr * 5.5
|
|
1269
|
+
|
|
1270
|
+
return {
|
|
1271
|
+
"installed_capacity_mw": round(cap_mw, 2),
|
|
1272
|
+
"net_capacity_factor": round(cf, 4),
|
|
1273
|
+
"capital_recovery_factor": round(crf, 5),
|
|
1274
|
+
"lcoe_usd_per_mwh": round(lcoe_usd_mwh, 2),
|
|
1275
|
+
"annual_generation_gwh": round(annual_gwh, 2),
|
|
1276
|
+
"annual_avoided_co2_tonnes": round(avoided_tco2_yr, 1),
|
|
1277
|
+
"potential_green_hydrogen_tonnes_yr": round(green_h2_tonnes_yr, 1),
|
|
1278
|
+
"potential_green_ammonia_tonnes_yr": round(green_ammonia_tonnes_yr, 1),
|
|
1279
|
+
"reference": "IRENA (2024, Renewable Power Generation Costs); GWEC (2024, Global Offshore Wind Report)",
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def marine_plastic_ghost_gear_risk(
|
|
1284
|
+
plastic_leakage_kt_yr: float = 108.0,
|
|
1285
|
+
beach_litter_items_100m: float = 435.0,
|
|
1286
|
+
aldfg_ghost_gear_share_pct: float = 19.8,
|
|
1287
|
+
coastline_km: float = 8333.0,
|
|
1288
|
+
) -> Dict[str, Any]:
|
|
1289
|
+
"""
|
|
1290
|
+
Compute the Global Marine Macro-Plastic Leakage, Coastal Debris Density &
|
|
1291
|
+
Abandoned/Lost Fishing Gear (ALDFG / Ghost Gear) Ecological Risk Index (0–100)
|
|
1292
|
+
following Jambeck et al. (Science 2015), Meijer et al. (Science Advances 2021),
|
|
1293
|
+
Richardson et al. (Science Advances 2022), and UNEP GPML / OECD (2024).
|
|
1294
|
+
"""
|
|
1295
|
+
leak = max(0.0, float(plastic_leakage_kt_yr))
|
|
1296
|
+
beach = max(0.0, float(beach_litter_items_100m))
|
|
1297
|
+
ghost = max(0.0, min(100.0, float(aldfg_ghost_gear_share_pct)))
|
|
1298
|
+
coast = max(1.0, float(coastline_km))
|
|
1299
|
+
|
|
1300
|
+
leak_per_km_t = (leak * 1000.0) / coast
|
|
1301
|
+
s_leak = min(100.0, (math.log1p(leak) / math.log1p(1800.0)) * 100.0)
|
|
1302
|
+
s_beach = min(100.0, (beach / 1000.0) * 100.0)
|
|
1303
|
+
s_ghost = min(100.0, (ghost / 45.0) * 100.0)
|
|
1304
|
+
composite_risk = 0.45 * s_leak + 0.30 * s_beach + 0.25 * s_ghost
|
|
1305
|
+
|
|
1306
|
+
if beach <= 20.0:
|
|
1307
|
+
msfd_status = "GES_ACHIEVED (<=20 items/100m — EU MSFD D10 Benchmark)"
|
|
1308
|
+
elif beach <= 200.0:
|
|
1309
|
+
msfd_status = "MODERATE_POLLUTION (20-200 items/100m)"
|
|
1310
|
+
else:
|
|
1311
|
+
msfd_status = "HIGH_MACRO_LITTER_EXCEEDANCE (>200 items/100m)"
|
|
1312
|
+
|
|
1313
|
+
return {
|
|
1314
|
+
"plastic_leakage_kt_yr": round(leak, 2),
|
|
1315
|
+
"specific_coastal_flux_tonnes_per_km": round(leak_per_km_t, 2),
|
|
1316
|
+
"beach_macro_litter_items_100m": round(beach, 1),
|
|
1317
|
+
"aldfg_ghost_gear_share_pct": round(ghost, 2),
|
|
1318
|
+
"estimated_aldfg_leakage_kt_yr": round(leak * (ghost / 100.0), 3),
|
|
1319
|
+
"composite_plastic_risk_index_0_100": round(composite_risk, 2),
|
|
1320
|
+
"msfd_descriptor10_status": msfd_status,
|
|
1321
|
+
"reference": "UNEP GPML (2024); Meijer et al. (2021, Sci. Adv.); Richardson et al. (2022, Sci. Adv.)",
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
def coastal_storm_surge_offshore_wind_nexus(
|
|
1326
|
+
tide_high_water_m_msl: float = 1.45,
|
|
1327
|
+
wind_50m_ms: float = 13.8,
|
|
1328
|
+
wind_10m_max_ms: float = 21.4,
|
|
1329
|
+
sea_level_pressure_kpa: float = 99.4,
|
|
1330
|
+
turbine_rated_mw: float = 15.0,
|
|
1331
|
+
) -> Dict[str, Any]:
|
|
1332
|
+
"""
|
|
1333
|
+
Compute the Coastal Storm-Surge Inverse Barometer Effect, Wind-Stress Setup &
|
|
1334
|
+
50m Offshore Wind Turbine Power Curve / Cut-Out Curtailment Nexus (Vousdoukas
|
|
1335
|
+
et al., Nature Climate Change 2018 & Nature Communications 2020; Muis et al.,
|
|
1336
|
+
Nature Communications 2020; NASA POWER MERRA-2 & NOAA CO-OPS/GLOSS).
|
|
1337
|
+
"""
|
|
1338
|
+
hw_m = float(tide_high_water_m_msl)
|
|
1339
|
+
v50 = max(0.0, float(wind_50m_ms))
|
|
1340
|
+
v10_max = max(0.0, float(wind_10m_max_ms))
|
|
1341
|
+
p_kpa = max(85.0, min(108.0, float(sea_level_pressure_kpa)))
|
|
1342
|
+
rated_mw = max(0.1, float(turbine_rated_mw))
|
|
1343
|
+
|
|
1344
|
+
# Inverse barometer surge: ~ +1 cm (+0.01 m) per 1 hPa (0.1 kPa) below 101.325 kPa
|
|
1345
|
+
ib_surge_m = max(0.0, (101.325 - p_kpa) * 10.0 * 0.01)
|
|
1346
|
+
# Wind-driven shallow coastal setup approximation
|
|
1347
|
+
wind_setup_m = 0.0018 * (v10_max ** 2)
|
|
1348
|
+
total_extreme_water_m = hw_m + ib_surge_m + wind_setup_m
|
|
1349
|
+
|
|
1350
|
+
# Standard IEC Class I 15 MW offshore wind turbine cubic power curve (cut-in 3 m/s, rated 11 m/s, cut-out 25 m/s)
|
|
1351
|
+
if v50 < 3.0 or v50 >= 25.0:
|
|
1352
|
+
cf_instant = 0.0
|
|
1353
|
+
elif v50 >= 11.0:
|
|
1354
|
+
cf_instant = 1.0
|
|
1355
|
+
else:
|
|
1356
|
+
cf_instant = ((v50 - 3.0) / (11.0 - 3.0)) ** 3
|
|
1357
|
+
instant_power_mw = rated_mw * cf_instant
|
|
1358
|
+
storm_curtailment = v50 >= 25.0 or v10_max >= 25.0
|
|
1359
|
+
|
|
1360
|
+
surge_risk_score = min(100.0, max(0.0, (total_extreme_water_m / 3.5) * 100.0))
|
|
1361
|
+
return {
|
|
1362
|
+
"observed_high_water_m_msl": round(hw_m, 3),
|
|
1363
|
+
"inverse_barometer_surge_m": round(ib_surge_m, 3),
|
|
1364
|
+
"wind_stress_setup_m": round(wind_setup_m, 3),
|
|
1365
|
+
"total_storm_tide_elevation_m": round(total_extreme_water_m, 3),
|
|
1366
|
+
"offshore_wind_50m_ms": round(v50, 2),
|
|
1367
|
+
"instantaneous_capacity_factor": round(cf_instant, 4),
|
|
1368
|
+
"turbine_output_mw": round(instant_power_mw, 3),
|
|
1369
|
+
"storm_cutout_curtailment_active": bool(storm_curtailment),
|
|
1370
|
+
"coastal_surge_hazard_index_0_100": round(surge_risk_score, 2),
|
|
1371
|
+
"reference": "Vousdoukas et al. (2020, Nat. Commun.); Muis et al. (2020, Nat. Commun.); NASA POWER MERRA-2 & NOAA CO-OPS",
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def climate_teleconnection_regime(
|
|
1376
|
+
enso_oni_c: float = 1.15,
|
|
1377
|
+
iod_dmi_c: float = 0.48,
|
|
1378
|
+
pdo_index: float = -0.85,
|
|
1379
|
+
amo_index_c: float = 0.34,
|
|
1380
|
+
nao_index: float = 0.62,
|
|
1381
|
+
sam_index: float = 0.95,
|
|
1382
|
+
amoc_26n_sv: float = 16.4,
|
|
1383
|
+
) -> Dict[str, Any]:
|
|
1384
|
+
"""
|
|
1385
|
+
Evaluate Coupled Ocean-Atmosphere Teleconnection Regimes (ENSO ONI, IOD DMI,
|
|
1386
|
+
PDO, AMO, NAO, SAM) and Atlantic Meridional Overturning Circulation (AMOC 26.5N
|
|
1387
|
+
RAPID-MOCHA transport in Sverdrups) following McPhaden et al. (Science 2006),
|
|
1388
|
+
Saji et al. (Nature 1999), and Caesar et al. (Nature Geoscience 2021).
|
|
1389
|
+
"""
|
|
1390
|
+
oni = float(enso_oni_c)
|
|
1391
|
+
dmi = float(iod_dmi_c)
|
|
1392
|
+
pdo = float(pdo_index)
|
|
1393
|
+
amo = float(amo_index_c)
|
|
1394
|
+
nao = float(nao_index)
|
|
1395
|
+
sam = float(sam_index)
|
|
1396
|
+
amoc = max(0.0, float(amoc_26n_sv))
|
|
1397
|
+
|
|
1398
|
+
if oni >= 1.5:
|
|
1399
|
+
enso_phase = "STRONG_EL_NINO (ONI >= +1.5 C)"
|
|
1400
|
+
elif oni >= 0.5:
|
|
1401
|
+
enso_phase = "EL_NINO (+0.5 <= ONI < +1.5 C)"
|
|
1402
|
+
elif oni <= -1.5:
|
|
1403
|
+
enso_phase = "STRONG_LA_NINA (ONI <= -1.5 C)"
|
|
1404
|
+
elif oni <= -0.5:
|
|
1405
|
+
enso_phase = "LA_NINA (-1.5 < ONI <= -0.5 C)"
|
|
1406
|
+
else:
|
|
1407
|
+
enso_phase = "ENSO_NEUTRAL (-0.5 < ONI < +0.5 C)"
|
|
1408
|
+
|
|
1409
|
+
iod_phase = "POSITIVE_IOD" if dmi >= 0.4 else ("NEGATIVE_IOD" if dmi <= -0.4 else "NEUTRAL_IOD")
|
|
1410
|
+
amoc_deficit_pct = max(0.0, ((18.5 - amoc) / 18.5) * 100.0)
|
|
1411
|
+
amoc_status = "WEAKENED_OVERTURNING_WARNING (<15 Sv)" if amoc < 15.0 else "NORMAL_RANGE (15-20 Sv)"
|
|
1412
|
+
|
|
1413
|
+
compound_forcing = min(
|
|
1414
|
+
100.0,
|
|
1415
|
+
28.0 * min(abs(oni) / 2.0, 1.5)
|
|
1416
|
+
+ 18.0 * min(abs(dmi) / 1.0, 1.5)
|
|
1417
|
+
+ 16.0 * min(abs(amo) / 0.5, 1.5)
|
|
1418
|
+
+ 18.0 * min(amoc_deficit_pct / 20.0, 1.5)
|
|
1419
|
+
+ 10.0 * min(abs(nao) / 2.0, 1.5)
|
|
1420
|
+
+ 10.0 * min(abs(sam) / 2.0, 1.5),
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
return {
|
|
1424
|
+
"enso_oni_c": round(oni, 3),
|
|
1425
|
+
"enso_phase": enso_phase,
|
|
1426
|
+
"iod_dmi_c": round(dmi, 3),
|
|
1427
|
+
"iod_phase": iod_phase,
|
|
1428
|
+
"pdo_index": round(pdo, 3),
|
|
1429
|
+
"amo_index_c": round(amo, 3),
|
|
1430
|
+
"nao_index": round(nao, 3),
|
|
1431
|
+
"sam_index": round(sam, 3),
|
|
1432
|
+
"amoc_rapid_26n_sv": round(amoc, 2),
|
|
1433
|
+
"amoc_historical_deficit_pct": round(amoc_deficit_pct, 2),
|
|
1434
|
+
"amoc_regime_status": amoc_status,
|
|
1435
|
+
"compound_teleconnection_forcing_index_0_100": round(compound_forcing, 2),
|
|
1436
|
+
"reference": "McPhaden et al. (2006, Science); Saji et al. (1999, Nature); Caesar et al. (2021, Nat. Geosci.)",
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def coral_bleaching_dhw_risk(
|
|
1441
|
+
dhw_c_weeks: float = 8.6,
|
|
1442
|
+
hotspot_sst_anomaly_c: float = 1.45,
|
|
1443
|
+
omega_aragonite: float = 3.15,
|
|
1444
|
+
live_coral_cover_pct: float = 28.5,
|
|
1445
|
+
) -> Dict[str, Any]:
|
|
1446
|
+
"""
|
|
1447
|
+
Compute NOAA Coral Reef Watch (CRW v3.1) Bleaching Alert Level (0 to Alert Level 5),
|
|
1448
|
+
Aragonite Calcification Impedance, and Reef Ecological Collapse Probability
|
|
1449
|
+
(Liu et al., Remote Sensing 2014; Hughes et al., Nature 2017/2018; Kleypas et al., Science 1999).
|
|
1450
|
+
"""
|
|
1451
|
+
dhw = max(0.0, float(dhw_c_weeks))
|
|
1452
|
+
hs = float(hotspot_sst_anomaly_c)
|
|
1453
|
+
omega = max(0.5, float(omega_aragonite))
|
|
1454
|
+
cover = max(0.0, min(100.0, float(live_coral_cover_pct)))
|
|
1455
|
+
|
|
1456
|
+
if hs <= 0.0:
|
|
1457
|
+
alert_level = "NO_STRESS (HotSpot <= 0 C)"
|
|
1458
|
+
elif hs < 1.0:
|
|
1459
|
+
alert_level = "BLEACHING_WATCH (0 < HotSpot < 1 C)"
|
|
1460
|
+
elif dhw < 4.0:
|
|
1461
|
+
alert_level = "BLEACHING_WARNING (HotSpot >= 1 C, 0 < DHW < 4)"
|
|
1462
|
+
elif dhw < 8.0:
|
|
1463
|
+
alert_level = "ALERT_LEVEL_1_SIGNIFICANT_BLEACHING (4 <= DHW < 8 C-weeks)"
|
|
1464
|
+
elif dhw < 12.0:
|
|
1465
|
+
alert_level = "ALERT_LEVEL_2_SEVERE_MORTALITY (8 <= DHW < 12 C-weeks)"
|
|
1466
|
+
elif dhw < 16.0:
|
|
1467
|
+
alert_level = "ALERT_LEVEL_3_MULTI_SPECIES_MORTALITY (12 <= DHW < 16 C-weeks)"
|
|
1468
|
+
elif dhw < 20.0:
|
|
1469
|
+
alert_level = "ALERT_LEVEL_4_SEVERE_MULTI_SPECIES_MORTALITY (16 <= DHW < 20 C-weeks)"
|
|
1470
|
+
else:
|
|
1471
|
+
alert_level = "ALERT_LEVEL_5_NEAR_COMPLETE_MORTALITY (DHW >= 20 C-weeks)"
|
|
1472
|
+
|
|
1473
|
+
calcification_penalty_pct = max(0.0, min(100.0, ((3.5 - omega) / 2.5) * 100.0))
|
|
1474
|
+
mortality_risk = min(100.0, 0.60 * min(100.0, (dhw / 16.0) * 100.0) + 0.25 * calcification_penalty_pct + 0.15 * max(0.0, (40.0 - cover) * 2.0))
|
|
1475
|
+
|
|
1476
|
+
return {
|
|
1477
|
+
"dhw_c_weeks": round(dhw, 2),
|
|
1478
|
+
"hotspot_sst_anomaly_c": round(hs, 2),
|
|
1479
|
+
"omega_aragonite": round(omega, 3),
|
|
1480
|
+
"live_coral_cover_pct": round(cover, 2),
|
|
1481
|
+
"noaa_crw_alert_status": alert_level,
|
|
1482
|
+
"calcification_depression_pct": round(calcification_penalty_pct, 2),
|
|
1483
|
+
"reef_mortality_risk_index_0_100": round(mortality_risk, 2),
|
|
1484
|
+
"reference": "Liu et al. (2014, Remote Sens.); Hughes et al. (2018, Nature); NOAA Coral Reef Watch v3.1",
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
def lme_trophic_hypoxia_stress(
|
|
1489
|
+
primary_productivity_mgc_m2_d: float = 680.0,
|
|
1490
|
+
fish_landings_mt_yr: float = 1.45,
|
|
1491
|
+
mean_trophic_level: float = 3.24,
|
|
1492
|
+
hypoxic_shelf_area_km2: float = 14500.0,
|
|
1493
|
+
sst_anomaly_c: float = 0.92,
|
|
1494
|
+
) -> Dict[str, Any]:
|
|
1495
|
+
"""
|
|
1496
|
+
Evaluate UN Large Marine Ecosystem (LME) Trophic Exploitation, Pauly & Christensen
|
|
1497
|
+
(1995) Primary Production Required (PPR), Fishing-down-Marine-Food-Webs status,
|
|
1498
|
+
and Shelf Deoxygenation Stress across the 66 global LMEs (Pauly et al., Science 1998;
|
|
1499
|
+
Sherman & Hempel, UNDP/GEF/NOAA 2008; Breitburg et al., Science 2018).
|
|
1500
|
+
"""
|
|
1501
|
+
pp = max(50.0, float(primary_productivity_mgc_m2_d))
|
|
1502
|
+
catch_mt = max(0.001, float(fish_landings_mt_yr))
|
|
1503
|
+
mtl = max(2.0, min(5.0, float(mean_trophic_level)))
|
|
1504
|
+
hyp_km2 = max(0.0, float(hypoxic_shelf_area_km2))
|
|
1505
|
+
sst_a = float(sst_anomaly_c)
|
|
1506
|
+
|
|
1507
|
+
# Pauly & Christensen (1995): PPR = Catch * (1/9) * 10^(MTL - 1) (with 10% trophic transfer efficiency)
|
|
1508
|
+
ppr_mt_c = (catch_mt / 9.0) * (10.0 ** (mtl - 1.0))
|
|
1509
|
+
trophic_depletion_score = max(0.0, min(100.0, ((3.65 - mtl) / 1.15) * 100.0))
|
|
1510
|
+
hypoxia_score = min(100.0, (math.log1p(hyp_km2) / math.log1p(80000.0)) * 100.0)
|
|
1511
|
+
thermal_score = min(100.0, max(0.0, (sst_a / 2.2) * 100.0))
|
|
1512
|
+
composite_lme_stress = 0.40 * trophic_depletion_score + 0.35 * hypoxia_score + 0.25 * thermal_score
|
|
1513
|
+
|
|
1514
|
+
return {
|
|
1515
|
+
"primary_productivity_mgc_m2_d": round(pp, 2),
|
|
1516
|
+
"fish_landings_mt_yr": round(catch_mt, 3),
|
|
1517
|
+
"mean_trophic_level_mtl": round(mtl, 3),
|
|
1518
|
+
"primary_production_required_mt_c_yr": round(ppr_mt_c, 3),
|
|
1519
|
+
"hypoxic_shelf_area_km2": round(hyp_km2, 1),
|
|
1520
|
+
"trophic_web_erosion_score_0_100": round(trophic_depletion_score, 2),
|
|
1521
|
+
"shelf_hypoxia_score_0_100": round(hypoxia_score, 2),
|
|
1522
|
+
"composite_lme_ecosystem_stress_0_100": round(composite_lme_stress, 2),
|
|
1523
|
+
"reference": "Pauly et al. (1998, Science); Sherman & Hempel (2008, UN LME Report); Breitburg et al. (2018, Science)",
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def river_delta_plastic_nutrient_export(
|
|
1528
|
+
discharge_km3_yr: float = 420.0,
|
|
1529
|
+
plastic_export_tonnes_yr: float = 28500.0,
|
|
1530
|
+
din_export_kt_n_yr: float = 540.0,
|
|
1531
|
+
dip_export_kt_p_yr: float = 48.0,
|
|
1532
|
+
relative_subsidence_mm_yr: float = 8.4,
|
|
1533
|
+
) -> Dict[str, Any]:
|
|
1534
|
+
"""
|
|
1535
|
+
Compute Global River-Delta Macro-Plastic Emission Density, GLOBAL NEWS-2
|
|
1536
|
+
Dissolved Inorganic N:P Molar Stoichiometry Deviation from Redfield (16:1),
|
|
1537
|
+
and Deltaic Subsidence-Drowning Risk across the top 50 river basins (Meijer et al.,
|
|
1538
|
+
Sci. Adv. 2021; Mayorga et al., Environ. Model. Softw. 2010; Syvitski et al., Nat. Geosci. 2009).
|
|
1539
|
+
"""
|
|
1540
|
+
q_km3 = max(0.1, float(discharge_km3_yr))
|
|
1541
|
+
plast_t = max(0.0, float(plastic_export_tonnes_yr))
|
|
1542
|
+
din_kt = max(0.01, float(din_export_kt_n_yr))
|
|
1543
|
+
dip_kt = max(0.001, float(dip_export_kt_p_yr))
|
|
1544
|
+
sub_mm = max(0.0, float(relative_subsidence_mm_yr))
|
|
1545
|
+
|
|
1546
|
+
# Molar N:P ratio = (mass_N / 14.007) / (mass_P / 30.974)
|
|
1547
|
+
np_molar = (din_kt / 14.007) / (dip_kt / 30.974)
|
|
1548
|
+
redfield_excess_ratio = np_molar / 16.0
|
|
1549
|
+
plastic_conc_g_m3 = (plast_t * 1e6) / (q_km3 * 1e9)
|
|
1550
|
+
|
|
1551
|
+
s_plast = min(100.0, (math.log1p(plast_t) / math.log1p(120000.0)) * 100.0)
|
|
1552
|
+
s_eutro = min(100.0, (abs(np_molar - 16.0) / 48.0) * 100.0)
|
|
1553
|
+
s_subs = min(100.0, (sub_mm / 25.0) * 100.0)
|
|
1554
|
+
delta_vulnerability = 0.40 * s_plast + 0.35 * s_eutro + 0.25 * s_subs
|
|
1555
|
+
|
|
1556
|
+
return {
|
|
1557
|
+
"discharge_km3_yr": round(q_km3, 2),
|
|
1558
|
+
"plastic_export_tonnes_yr": round(plast_t, 1),
|
|
1559
|
+
"mean_plastic_concentration_g_m3": round(plastic_conc_g_m3, 6),
|
|
1560
|
+
"din_export_kt_n_yr": round(din_kt, 2),
|
|
1561
|
+
"dip_export_kt_p_yr": round(dip_kt, 2),
|
|
1562
|
+
"estuarine_din_dip_molar_ratio": round(np_molar, 2),
|
|
1563
|
+
"redfield_16_1_deviation_multiple": round(redfield_excess_ratio, 2),
|
|
1564
|
+
"relative_subsidence_mm_yr": round(sub_mm, 2),
|
|
1565
|
+
"delta_anthropogenic_pressure_index_0_100": round(delta_vulnerability, 2),
|
|
1566
|
+
"reference": "Meijer et al. (2021, Sci. Adv.); Mayorga et al. (2010, GLOBAL NEWS-2); Syvitski et al. (2009, Nat. Geosci.)",
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
def sids_slr_cyclone_vulnerability(
|
|
1571
|
+
coastal_pop_below_5m_pct: float = 64.0,
|
|
1572
|
+
annual_cyclone_damage_gdp_pct: float = 3.2,
|
|
1573
|
+
eez_to_land_ratio: float = 3080.0,
|
|
1574
|
+
blue_economy_gdp_share_pct: float = 48.5,
|
|
1575
|
+
adaptation_finance_usd_capita: float = 185.0,
|
|
1576
|
+
) -> Dict[str, Any]:
|
|
1577
|
+
"""
|
|
1578
|
+
Compute the UN-OHRLLS / AOSIS Small Island Developing States (SIDS) Multidimensional
|
|
1579
|
+
Climate-Ocean Vulnerability & Blue Economy Resilience Index across 38 UN SIDS nations
|
|
1580
|
+
(IPCC AR6 WGII Ch. 15 Small Islands 2022; UN SIDS4 Antigua and Barbuda Agenda 2024).
|
|
1581
|
+
"""
|
|
1582
|
+
pop5m = max(0.0, min(100.0, float(coastal_pop_below_5m_pct)))
|
|
1583
|
+
cyc_loss = max(0.0, float(annual_cyclone_damage_gdp_pct))
|
|
1584
|
+
eez_ratio = max(1.0, float(eez_to_land_ratio))
|
|
1585
|
+
blue_gdp = max(0.0, min(100.0, float(blue_economy_gdp_share_pct)))
|
|
1586
|
+
fin_cap = max(0.0, float(adaptation_finance_usd_capita))
|
|
1587
|
+
|
|
1588
|
+
exposure_score = min(100.0, 0.55 * pop5m + 0.45 * min(100.0, cyc_loss * 12.5))
|
|
1589
|
+
ocean_stewardship_leverage = min(100.0, (math.log10(eez_ratio) / 4.5) * 60.0 + 0.40 * blue_gdp)
|
|
1590
|
+
adaptation_buffer = min(100.0, (math.log1p(fin_cap) / math.log1p(500.0)) * 100.0)
|
|
1591
|
+
net_sids_vulnerability = max(0.0, min(100.0, 0.65 * exposure_score + 0.20 * (100.0 - adaptation_buffer) + 0.15 * (100.0 - ocean_stewardship_leverage)))
|
|
1592
|
+
|
|
1593
|
+
return {
|
|
1594
|
+
"coastal_pop_below_5m_pct": round(pop5m, 2),
|
|
1595
|
+
"annual_cyclone_damage_gdp_pct": round(cyc_loss, 2),
|
|
1596
|
+
"eez_to_land_ratio": round(eez_ratio, 1),
|
|
1597
|
+
"blue_economy_gdp_share_pct": round(blue_gdp, 2),
|
|
1598
|
+
"adaptation_finance_usd_capita": round(fin_cap, 2),
|
|
1599
|
+
"slr_cyclone_exposure_score_0_100": round(exposure_score, 2),
|
|
1600
|
+
"ocean_stewardship_leverage_0_100": round(ocean_stewardship_leverage, 2),
|
|
1601
|
+
"net_sids_vulnerability_index_0_100": round(net_sids_vulnerability, 2),
|
|
1602
|
+
"reference": "IPCC AR6 WGII Ch. 15 (Mycoo et al., 2022); UN SIDS4 ABAS Agenda (2024); Sachs et al. (UN MVI, 2024)",
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
|
|
1606
|
+
def polar_cryosphere_basal_melt_albedo(
|
|
1607
|
+
sea_ice_extent_mkm2: float = 0.72,
|
|
1608
|
+
ice_thickness_m: float = 1.65,
|
|
1609
|
+
basal_melt_rate_m_yr: float = 14.5,
|
|
1610
|
+
albedo_radiative_forcing_wm2: float = 3.85,
|
|
1611
|
+
) -> Dict[str, Any]:
|
|
1612
|
+
"""
|
|
1613
|
+
Evaluate Polar Cryosphere Sea-Ice Volume Anomaly, Ice-Shelf Basal Melt Freshwater Flux
|
|
1614
|
+
(Gt/yr), and Ice-Albedo Radiative Amplification across 20 Arctic & Antarctic basins
|
|
1615
|
+
(Rignot et al., PNAS 2019; Paolo et al., Science 2015; NSIDC / CryoSat-2 / ICESat-2 2026).
|
|
1616
|
+
"""
|
|
1617
|
+
ext = max(0.01, float(sea_ice_extent_mkm2))
|
|
1618
|
+
thick = max(0.05, float(ice_thickness_m))
|
|
1619
|
+
melt_m = max(0.0, float(basal_melt_rate_m_yr))
|
|
1620
|
+
forcing = max(0.0, float(albedo_radiative_forcing_wm2))
|
|
1621
|
+
|
|
1622
|
+
# Sea-ice volume in thousand km3: extent (10^6 km2) * thickness (m / 1000 km) * 1000
|
|
1623
|
+
ice_volume_kkm3 = ext * thick
|
|
1624
|
+
# Effective ice-shelf fringe area ~ 6% of basin extent -> basal freshwater flux in Gt/yr (rho_ice = 0.917 Gt/km3)
|
|
1625
|
+
basal_melt_flux_gt_yr = ext * 0.06 * melt_m * 0.917
|
|
1626
|
+
cryo_instability_score = min(100.0, 0.45 * min(100.0, (melt_m / 35.0) * 100.0) + 0.35 * min(100.0, (forcing / 6.5) * 100.0) + 0.20 * max(0.0, (3.0 - thick) / 3.0 * 100.0))
|
|
1627
|
+
|
|
1628
|
+
return {
|
|
1629
|
+
"sea_ice_extent_mkm2": round(ext, 3),
|
|
1630
|
+
"mean_ice_thickness_m": round(thick, 3),
|
|
1631
|
+
"estimated_sea_ice_volume_kkm3": round(ice_volume_kkm3, 3),
|
|
1632
|
+
"basal_melt_rate_m_yr": round(melt_m, 2),
|
|
1633
|
+
"estimated_basal_freshwater_flux_gt_yr": round(basal_melt_flux_gt_yr, 2),
|
|
1634
|
+
"albedo_radiative_forcing_wm2": round(forcing, 2),
|
|
1635
|
+
"cryosphere_instability_index_0_100": round(cryo_instability_score, 2),
|
|
1636
|
+
"reference": "Rignot et al. (2019, PNAS); Kacimi & Kwok (2022, Cryosphere); NSIDC / CryoSat-2 / ICESat-2 (2026)",
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
|