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/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
+