-
Notifications
You must be signed in to change notification settings - Fork 9
Environment module (Climate and AQ results from emissions) #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,305 @@ | ||
| # 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why Also, looking at your 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Quick, without looking at your list, what's 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 | ||
| ``` | ||
|
|
||
| --- | ||
There was a problem hiding this comment.
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.