From d18e1ac1007832b891d72ebe265eda300113a17a Mon Sep 17 00:00:00 2001 From: Aditeya Shukla Date: Mon, 5 Jan 2026 18:34:09 +0530 Subject: [PATCH 1/3] added 2box temp response model+test, not using config yet --- src/AEIC/environment/temperature_response.py | 122 +++++++ tests/test_environment.py | 333 +++++++++++++++++++ 2 files changed, 455 insertions(+) create mode 100644 src/AEIC/environment/temperature_response.py create mode 100644 tests/test_environment.py diff --git a/src/AEIC/environment/temperature_response.py b/src/AEIC/environment/temperature_response.py new file mode 100644 index 00000000..41cbd275 --- /dev/null +++ b/src/AEIC/environment/temperature_response.py @@ -0,0 +1,122 @@ +from dataclasses import dataclass +from functools import cached_property + +import numpy as np + + +# TODO: Import this into environment config and full Config +@dataclass +class deltaT_config: + # Specific heat of liquid water + c_water = 4.2e3 # J*K^-1*kg^-1 + # Density of water + rho = 1000 # kg/m^3 + + # Specific heat of Mixed Layer (70m) + specific_heat = 441333333.33 # J*K^-1*m^-2 + # Heat capacity of the Mixed Layer + C1 = specific_heat * 0.71 # J*K^-1*m^-2 + # Heat Capacity of Deep Ocean (3000m) + C2 = 14700000000 # J*K^-1*m^-2 + + # Equilibrium Climate Sensitivity (ECS) + # The equilibrium temperature change (K or C) that results from doubling CO2 + # Typical range**: 1.5 - 4.5 K (IPCC AR5) + ECS = 4.0 # K + # Radiative Forcing from CO₂ Doubling + RF2xCO2 = 3.93 # W/m^2 + + # Advective mass flux of water from boundary layer + advective_mass = 1.435e-4 # kg*m^-2*s^-1 + # Diffusion coefficient for turbulent Mixing + diffuse_coeff = 7e-5 # m^2*s^-1 + # Mixing depth + z = 1166.6667 # m + + @cached_property + def lambda_climate(self): + """ + Climate Sensitivity Parameter: Temperature response per unit radiative forcing + units: K/Wm^-2 + """ + return self.ECS / self.RF2xCO2 + + @cached_property + def tau_deep(self): + """ + Ocean Mixed Layer Response Time + units: seconds + """ + return self.C1 * self.lambda_climate + + @cached_property + def alpha_1(self): + """ + Mixed Layer Heat Exchange Coefficient + Rate of heat exchange between the surface mixed layer and deep ocean. + units: seconds^-1 + """ + return self._get_alpha(self.C1) + + @cached_property + def alpha_2(self): + """ + Deep Ocean Heat Exchange Coefficient + Rate of heat exchange between deep ocean and surface mixed layer + units: seconds^-1 + """ + return self._get_alpha(self.C2) + + def _get_alpha(self, C): + return ( + self.c_water + / C + * (self.advective_mass + (self.diffuse_coeff * self.rho) / self.z) + ) + + +@dataclass(frozen=True) +class deltaT_2box_output: + deltaT_surface: np.ndarray + deltaT_deep: np.ndarray + + +def deltaT_2box(RF, config): + """ + Two-box temperature response model with: + + Mixed layer (ocean surface, ~70m): heat capacity C1 + Deep ocean (~3000m): heat capacity C2 + Climate sensitivity: lambda_climate = ECS / RF2xCO2 (K/(W/m²)) + Heat exchange: advection + diffusion + + For each forcer: + + Calculates surface temperature change: ΔT_surface + Calculates deep ocean temperature change: ΔT_deep + Uses iterative forward stepping through time + """ + years = len(RF) + sec_per_yr = 60 * 60 * 24 * 365.25 # seconds per year + + # Initialize arrays + deltaT_surface = np.zeros_like(RF) + deltaT_deep = np.zeros_like(RF) + + # Main calculation loop + for j in range(years - 1): + deltaT_surface[j + 1] = ( + sec_per_yr + * ( + RF[j] / config.C1 + - deltaT_surface[j] / config.tau_deep + - config.alpha_1 * (deltaT_surface[j] - deltaT_deep[j]) + ) + + deltaT_surface[j] + ) + deltaT_deep[j + 1] = ( + sec_per_yr * config.alpha_2 * (deltaT_surface[j] - deltaT_deep[j]) + + deltaT_deep[j] + ) + + return deltaT_2box_output(deltaT_surface=deltaT_surface, deltaT_deep=deltaT_deep) diff --git a/tests/test_environment.py b/tests/test_environment.py new file mode 100644 index 00000000..4b9be85e --- /dev/null +++ b/tests/test_environment.py @@ -0,0 +1,333 @@ +import numpy as np + +from AEIC.environment.temperature_response import deltaT_2box, deltaT_config + + +def test_2box_deltaT(): + # Unit test for 2box temperature response model using CO2 RF 2019 + RF_testing = np.array( + [ + 0.00154937, + 0.001447, + 0.00136368, + 0.00129551, + 0.00123936, + 0.00119277, + 0.00115381, + 0.00112093, + 0.0010929, + 0.00106878, + 0.00104777, + 0.00102929, + 0.00101286, + 0.00099808, + 0.00098465, + 0.00097235, + 0.00096096, + 0.00095035, + 0.00094039, + 0.00093098, + 0.00092204, + 0.00091352, + 0.00090535, + 0.00089751, + 0.00088995, + 0.00088265, + 0.00087558, + 0.00086874, + 0.0008621, + 0.00085564, + 0.00084937, + 0.00084326, + 0.00083732, + 0.00083153, + 0.00082588, + 0.00082038, + 0.00081501, + 0.00080977, + 0.00080466, + 0.00079967, + 0.0007948, + 0.00079004, + 0.00078539, + 0.00078086, + 0.00077642, + 0.00077209, + 0.00076786, + 0.00076372, + 0.00075968, + 0.00075573, + 0.00075187, + 0.00074809, + 0.0007444, + 0.00074079, + 0.00073726, + 0.0007338, + 0.00073042, + 0.00072712, + 0.00072388, + 0.00072072, + 0.00071762, + 0.00071459, + 0.00071162, + 0.00070871, + 0.00070586, + 0.00070308, + 0.00070035, + 0.00069767, + 0.00069505, + 0.00069249, + 0.00068997, + 0.00068751, + 0.00068509, + 0.00068272, + 0.0006804, + 0.00067812, + 0.00067589, + 0.0006737, + 0.00067155, + 0.00066944, + 0.00066738, + 0.00066535, + 0.00066335, + 0.0006614, + 0.00065948, + 0.00065759, + 0.00065574, + 0.00065392, + 0.00065213, + 0.00065038, + 0.00064865, + 0.00064696, + 0.00064529, + 0.00064365, + 0.00064204, + 0.00064045, + 0.00063889, + 0.00063736, + 0.00063585, + 0.00063436, + ] + ) + + testdT1 = np.array( + [ + 0.0, + 0.00015604, + 0.0002729, + 0.00035977, + 0.00042374, + 0.00047028, + 0.00050358, + 0.00052687, + 0.00054262, + 0.00055272, + 0.0005586, + 0.00056136, + 0.00056184, + 0.00056067, + 0.0005583, + 0.00055512, + 0.00055136, + 0.00054725, + 0.00054291, + 0.00053845, + 0.00053395, + 0.00052947, + 0.00052504, + 0.00052069, + 0.00051644, + 0.00051228, + 0.00050824, + 0.00050432, + 0.00050051, + 0.00049681, + 0.00049322, + 0.00048973, + 0.00048635, + 0.00048307, + 0.00047989, + 0.0004768, + 0.00047379, + 0.00047088, + 0.00046804, + 0.00046529, + 0.00046261, + 0.00046, + 0.00045746, + 0.000455, + 0.0004526, + 0.00045026, + 0.00044798, + 0.00044577, + 0.00044361, + 0.00044151, + 0.00043946, + 0.00043747, + 0.00043553, + 0.00043363, + 0.00043179, + 0.00042999, + 0.00042824, + 0.00042654, + 0.00042487, + 0.00042325, + 0.00042167, + 0.00042013, + 0.00041863, + 0.00041716, + 0.00041573, + 0.00041434, + 0.00041298, + 0.00041166, + 0.00041036, + 0.0004091, + 0.00040787, + 0.00040668, + 0.00040551, + 0.00040436, + 0.00040325, + 0.00040216, + 0.0004011, + 0.00040007, + 0.00039906, + 0.00039807, + 0.00039711, + 0.00039617, + 0.00039525, + 0.00039436, + 0.00039348, + 0.00039263, + 0.0003918, + 0.00039098, + 0.00039019, + 0.00038941, + 0.00038865, + 0.00038791, + 0.00038719, + 0.00038648, + 0.00038579, + 0.00038511, + 0.00038445, + 0.00038381, + 0.00038317, + 0.00038256, + ] + ) + testdT2 = np.array( + [ + 0.00000000e00, + 0.00000000e00, + 2.86308581e-07, + 7.86507869e-07, + 1.44518275e-06, + 2.22003096e-06, + 3.07884848e-06, + 3.99719582e-06, + 4.95659320e-06, + 5.94312672e-06, + 6.94637303e-06, + 7.95857112e-06, + 8.97398530e-06, + 9.98841604e-06, + 1.09988251e-05, + 1.20030488e-05, + 1.29995790e-05, + 1.39873968e-05, + 1.49658457e-05, + 1.59345360e-05, + 1.68932726e-05, + 1.78420005e-05, + 1.87807646e-05, + 1.97096789e-05, + 2.06289048e-05, + 2.15386344e-05, + 2.24390785e-05, + 2.33304581e-05, + 2.42129984e-05, + 2.50869245e-05, + 2.59524583e-05, + 2.68098170e-05, + 2.76592117e-05, + 2.85008464e-05, + 2.93349185e-05, + 3.01616180e-05, + 3.09811278e-05, + 3.17936241e-05, + 3.25992765e-05, + 3.33982483e-05, + 3.41906970e-05, + 3.49767743e-05, + 3.57566268e-05, + 3.65303957e-05, + 3.72982180e-05, + 3.80602257e-05, + 3.88165469e-05, + 3.95673055e-05, + 4.03126216e-05, + 4.10526117e-05, + 4.17873888e-05, + 4.25170625e-05, + 4.32417394e-05, + 4.39615228e-05, + 4.46765132e-05, + 4.53868084e-05, + 4.60925031e-05, + 4.67936899e-05, + 4.74904584e-05, + 4.81828960e-05, + 4.88710877e-05, + 4.95551161e-05, + 5.02350618e-05, + 5.09110030e-05, + 5.15830160e-05, + 5.22511750e-05, + 5.29155522e-05, + 5.35762179e-05, + 5.42332406e-05, + 5.48866870e-05, + 5.55366221e-05, + 5.61831090e-05, + 5.68262094e-05, + 5.74659831e-05, + 5.81024887e-05, + 5.87357829e-05, + 5.93659213e-05, + 5.99929578e-05, + 6.06169451e-05, + 6.12379342e-05, + 6.18559751e-05, + 6.24711166e-05, + 6.30834058e-05, + 6.36928890e-05, + 6.42996112e-05, + 6.49036162e-05, + 6.55049467e-05, + 6.61036443e-05, + 6.66997496e-05, + 6.72933020e-05, + 6.78843403e-05, + 6.84729018e-05, + 6.90590232e-05, + 6.96427401e-05, + 7.02240874e-05, + 7.08030990e-05, + 7.13798080e-05, + 7.19542464e-05, + 7.25264459e-05, + 7.30964370e-05, + ] + ) + + config = deltaT_config() + + assert config.C1 == 313346666.66429996 + assert config.C2 == 14700000000 + assert config.advective_mass == 0.0001435 + assert config.diffuse_coeff == 7e-05 + assert config.z == 1166.6667 + assert config.lambda_climate == 1.0178117048346056 + + output = deltaT_2box(RF_testing, config) + assert np.allclose(output.deltaT_surface, testdT1, rtol=1e-4) + assert np.allclose(output.deltaT_deep, testdT2, rtol=1e-4) From 827dcd3eda8d65f4722b5ecd76829fb6b788e7d1 Mon Sep 17 00:00:00 2001 From: Aditeya Shukla Date: Tue, 13 Jan 2026 21:11:21 +0530 Subject: [PATCH 2/3] added DESIGN_NOTES --- src/AEIC/environment/DESIGN_NOTES.md | 315 +++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 src/AEIC/environment/DESIGN_NOTES.md diff --git a/src/AEIC/environment/DESIGN_NOTES.md b/src/AEIC/environment/DESIGN_NOTES.md new file mode 100644 index 00000000..c2973cf6 --- /dev/null +++ b/src/AEIC/environment/DESIGN_NOTES.md @@ -0,0 +1,315 @@ +# Environment Module + +## Purpose + +The goal of the environment module is to get detailed and all possible climate and air-quality impact metrics from the emissions output for a choice of configuration parameters, background scenarios, discount rates, etc. + +The aim is to get all climate and AQ metrics from individual components available for different kinds of messaging. Certain users/audiences would prefer GWP or deltaT over Net Present Value/monetized damages. + +While the module has all the pipeline to calculate all the metrics needed (list later in the doc), it also has the ability to switch out with external modules. For example, there is a way to quickly calculate RF from contrails using an estimate of RF/km contrail. But if the user wants they can also choose to calculate contrail impacts using PyContrails. + +--- + +## High-Level Flow + +### Climate + +``` +Emissions + ↓ +Radiative Forcing (RF) + ↓ +Temperature Change (ΔT) + ↓ +Climate Metrics (GWP, TP, ATR, CO2e) + ↓ +Damages ($) + ↓ +Discounting + ↓ +Net Present Value (NPV) +``` + +### Air Quality + +``` +Emissions + ↓ +Pollutant Concentration + ↓ +Concentration Response Functions + ↓ +Mortalities + ↓ +Value of Statistical Life + ↓ +Discounting + ↓ +Net Present Value (NPV) +``` + +Climate and air-quality paths can be thought of seperately and are merged only at the monetization/NPV level. + +--- + +## Simplest Usage + +```python +env_config = config.environment() + +env = AEIC.environment.EnvironmentClass(config=env_config) + +# Radiative forcing only +rf = env.climate.emit_RF(emissions=em) + +# Full pipeline +out = env.emit(emissions=em) + +print(f""" +RF CO2 (year 1): {rf.CO2[0]} W/m^2 +ΔT CO2 (year 1): {out.climate.deltaT.CO2[0]} K +NPV NOx damages: {out.climate.NPV.NOx} +NPV total climate: {out.climate.NPV.total} +""") +``` + +--- + +## Supported Use Cases + +### 1. Configuration Sensitivity + +```python + +RE_CO2_options = [1.0, 1.1, 0.9] + +results = [] +for RE_CO2_i in RE_CO2_options: + env_config = config.environment(RE_CO2 = RE_CO2_i) + env = EnvironmentClass(config=env_config) + results.append(env.emit(emissions=em)) + +for i, r in enumerate(results): + print(f"Config {i}: NPV = {r.climate.NPV.total}") +``` + +--- + +### 2. Swapping Physical Models + +```python +# Contrails +env_config = config.environment( + contrail_model="pycontrails" # default: "simple") +) +env = EnvironmentClass( + config=env_config, +) + +# AQ adjoint sensitivities +env_config = config.environment( + adjoint_sens_file="custom_adjoints.nc" +) +env = EnvironmentClass( + config=env_config +) +``` + +--- + +### 3. Partial Pipelines + +```python +climate_results = env.emit_climate(emissions=em) # climate only +AQ_results = env.emit_AQ(emissions=em) # AQ only +GWP_results = env.climate.get_GWP(emissions=em, time_horizon=100) +``` + +--- + +## Core Data Model + +### Dimensional Convention + +| Dimension | Meaning | +| ----------- | --------------------------------------------- | +| `forcer`. | Forcing agent (CO2, contrails, O3, PM, etc.) | +| `time` | Years since emission (annual resolution) | + +**DIMENSIONS:** +All time-resolved outputs are shaped as: + +``` +(forcer × time) +``` + +--- + +### Emissions Input + +**Class:** `EmissionsOutput` + +| Field | Units | Shape | +| ------------- | ----- | ------------------- | +| fuel_burn | kg | (t_emit,) | +| CO2 | kg | (t_emit,) | +| NOx | kg | (t_emit,) | +| PM | kg | (t_emit,) | +| H2O | kg | (t_emit,) | +| flight_km | km | (t_emit,) | +| CO2_lifecycle | kg | (t_emit,) | + +Optional (for pycontrails): + +**Class:** `Trajectory` + +--- + +## Configuration (`EnvironmentConfig`) + +### ClimateConfig + +| Parameter | Default | +| ---------------------- | --------- | +| radiative_efficacy_CO2 | 1.0 | +| CO2_IRF_source | Joos2013 | +| climate_sensitivity | 3.0 K | +| contrail_model | simple | +| time_horizon | 100 years | +| time_step | 1 year | + +### MonetizationConfig + +| Parameter | Default | +| --------------- | -------- | +| discount_rate | 3% | +| discount_type | constant | +| damage_function | DICE | +| background_scenario | SSP2-4.5 | + +### Air Quality + +| Parameter | Default | +| ------------------- | ----------- | +| VSL | $10M | +| CRF_source | Burnett2018 | +| adjoint_sens_source | GEOS-Chem | + +--- + +## Outputs (Unified Structure) + +```python +EnvironmentOutput( + climate=ClimateOutput(...), + air_quality=AirQualityOutput(...) +) +``` + +--- + +## Climate Outputs + +### Radiative Forcing — `RFOutput` + +**Shape:** `(component, time)` + +**Components (canonical):** + +| Index | Component | +| ----- | --------------- | +| 1 | CO2 | +| 2 | CO2_background | +| 3 | CO2_lifecycle | +| 4 | O3_short | +| 5 | H2O_short | +| 6 | contrails_short | +| 7 | sulfates_short | +| 8 | soot_short | +| 9 | nitrate_short | +| 10 | O3_long | +| 11 | CH4_long | + +Access: + +```python +out.climate.RF.CO2 +out.climate.RF.total +out.climate.RF.components +``` + +--- + +### Temperature Change — `TemperatureOutput` + +Same structure as RF. + +```python +out.climate.deltaT.CO2 +out.climate.deltaT.total +``` + +--- + +### Damages — `DamageOutput` + +* Climate damages: driven by ΔT +* AQ damages: driven by mortality + +```python +out.climate.damage_costs.CO2 +out.air_quality.damage_costs.PM25 +out.damage_costs.total +``` + +--- + +### Discounted Damages — `DiscountedDamageOutput` + +Same `(component, time)` shape. + +--- + +### Net Present Value — `NPVOutput` + +**Shape:** `(component,)` + +```python +out.climate.NPV.CO2 +out.climate.NPV.NOx +out.NPV.total +``` + +--- + +## Climate Metrics + +### GWP + +Stored per horizon: + +```python +out.climate.GWP_20 +out.climate.GWP_100 +out.climate.GWP_500 +``` + +Derived from GWP + +```python +out.climate.get_CO2e(100) +out.climate.get_AGWP(100) +``` + +--- + +### TP and ATR + +Derived from deltaT + +```python +out.climate.get_TP +out.climate.get_ATR +``` + +--- From 9309178b02bda9af819144eb1a93968bb7c125c6 Mon Sep 17 00:00:00 2001 From: Aditeya Shukla Date: Tue, 13 Jan 2026 21:13:54 +0530 Subject: [PATCH 3/3] added spreadsheet link to design notes --- src/AEIC/environment/DESIGN_NOTES.md | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/AEIC/environment/DESIGN_NOTES.md b/src/AEIC/environment/DESIGN_NOTES.md index c2973cf6..82bf4f32 100644 --- a/src/AEIC/environment/DESIGN_NOTES.md +++ b/src/AEIC/environment/DESIGN_NOTES.md @@ -169,14 +169,8 @@ Optional (for pycontrails): ### ClimateConfig -| Parameter | Default | -| ---------------------- | --------- | -| radiative_efficacy_CO2 | 1.0 | -| CO2_IRF_source | Joos2013 | -| climate_sensitivity | 3.0 K | -| contrail_model | simple | -| time_horizon | 100 years | -| time_step | 1 year | +Making a spreadsheet with detailed info on constants, configs etc here: +https://docs.google.com/spreadsheets/d/1zDOw2smQkYmstu-6Txfnw04_3jfwQ2vSCGcHdnl46XA/edit?usp=sharing ### MonetizationConfig @@ -189,11 +183,7 @@ Optional (for pycontrails): ### Air Quality -| Parameter | Default | -| ------------------- | ----------- | -| VSL | $10M | -| CRF_source | Burnett2018 | -| adjoint_sens_source | GEOS-Chem | +Added on spreadsheet ---