Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 305 additions & 0 deletions src/AEIC/environment/DESIGN_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
# Environment Module
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a good name for this functionality? You're including radiative transfer calculations, climate sensitivity, climate benchmarks, economic calculations, plus pollution and air quality and mortality calculations. That seems like a lot to put under the innocuous name of "environment". Would "impacts module" be better? That's what that "I" in ACAI is for.


## 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the language here ("all possible", "choice of configuration parameters, background scenarios, discount rates, etc.", "all climate and AQ metrics from individual components available for different kinds of message") is too maximalist for my taste. I think you would be much better off starting with a relatively limited number of things you want to calculate, coming up with a good way to organize those calculations and adding the things that need to be in AEIC incrementally as the need arises.

This gets at what we were talking about in the meeting the other day: the functionality that AEIC might present for these kinds of calculations will be the simplest and most straightforward versions of these things. Anyone who really cares about one of these things will do more detailed modelling themself using more domain-appropriate tools. I think that that means that AEIC doesn't need to take such a "do all the things" approach to impacts calculations.


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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you write down some realistic use cases? And identify some of these "certain users/audiences"?


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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a genuine need for this? Would it not be better simply to provide utilities to generate PyContrails-friendly input from AEIC data? Wanting to incorporate all external tools into your own thing is usually a bad idea. It's better to organize things so that you can work nicely with external tools, rather than imposing some sort of software totalitarianism on your users.

Doing what you want to do here also introduces a direct dependency on PyContrails, which really seems out of scope for AEIC. My general feeling about this whole "environment/impacts" module is that you should be implementing a toolkit of the more basic ways of calculating the things that people might be interested in. Anyone who wants to do something more sophisticated will, well, do something more sophisticated.


---

## High-Level Flow

### Climate

```
Emissions
Radiative Forcing (RF)
Temperature Change (ΔT)
Climate Metrics (GWP, TP, ATR, CO2e)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pipeline picture isn't correct, is it? Some of these climate metrics aren't just a function of temperature change. (ATR is, GWP is not, I don't know what TP is, and I don't know what a CO2 equivalent metric is supposed to be here - the amount of CO2 to get a GWP equal to the GWP from the actual emissions?)

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do all of these calculations need to be done by a single class? This looks very much like the design of the emissions module, which had the same approach (one Emissions class with lots of things in it), that I'm in the process of breaking down into a more modular view of things.

Most of the things that need to be calculated here are pure functions. You've even drawn pipelines above that emphasize that. There's really no need for these things to be parts of some overriding class, because there should be no state shared between them.

If you want another perspective on this, think about user choice. By putting everything into one big class, you are removing the possibility from the user to compose the different calculations and tools as they like, or to use them in other tools. It's your way or the highway.


# Radiative forcing only
rf = env.climate.emit_RF(emissions=em)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why emit_RF? That's a weird name. I can understand why you had emit in the emissions module, but why here too?

Also, looking at your EmissionsOutput class below, I assume this is not the output of the emissions module (which calculates emissions per trajectory). It's some sort of aggregated emissions time series, right? So we need the post-processing step to produce those aggregated emissions values before we get to impacts. Is that right?

If so, I guess that means that all of these impacts/environment calculations have no spatial component to them. I don't know very much about this side of climate research, but do people still do this for calculating things like mean radiative forcing or temperature changes? I know that those things are reported as global averages, but aren't the calculations usually done with spatially resolved models and the results averaged afterwards? I know even less about the air quality side of things, but there spatial variability is even more important.


# Full pipeline
out = env.emit(emissions=em)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, why make this monolithic, all in one class? What does that actually buy you? If the idea is for users to use this module programmatically anyway, why not represent the individual calculations as separate functions? If your "pipeline" picture is accurate, there is no shared state between the separate calculations, and the result of stage N can just be passed on to stage N+1.


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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should defer any thoughts about this until we decide how to manage uncertainty quantification in AEIC more generally. The discussion we had in the meeting the other day made it pretty clear that we don't yet know how to do this. What does twiddling individual parameters like in this example actually tell you? Also, since you have everything rolled up in a single class, it's not transparent to a user which of the outputs is affected by varying any particular parameter.


```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:
Copy link
Member

@ian-ross ian-ross Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "shaped as" you mean they're Numpy arrays, don't you? Try to get out of the habit of thinking of every piece of composite data as a Numpy array. In this case, the data you're representing is not a matrix, it's a set of parallel time series. There's some more about this in the comment right above.


```
(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

Making a spreadsheet with detailed info on constants, configs etc here:
https://docs.google.com/spreadsheets/d/1zDOw2smQkYmstu-6Txfnw04_3jfwQ2vSCGcHdnl46XA/edit?usp=sharing

### MonetizationConfig
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I don't think this stuff needs to be in a configuration object. These things can just be parameters to functions, with default values. They're then there right in the place where they're used, so when a user is looking at the documentation about discounting, they see the functions related to discounting and the default discount rate right there in the documentation for the function, rather than in a separate piece of documentation about a configuration class.


| Parameter | Default |
| --------------- | -------- |
| discount_rate | 3% |
| discount_type | constant |
| damage_function | DICE |
| background_scenario | SSP2-4.5 |

### Air Quality

Added on spreadsheet

---

## Outputs (Unified Structure)

```python
EnvironmentOutput(
climate=ClimateOutput(...),
air_quality=AirQualityOutput(...)
)
```

---

## Climate Outputs

### Radiative Forcing — `RFOutput`

**Shape:** `(component, time)`

**Components (canonical):**

| Index | Component |
| ----- | --------------- |
| 1 | CO2 |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick, without looking at your list, what's rf[6, :]? Using numeric codes like this is what you do in Fortran 77. Don't do it in Python. I would write this as something approximately like:

from enum import StrEnum, auto

class RFComponent(StrEnum):
    CO2_BACKGROUND = auto()
    CO2_LIFECYCLE = auto()
    O2_SHORT = auto()
    H2O_SHORT = auto()
    ...
    CH4_LONG = auto()

class TimeSeries:
    # Whatever. Could just be a Numpy array, or could be something with
    # explicit time information.
    ...

RFComponents = dict[RFComponent, TimeSeries]

class RFOutput:
    def __init__(self, components: RFComponents):
        self.components = components
        # Assuming TimeSeries can be added:
        self.total = sum(components.values())
        # Example:
        self.CO2 = (
            components[RFComponent.CO2_BACKGROUND] +
            components[RFComponent.CO2_LIFECYCLE]
        )

| 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
```

---
Loading