dynamic-characterization 0.0.1.dev1__tar.gz → 0.0.3__tar.gz
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.
- {dynamic_characterization-0.0.1.dev1/dynamic_characterization.egg-info → dynamic_characterization-0.0.3}/PKG-INFO +14 -6
- {dynamic_characterization-0.0.1.dev1 → dynamic_characterization-0.0.3}/README.md +10 -5
- {dynamic_characterization-0.0.1.dev1 → dynamic_characterization-0.0.3}/dynamic_characterization/__init__.py +3 -1
- dynamic_characterization-0.0.3/dynamic_characterization/classes.py +5 -0
- dynamic_characterization-0.0.3/dynamic_characterization/dynamic_characterization.py +335 -0
- dynamic_characterization-0.0.3/dynamic_characterization/temporalis/__init__.py +15 -0
- dynamic_characterization-0.0.3/dynamic_characterization/temporalis/radiative_forcing.py +137 -0
- dynamic_characterization-0.0.3/dynamic_characterization/timex/__init__.py +23 -0
- dynamic_characterization-0.0.3/dynamic_characterization/timex/radiative_forcing.py +471 -0
- {dynamic_characterization-0.0.1.dev1 → dynamic_characterization-0.0.3/dynamic_characterization.egg-info}/PKG-INFO +14 -6
- dynamic_characterization-0.0.3/dynamic_characterization.egg-info/SOURCES.txt +16 -0
- {dynamic_characterization-0.0.1.dev1 → dynamic_characterization-0.0.3}/dynamic_characterization.egg-info/requires.txt +3 -0
- {dynamic_characterization-0.0.1.dev1 → dynamic_characterization-0.0.3}/pyproject.toml +8 -1
- dynamic_characterization-0.0.1.dev1/dynamic_characterization.egg-info/SOURCES.txt +0 -10
- {dynamic_characterization-0.0.1.dev1 → dynamic_characterization-0.0.3}/LICENSE +0 -0
- {dynamic_characterization-0.0.1.dev1 → dynamic_characterization-0.0.3}/MANIFEST.in +0 -0
- {dynamic_characterization-0.0.1.dev1 → dynamic_characterization-0.0.3}/dynamic_characterization.egg-info/dependency_links.txt +0 -0
- {dynamic_characterization-0.0.1.dev1 → dynamic_characterization-0.0.3}/dynamic_characterization.egg-info/top_level.txt +0 -0
- {dynamic_characterization-0.0.1.dev1 → dynamic_characterization-0.0.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dynamic_characterization
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Collection of dynamic characterization functions for life cycle inventories with temporal information
|
|
5
5
|
Author-email: Timo Diepers <timo.diepers@ltt.rwth-aachen.de>
|
|
6
6
|
Maintainer-email: Timo Diepers <timo.diepers@ltt.rwth-aachen.de>
|
|
@@ -20,6 +20,9 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
20
20
|
Requires-Python: >=3.9
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pandas
|
|
24
|
+
Requires-Dist: numpy
|
|
25
|
+
Requires-Dist: bw2data
|
|
23
26
|
Provides-Extra: testing
|
|
24
27
|
Requires-Dist: dynamic_characterization; extra == "testing"
|
|
25
28
|
Requires-Dist: pytest; extra == "testing"
|
|
@@ -76,11 +79,12 @@ Here's an example of what such a function could look like:
|
|
|
76
79
|
|
|
77
80
|
```python
|
|
78
81
|
def characterize_something(series, period: int = 100, cumulative=False) -> pd.DataFrame:
|
|
79
|
-
date_beginning: np.datetime64 = series
|
|
80
|
-
|
|
82
|
+
date_beginning: np.datetime64 = series.date.to_numpy()
|
|
83
|
+
dates_characterized: np.ndarray = date_beginning + np.arange(
|
|
81
84
|
start=0, stop=period, dtype="timedelta64[Y]"
|
|
82
85
|
).astype("timedelta64[s]")
|
|
83
86
|
|
|
87
|
+
# let's assume some simple decay function
|
|
84
88
|
decay_multipliers: list = np.array(
|
|
85
89
|
[
|
|
86
90
|
1.234 * (1 - np.exp(-year / 56.789))
|
|
@@ -88,13 +92,14 @@ def characterize_something(series, period: int = 100, cumulative=False) -> pd.Da
|
|
|
88
92
|
]
|
|
89
93
|
)
|
|
90
94
|
|
|
91
|
-
forcing =
|
|
95
|
+
forcing = np.array(series.amount * decay_multipliers, dtype="float64")
|
|
96
|
+
|
|
92
97
|
if not cumulative:
|
|
93
|
-
forcing =
|
|
98
|
+
forcing = np.diff(forcing, prepend=0)
|
|
94
99
|
|
|
95
100
|
return pd.DataFrame(
|
|
96
101
|
{
|
|
97
|
-
"date":
|
|
102
|
+
"date": np.array(dates_characterized, dtype="datetime64[s]"),
|
|
98
103
|
"amount": forcing,
|
|
99
104
|
"flow": series.flow,
|
|
100
105
|
"activity": series.activity,
|
|
@@ -131,6 +136,9 @@ _dynamic_characterization_ is free and open source software.
|
|
|
131
136
|
If you encounter any problems,
|
|
132
137
|
please [file an issue][Issue Tracker] along with a detailed description.
|
|
133
138
|
|
|
139
|
+
## Support
|
|
140
|
+
|
|
141
|
+
If you have any questions or need help, do not hesitate to contact Timo Diepers ([timo.diepers@ltt.rwth-aachen.de](mailto:timo.diepers@ltt.rwth-aachen.de))
|
|
134
142
|
|
|
135
143
|
<!-- github-only -->
|
|
136
144
|
|
|
@@ -40,11 +40,12 @@ Here's an example of what such a function could look like:
|
|
|
40
40
|
|
|
41
41
|
```python
|
|
42
42
|
def characterize_something(series, period: int = 100, cumulative=False) -> pd.DataFrame:
|
|
43
|
-
date_beginning: np.datetime64 = series
|
|
44
|
-
|
|
43
|
+
date_beginning: np.datetime64 = series.date.to_numpy()
|
|
44
|
+
dates_characterized: np.ndarray = date_beginning + np.arange(
|
|
45
45
|
start=0, stop=period, dtype="timedelta64[Y]"
|
|
46
46
|
).astype("timedelta64[s]")
|
|
47
47
|
|
|
48
|
+
# let's assume some simple decay function
|
|
48
49
|
decay_multipliers: list = np.array(
|
|
49
50
|
[
|
|
50
51
|
1.234 * (1 - np.exp(-year / 56.789))
|
|
@@ -52,13 +53,14 @@ def characterize_something(series, period: int = 100, cumulative=False) -> pd.Da
|
|
|
52
53
|
]
|
|
53
54
|
)
|
|
54
55
|
|
|
55
|
-
forcing =
|
|
56
|
+
forcing = np.array(series.amount * decay_multipliers, dtype="float64")
|
|
57
|
+
|
|
56
58
|
if not cumulative:
|
|
57
|
-
forcing =
|
|
59
|
+
forcing = np.diff(forcing, prepend=0)
|
|
58
60
|
|
|
59
61
|
return pd.DataFrame(
|
|
60
62
|
{
|
|
61
|
-
"date":
|
|
63
|
+
"date": np.array(dates_characterized, dtype="datetime64[s]"),
|
|
62
64
|
"amount": forcing,
|
|
63
65
|
"flow": series.flow,
|
|
64
66
|
"activity": series.activity,
|
|
@@ -95,6 +97,9 @@ _dynamic_characterization_ is free and open source software.
|
|
|
95
97
|
If you encounter any problems,
|
|
96
98
|
please [file an issue][Issue Tracker] along with a detailed description.
|
|
97
99
|
|
|
100
|
+
## Support
|
|
101
|
+
|
|
102
|
+
If you have any questions or need help, do not hesitate to contact Timo Diepers ([timo.diepers@ltt.rwth-aachen.de](mailto:timo.diepers@ltt.rwth-aachen.de))
|
|
98
103
|
|
|
99
104
|
<!-- github-only -->
|
|
100
105
|
|
|
@@ -4,12 +4,14 @@ Collection of dynamic characterization functions for life cycle inventories with
|
|
|
4
4
|
|
|
5
5
|
__all__ = (
|
|
6
6
|
"__version__",
|
|
7
|
+
"dynamic_characterization.characterize_dynamic_inventory",
|
|
7
8
|
"temporalis",
|
|
8
9
|
"timex",
|
|
9
10
|
# Add functions and variables you want exposed in `dynamic_characterization.` namespace here
|
|
10
11
|
)
|
|
11
12
|
|
|
12
|
-
__version__ = "0.0.
|
|
13
|
+
__version__ = "0.0.3"
|
|
13
14
|
|
|
15
|
+
from .dynamic_characterization import characterize_dynamic_inventory
|
|
14
16
|
from . import temporalis
|
|
15
17
|
from . import timex
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import warnings
|
|
4
|
+
from collections.abc import Collection
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Callable, Dict, Tuple
|
|
7
|
+
|
|
8
|
+
import bw2data as bd
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from bw2data.utils import UnknownObject
|
|
12
|
+
|
|
13
|
+
from dynamic_characterization.timex.radiative_forcing import (
|
|
14
|
+
characterize_ch4,
|
|
15
|
+
characterize_co,
|
|
16
|
+
characterize_co2,
|
|
17
|
+
characterize_co2_uptake,
|
|
18
|
+
characterize_n2o,
|
|
19
|
+
create_generic_characterization_function,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def characterize_dynamic_inventory(
|
|
24
|
+
dynamic_inventory_df: pd.DataFrame,
|
|
25
|
+
metric: str = "radiative_forcing",
|
|
26
|
+
characterization_function_dict: Dict[str, Callable] = None,
|
|
27
|
+
base_lcia_method: Tuple[str, ...] = None,
|
|
28
|
+
time_horizon: int = 100,
|
|
29
|
+
fixed_time_horizon: bool = False,
|
|
30
|
+
time_horizon_start: datetime = datetime.now(),
|
|
31
|
+
characterization_function_co2: Callable = None,
|
|
32
|
+
) -> pd.DataFrame:
|
|
33
|
+
"""
|
|
34
|
+
Characterizes the dynamic inventory, formatted as a Dataframe, by evaluating each emission (row in DataFrame) using given dynamic characterization functions.
|
|
35
|
+
|
|
36
|
+
Available metrics are radiative forcing [W/m2] and GWP [kg CO2eq], defaulting to `radiative_forcing`.
|
|
37
|
+
|
|
38
|
+
In case users don't provide own dynamic characterization functions, it adds dynamic characterization functions from the timex submodule
|
|
39
|
+
for the GHGs mentioned in the IPCC AR6 Chapter 7, if these GHG are also characterized in the selected static LCA method.
|
|
40
|
+
|
|
41
|
+
This method applies the dynamic characterization functions to each row of the dynamic_inventory_df for the duration of `time_horizon`, defaulting to 100 years.
|
|
42
|
+
The `fixed_time_horizon` parameter determines whether the evaluation time horizon for all emissions is calculated from the
|
|
43
|
+
functional unit (`fixed_time_horizon=True`), regardless of when the actual emission occurs, or from the time of the emission itself(`fixed_time_horizon=False`).
|
|
44
|
+
The former is the implementation of the Levasseur approach (https://doi.org/10.1021/es9030003), while the latter is how conventional LCA is done.
|
|
45
|
+
The Levasseur approach means that earlier emissions are characterized for a longer time period than later emissions.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
dynamic_inventory_df : pd.DataFrame
|
|
50
|
+
Dynamic inventory, formatted as a DataFrame, which contains the timing, id and amount of emissions and the emitting activity.
|
|
51
|
+
metric : str, optional
|
|
52
|
+
The metric for which the dynamic LCIA should be calculated. Default is "GWP". Available: "GWP" and "radiative_forcing". Default is "radiative_forcing".
|
|
53
|
+
characterization_function_dict : dict, optional
|
|
54
|
+
A dictionary of the form {biosphere_flow_id: dynamic_characterization_function} allowing users to specify their own functions and what flows to apply them to.
|
|
55
|
+
Default is none, in which case a set of default functions are added based on the base_lcia_method.
|
|
56
|
+
base_lcia_method : tuple, optional
|
|
57
|
+
Tuple of the selcted the LCIA method, e.g. `("EF v3.1", "climate change", "global warming potential (GWP100)")`. This is
|
|
58
|
+
required for adding the default characterization functions and can be kept empty if custom ones are provided.
|
|
59
|
+
time_horizon: int, optional
|
|
60
|
+
Length of the time horizon for the dynamic characterization. Default is 100 years.
|
|
61
|
+
fixed_time_horizon: bool, optional
|
|
62
|
+
If True, the time horizon is calculated from the time of the functional unit (FU) instead of the time of emission. Default is False.
|
|
63
|
+
time_horizon_start: pd.Timestamp, optional
|
|
64
|
+
The starting timestamp of the time horizon for the dynamic characterization. Only needed for fixed time horizons. Default is datetime.now().
|
|
65
|
+
characterization_function_co2: Callable, optional
|
|
66
|
+
Characterization function for CO2. This is required for the GWP calculation. If None is given, we try using timex' default CO2 function from the (separate) dynamic_characterization package (https://dynamic-characterization.readthedocs.io/en/latest/).
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
pd.DataFrame
|
|
71
|
+
characterized dynamic inventory
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
if metric not in {"radiative_forcing", "GWP"}:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Metric must be either 'radiative_forcing' or 'GWP', not {metric}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if not characterization_function_dict:
|
|
80
|
+
warnings.warn(
|
|
81
|
+
"No custom dynamic characterization functions provided. Using default dynamic characterization functions from `dynamic_characterization` meant to work with biosphere3 flows. The flows that are characterized are based on the selection of the initially chosen impact category. You can look up the mapping in the bw_timex.dynamic_characterizer.characterization_function_dict."
|
|
82
|
+
)
|
|
83
|
+
if not base_lcia_method:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"Please provide an LCIA method to base the default dynamic characterization functions on."
|
|
86
|
+
)
|
|
87
|
+
characterization_function_dict = create_default_characterization_function_dict(
|
|
88
|
+
base_lcia_method
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if metric == "GWP" and not characterization_function_co2:
|
|
92
|
+
characterization_function_co2 = _get_default_co2_function()
|
|
93
|
+
|
|
94
|
+
characterized_inventory_data = []
|
|
95
|
+
|
|
96
|
+
for row in dynamic_inventory_df.itertuples(index=False):
|
|
97
|
+
|
|
98
|
+
# skip uncharacterized biosphere flows
|
|
99
|
+
if row.flow not in characterization_function_dict.keys():
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
dynamic_time_horizon = _calculate_dynamic_time_horizon(
|
|
103
|
+
emission_date=row.date,
|
|
104
|
+
time_horizon_start=time_horizon_start,
|
|
105
|
+
time_horizon=time_horizon,
|
|
106
|
+
fixed_time_horizon=fixed_time_horizon,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if metric == "radiative_forcing": # radiative forcing in W/m2
|
|
110
|
+
characterized_inventory_data.append(
|
|
111
|
+
_characterize_radiative_forcing(
|
|
112
|
+
characterization_function_dict, row, dynamic_time_horizon
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if metric == "GWP": # scale radiative forcing to GWP [kg CO2 equivalent]
|
|
117
|
+
characterized_inventory_data.append(
|
|
118
|
+
_characterize_gwp(
|
|
119
|
+
characterization_function_dict=characterization_function_dict,
|
|
120
|
+
row=row,
|
|
121
|
+
original_time_horizon=time_horizon,
|
|
122
|
+
dynamic_time_horizon=dynamic_time_horizon,
|
|
123
|
+
characterization_function_co2=characterization_function_co2,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if not characterized_inventory_data:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
"There are no flows to characterize. Please make sure your time horizon matches the timing of emissions and make sure there are characterization functions for the flows in the dynamic inventories."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
characterized_inventory = (
|
|
133
|
+
pd.DataFrame(characterized_inventory_data)
|
|
134
|
+
.explode(["amount", "date"])
|
|
135
|
+
.astype({"amount": "float64"})
|
|
136
|
+
.query("amount != 0")[["date", "amount", "flow", "activity"]]
|
|
137
|
+
.sort_values(by="date")
|
|
138
|
+
.reset_index(drop=True)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return characterized_inventory
|
|
142
|
+
|
|
143
|
+
def create_default_characterization_function_dict(
|
|
144
|
+
base_lcia_method: Tuple[str, ...]
|
|
145
|
+
) -> dict:
|
|
146
|
+
"""
|
|
147
|
+
Add default dynamic characterization functions from the (separate) dynamic_characterization package (https://dynamic-characterization.readthedocs.io/en/latest/)
|
|
148
|
+
for CO2, CH4, N2O and other GHGs, based on IPCC AR6 Chapter 7 decay curves.
|
|
149
|
+
|
|
150
|
+
Please note: Currently, only CO2, CH4 and N2O include climate-carbon feedbacks.
|
|
151
|
+
|
|
152
|
+
This has not yet been added for other GHGs. Refer to https://esd.copernicus.org/articles/8/235/2017/esd-8-235-2017.html"
|
|
153
|
+
"Methane, non-fossil" is currently also excluded from the default characterization functions, as it has a different static CF than fossil methane and we need to check the correct value (#TODO)
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
base_lcia_method : tuple
|
|
158
|
+
Tuple of the selected the LCIA method, e.g. `("EF v3.1", "climate change", "global warming potential (GWP100)")`.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
None but adds default dynamic characterization functions to the `characterization_function_dict` attribute of the DynamicCharacterization object.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
characterization_function_dict = dict()
|
|
167
|
+
|
|
168
|
+
# load pre-calculated decay multipliers for GHGs (except for CO2, CH4, N2O & CO)
|
|
169
|
+
filepath = os.path.join(
|
|
170
|
+
os.path.dirname(os.path.abspath(__file__)), "data", "decay_multipliers.json"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
with open(filepath) as json_file:
|
|
174
|
+
decay_multipliers = json.load(json_file)
|
|
175
|
+
|
|
176
|
+
# look up which GHGs are characterized in the selected static LCA method
|
|
177
|
+
method_data = bd.Method(base_lcia_method).load()
|
|
178
|
+
|
|
179
|
+
biosphere_db = bd.Database(bd.config.biosphere)
|
|
180
|
+
|
|
181
|
+
# the bioflow-identifier stored in the method data can be the database id or the tuple (database, code)
|
|
182
|
+
def get_bioflow_node(identifier):
|
|
183
|
+
if (
|
|
184
|
+
isinstance(identifier, Collection) and len(identifier) == 2
|
|
185
|
+
): # is (probably) tuple of (database, code)
|
|
186
|
+
try:
|
|
187
|
+
biosphere_node = biosphere_db.get(
|
|
188
|
+
database=identifier[0], code=identifier[1]
|
|
189
|
+
)
|
|
190
|
+
except UnknownObject as e:
|
|
191
|
+
raise UnknownObject(
|
|
192
|
+
f"Failed to set up the default characterization functions because a biosphere node was not found. Make sure the biosphere is set up correctly or provide a characterization_function_dict. Original error: {e}"
|
|
193
|
+
)
|
|
194
|
+
return biosphere_node
|
|
195
|
+
|
|
196
|
+
if isinstance(identifier, int): # id is an int
|
|
197
|
+
return biosphere_db.get(id=identifier)
|
|
198
|
+
else:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
"The flow-identifier stored in the selected method is neither an id nor the tuple (database, code). No automatic matching possible."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
bioflow_nodes = set(
|
|
204
|
+
get_bioflow_node(identifier) for identifier, _ in method_data
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
for node in bioflow_nodes:
|
|
208
|
+
if "carbon dioxide" in node["name"].lower():
|
|
209
|
+
if "soil" in node.get("categories", []):
|
|
210
|
+
characterization_function_dict[node.id] = (
|
|
211
|
+
characterize_co2_uptake # negative emission because uptake by soil
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
else:
|
|
215
|
+
characterization_function_dict[node.id] = characterize_co2
|
|
216
|
+
|
|
217
|
+
elif (
|
|
218
|
+
"methane, fossil" in node["name"].lower()
|
|
219
|
+
or "methane, from soil or biomass stock" in node["name"].lower()
|
|
220
|
+
):
|
|
221
|
+
# TODO Check why "methane, non-fossil" has a CF of 27 instead of 29.8, currently excluded
|
|
222
|
+
characterization_function_dict[node.id] = characterize_ch4
|
|
223
|
+
|
|
224
|
+
elif "dinitrogen monoxide" in node["name"].lower():
|
|
225
|
+
characterization_function_dict[node.id] = characterize_n2o
|
|
226
|
+
|
|
227
|
+
elif "carbon monoxide" in node["name"].lower():
|
|
228
|
+
characterization_function_dict[node.id] = characterize_co
|
|
229
|
+
|
|
230
|
+
else:
|
|
231
|
+
cas_number = node.get("CAS number")
|
|
232
|
+
if cas_number:
|
|
233
|
+
decay_series = decay_multipliers.get(cas_number)
|
|
234
|
+
if decay_series is not None:
|
|
235
|
+
characterization_function_dict[node.id] = (
|
|
236
|
+
create_generic_characterization_function(
|
|
237
|
+
np.array(decay_series)
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
return characterization_function_dict
|
|
241
|
+
|
|
242
|
+
def _get_default_co2_function() -> Callable:
|
|
243
|
+
"""
|
|
244
|
+
Get the default CO2 characterization function from the (separate) dynamic_characterization package (https://dynamic-characterization.readthedocs.io/en/latest/).
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
None
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
Callable
|
|
253
|
+
Default CO2 characterization function
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
from dynamic_characterization.timex.radiative_forcing import (
|
|
257
|
+
characterize_co2,
|
|
258
|
+
)
|
|
259
|
+
except ImportError:
|
|
260
|
+
raise ImportError(
|
|
261
|
+
"The default CO2 characterization function could not be loaded. Please make sure the package 'dynamic_characterization' (https://dynamic-characterization.readthedocs.io/en/latest/) is installed or provide your own function for the dynamic characterization of CO2. This is necessary for the GWP calculations."
|
|
262
|
+
)
|
|
263
|
+
warnings.warn(
|
|
264
|
+
"Using bw_timex's default CO2 characterization function for GWP reference."
|
|
265
|
+
)
|
|
266
|
+
return characterize_co2
|
|
267
|
+
|
|
268
|
+
def _calculate_dynamic_time_horizon(
|
|
269
|
+
emission_date: pd.Timestamp,
|
|
270
|
+
time_horizon_start: pd.Timestamp,
|
|
271
|
+
time_horizon: int,
|
|
272
|
+
fixed_time_horizon: bool,
|
|
273
|
+
) -> datetime:
|
|
274
|
+
"""
|
|
275
|
+
Calculate the dynamic time horizon for the dynamic characterization of an emission.
|
|
276
|
+
Distinguishes between the Levasseur approach (fixed_time_horizon = True) and the conventional approach (fixed_time_horizon = False).
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
emission_date: pd.Timestamp
|
|
281
|
+
The date of the emission
|
|
282
|
+
time_horizon_start: pd.Timestamp
|
|
283
|
+
Start timestamp of the time horizon
|
|
284
|
+
time_horizon: int
|
|
285
|
+
Length of the time horizon in years
|
|
286
|
+
fixed_time_horizon: bool
|
|
287
|
+
If True, the time horizon is calculated from the time of the functional unit (FU) instead of the time of emission
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
datetime.datetime
|
|
292
|
+
dynamic time horizon for the specific emission
|
|
293
|
+
"""
|
|
294
|
+
if fixed_time_horizon:
|
|
295
|
+
# Levasseur approach: time_horizon for all emissions starts at timing of FU + time_horizon
|
|
296
|
+
# e.g. an emission occuring n years before FU is characterized for time_horizon+n years
|
|
297
|
+
end_time_horizon = time_horizon_start + pd.DateOffset(years=time_horizon)
|
|
298
|
+
emission_datetime = emission_date.to_pydatetime()
|
|
299
|
+
|
|
300
|
+
return max(0, round((end_time_horizon - emission_datetime).days / 365.25))
|
|
301
|
+
|
|
302
|
+
else:
|
|
303
|
+
# conventional approach, emission is calculated from t emission for the length of time horizon
|
|
304
|
+
return time_horizon
|
|
305
|
+
|
|
306
|
+
def _characterize_radiative_forcing(characterization_function_dict, row, time_horizon) -> Dict:
|
|
307
|
+
return characterization_function_dict[row.flow](row, time_horizon)
|
|
308
|
+
|
|
309
|
+
def _characterize_gwp(
|
|
310
|
+
characterization_function_dict,
|
|
311
|
+
row,
|
|
312
|
+
original_time_horizon,
|
|
313
|
+
dynamic_time_horizon,
|
|
314
|
+
characterization_function_co2,
|
|
315
|
+
) -> Dict:
|
|
316
|
+
radiative_forcing_ghg = characterization_function_dict[row.flow](
|
|
317
|
+
row,
|
|
318
|
+
dynamic_time_horizon,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# calculate reference radiative forcing for 1 kg of CO2
|
|
322
|
+
radiative_forcing_co2 = characterization_function_co2(
|
|
323
|
+
row._replace(amount=1), original_time_horizon
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
ghg_integral = radiative_forcing_ghg.amount.sum()
|
|
327
|
+
co2_integral = radiative_forcing_co2.amount.sum()
|
|
328
|
+
co2_equiv = ghg_integral / co2_integral
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
"date": row.date,
|
|
332
|
+
"amount": co2_equiv,
|
|
333
|
+
"flow": row.flow,
|
|
334
|
+
"activity": row.activity,
|
|
335
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic characterization functions from the bw_temporalis package (https://github.com/brightway-lca/bw_temporalis).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"__version__",
|
|
7
|
+
"characterize_co2",
|
|
8
|
+
"characterize_methane",
|
|
9
|
+
# Add functions and variables you want exposed in `dynamic_characterization.` namespace here
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__version__ = "0.0.1"
|
|
13
|
+
|
|
14
|
+
from .radiative_forcing import characterize_co2
|
|
15
|
+
from .radiative_forcing import characterize_methane
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
from dynamic_characterization.classes import CharacterizedRow
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def characterize_co2(
|
|
8
|
+
series,
|
|
9
|
+
period: int | None = 100,
|
|
10
|
+
cumulative: bool | None = False,
|
|
11
|
+
) -> CharacterizedRow:
|
|
12
|
+
"""
|
|
13
|
+
Calculate the cumulative or marginal radiative forcing (CRF) from CO2 for each year in a given period.
|
|
14
|
+
|
|
15
|
+
If `cumulative` is True, the cumulative CRF is calculated. If `cumulative` is False, the marginal CRF is calculated.
|
|
16
|
+
Takes a single row of the TimeSeries Pandas DataFrame (corresponding to a set of (`date`/`amount`/`flow`/`activity`).
|
|
17
|
+
For each year in the given period, the CRF is calculated.
|
|
18
|
+
Units are watts/square meter/kilogram of CO2.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
A TimeSeries dataframe with the following columns:
|
|
23
|
+
- date: datetime64[s]
|
|
24
|
+
- amount: float
|
|
25
|
+
- flow: str
|
|
26
|
+
- activity: str
|
|
27
|
+
|
|
28
|
+
Notes
|
|
29
|
+
-----
|
|
30
|
+
See also the relevant scientific publication on CRF: https://doi.org/10.5194/acp-13-2793-2013
|
|
31
|
+
See also the relevant scientific publication on the numerical calculation of CRF: http://pubs.acs.org/doi/abs/10.1021/acs.est.5b01118
|
|
32
|
+
|
|
33
|
+
See Also
|
|
34
|
+
--------
|
|
35
|
+
characterize_methane: The same function for CH4
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# functional variables and units (from publications listed in docstring)
|
|
39
|
+
RE = 1.76e-15 # Radiative forcing (W/m2/kg)
|
|
40
|
+
alpha_0, alpha_1, alpha_2, alpha_3 = 0.2173, 0.2240, 0.2824, 0.2763
|
|
41
|
+
tau_1, tau_2, tau_3 = 394.4, 36.54, 4.304
|
|
42
|
+
decay_term = lambda year, alpha, tau: alpha * tau * (1 - np.exp(-year / tau))
|
|
43
|
+
|
|
44
|
+
date_beginning: np.datetime64 = series.date.to_numpy()
|
|
45
|
+
dates_characterized: np.ndarray = date_beginning + np.arange(
|
|
46
|
+
start=0, stop=period, dtype="timedelta64[Y]"
|
|
47
|
+
).astype("timedelta64[s]")
|
|
48
|
+
|
|
49
|
+
decay_multipliers: np.ndarray = np.array(
|
|
50
|
+
[
|
|
51
|
+
RE
|
|
52
|
+
* (
|
|
53
|
+
alpha_0 * year
|
|
54
|
+
+ decay_term(year, alpha_1, tau_1)
|
|
55
|
+
+ decay_term(year, alpha_2, tau_2)
|
|
56
|
+
+ decay_term(year, alpha_3, tau_3)
|
|
57
|
+
)
|
|
58
|
+
for year in range(period)
|
|
59
|
+
]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
forcing = pd.Series(data=series.amount * decay_multipliers, dtype="float64")
|
|
63
|
+
if not cumulative:
|
|
64
|
+
forcing = forcing.diff(periods=1).fillna(0)
|
|
65
|
+
|
|
66
|
+
return CharacterizedRow(
|
|
67
|
+
date=np.array(dates_characterized, dtype="datetime64[s]"),
|
|
68
|
+
amount=forcing,
|
|
69
|
+
flow=series.flow,
|
|
70
|
+
activity=series.activity,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def characterize_methane(series, period: int = 100, cumulative=False) -> CharacterizedRow:
|
|
75
|
+
"""
|
|
76
|
+
Calculate the cumulative or marginal radiative forcing (CRF) from CH4 for each year in a given period.
|
|
77
|
+
|
|
78
|
+
If `cumulative` is True, the cumulative CRF is calculated. If `cumulative` is False, the marginal CRF is calculated.
|
|
79
|
+
Takes a single row of the TimeSeries Pandas DataFrame (corresponding to a set of (`date`/`amount`/`flow`/`activity`).
|
|
80
|
+
For earch year in the given period, the CRF is calculated.
|
|
81
|
+
Units are watts/square meter/kilogram of CH4.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
series : array-like
|
|
86
|
+
A single row of the TimeSeries dataframe.
|
|
87
|
+
period : int, optional
|
|
88
|
+
Time period for calculation (number of years), by default 100
|
|
89
|
+
cumulative : bool, optional
|
|
90
|
+
Should the RF amounts be summed over time?
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
A TimeSeries dataframe with the following columns:
|
|
95
|
+
- date: datetime64[s]
|
|
96
|
+
- amount: float
|
|
97
|
+
- flow: str
|
|
98
|
+
- activity: str
|
|
99
|
+
|
|
100
|
+
Notes
|
|
101
|
+
-----
|
|
102
|
+
See also the relevant scientific publication on CRF: https://doi.org/10.5194/acp-13-2793-2013
|
|
103
|
+
See also the relevant scientific publication on the numerical calculation of CRF: http://pubs.acs.org/doi/abs/10.1021/acs.est.5b01118
|
|
104
|
+
|
|
105
|
+
See Also
|
|
106
|
+
--------
|
|
107
|
+
characterize_co2: The same function for CO2
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# functional variables and units (from publications listed in docstring)
|
|
111
|
+
f1 = 0.5 # Unitless
|
|
112
|
+
f2 = 0.15 # Unitless
|
|
113
|
+
alpha = 1.27e-13 # Radiative forcing (W/m2/kg)
|
|
114
|
+
tau = 12.4 # Lifetime (years)
|
|
115
|
+
|
|
116
|
+
date_beginning: np.datetime64 = series.date.to_numpy()
|
|
117
|
+
dates_characterized: np.ndarray = date_beginning + np.arange(
|
|
118
|
+
start=0, stop=period, dtype="timedelta64[Y]"
|
|
119
|
+
).astype("timedelta64[s]")
|
|
120
|
+
|
|
121
|
+
decay_multipliers: list = np.array(
|
|
122
|
+
[
|
|
123
|
+
(1 + f1 + f2) * alpha * tau * (1 - np.exp(-year / tau))
|
|
124
|
+
for year in range(period)
|
|
125
|
+
]
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
forcing = pd.Series(data=series.amount * decay_multipliers, dtype="float64")
|
|
129
|
+
if not cumulative:
|
|
130
|
+
forcing = forcing.diff(periods=1).fillna(0)
|
|
131
|
+
|
|
132
|
+
return CharacterizedRow(
|
|
133
|
+
date=np.array(dates_characterized, dtype="datetime64[s]"),
|
|
134
|
+
amount=forcing,
|
|
135
|
+
flow=series.flow,
|
|
136
|
+
activity=series.activity,
|
|
137
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic characterization functions from the bw_timex package (https://github.com/brightway-lca/bw_timex).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"__version__",
|
|
7
|
+
"characterize_co2",
|
|
8
|
+
"characterize_co2_uptake",
|
|
9
|
+
"characterize_co",
|
|
10
|
+
"characterize_ch4",
|
|
11
|
+
"characterize_n2o",
|
|
12
|
+
"create_generic_characterization_function",
|
|
13
|
+
# Add functions and variables you want exposed in `dynamic_characterization.` namespace here
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.0.1"
|
|
17
|
+
|
|
18
|
+
from .radiative_forcing import characterize_co2
|
|
19
|
+
from .radiative_forcing import characterize_co2_uptake
|
|
20
|
+
from .radiative_forcing import characterize_co
|
|
21
|
+
from .radiative_forcing import characterize_ch4
|
|
22
|
+
from .radiative_forcing import characterize_n2o
|
|
23
|
+
from .radiative_forcing import create_generic_characterization_function
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from dynamic_characterization.classes import CharacterizedRow
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def IRF_co2(year) -> callable:
|
|
8
|
+
"""
|
|
9
|
+
Impulse Resonse Function (IRF) of CO2
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
year : int
|
|
14
|
+
The year after emission for which the IRF is calculated.
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
float
|
|
19
|
+
The IRF value for the given year.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
alpha_0, alpha_1, alpha_2, alpha_3 = 0.2173, 0.2240, 0.2824, 0.2763
|
|
23
|
+
tau_1, tau_2, tau_3 = 394.4, 36.54, 4.304
|
|
24
|
+
exponentials = lambda year, alpha, tau: alpha * tau * (1 - np.exp(-year / tau))
|
|
25
|
+
return (
|
|
26
|
+
alpha_0 * year
|
|
27
|
+
+ exponentials(year, alpha_1, tau_1)
|
|
28
|
+
+ exponentials(year, alpha_2, tau_2)
|
|
29
|
+
+ exponentials(year, alpha_3, tau_3)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def characterize_co2(
|
|
34
|
+
series,
|
|
35
|
+
period: int | None = 100,
|
|
36
|
+
cumulative: bool | None = False,
|
|
37
|
+
) -> CharacterizedRow:
|
|
38
|
+
"""
|
|
39
|
+
Calculate the cumulative or marginal radiative forcing (CRF) from CO2 for each year in a given period.
|
|
40
|
+
|
|
41
|
+
Based on characterize_co2 from bw_temporalis, but updated numerical values from IPCC AR6 Ch7 & SM.
|
|
42
|
+
|
|
43
|
+
If `cumulative` is True, the cumulative CRF is calculated. If `cumulative` is False, the marginal CRF is calculated.
|
|
44
|
+
Takes a single row of the TimeSeries Pandas DataFrame (corresponding to a set of (`date`/`amount`/`flow`/`activity`).
|
|
45
|
+
For each year in the given period, the CRF is calculated.
|
|
46
|
+
Units are watts/square meter/kilogram of CO2.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
A TimeSeries dataframe with the following columns:
|
|
51
|
+
- date: datetime64[s]
|
|
52
|
+
- amount: float
|
|
53
|
+
- flow: str
|
|
54
|
+
- activity: str
|
|
55
|
+
|
|
56
|
+
See also
|
|
57
|
+
--------
|
|
58
|
+
Joos2013: Relevant scientific publication on CRF: https://doi.org/10.5194/acp-13-2793-2013
|
|
59
|
+
Schivley2015: Relevant scientific publication on the numerical calculation of CRF: https://doi.org/10.1021/acs.est.5b01118
|
|
60
|
+
Forster2023: Updated numerical values from IPCC AR6 Chapter 7 (Table 7.15): https://doi.org/10.1017/9781009157896.009
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# functional variables and units (from publications listed in docstring)
|
|
64
|
+
radiative_efficiency_ppb = (
|
|
65
|
+
1.33e-5 # W/m2/ppb; 2019 background co2 concentration; IPCC AR6 Table 7.15
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# for conversion from ppb to kg-CO2
|
|
69
|
+
M_co2 = 44.01 # g/mol
|
|
70
|
+
M_air = 28.97 # g/mol, dry air
|
|
71
|
+
m_atmosphere = 5.135e18 # kg [Trenberth and Smith, 2005]
|
|
72
|
+
|
|
73
|
+
radiative_efficiency_kg = (
|
|
74
|
+
radiative_efficiency_ppb * M_air / M_co2 * 1e9 / m_atmosphere
|
|
75
|
+
) # W/m2/kg-CO2
|
|
76
|
+
|
|
77
|
+
date_beginning: np.datetime64 = series.date.to_numpy()
|
|
78
|
+
dates_characterized: np.ndarray = date_beginning + np.arange(
|
|
79
|
+
start=0, stop=period, dtype="timedelta64[Y]"
|
|
80
|
+
).astype("timedelta64[s]")
|
|
81
|
+
|
|
82
|
+
decay_multipliers: np.ndarray = np.array(
|
|
83
|
+
[radiative_efficiency_kg * IRF_co2(year) for year in range(period)]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
forcing = np.array(series.amount * decay_multipliers, dtype="float64")
|
|
87
|
+
|
|
88
|
+
if not cumulative:
|
|
89
|
+
forcing = np.diff(forcing, prepend=0)
|
|
90
|
+
|
|
91
|
+
return CharacterizedRow(
|
|
92
|
+
date=np.array(dates_characterized, dtype="datetime64[s]"),
|
|
93
|
+
amount=forcing,
|
|
94
|
+
flow=series.flow,
|
|
95
|
+
activity=series.activity,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def characterize_co2_uptake(
|
|
100
|
+
series,
|
|
101
|
+
period: int | None = 100,
|
|
102
|
+
cumulative: bool | None = False,
|
|
103
|
+
) -> CharacterizedRow:
|
|
104
|
+
"""
|
|
105
|
+
The same as characterize_co2, but with a negative sign for uptake of CO2.
|
|
106
|
+
|
|
107
|
+
Based on characterize_co2 from bw_temporalis, but updated numerical values from IPCC AR6 Ch7 & SM.
|
|
108
|
+
|
|
109
|
+
Calculate the negative cumulative or marginal radiative forcing (CRF) from CO2-uptake for each year in a given period.
|
|
110
|
+
|
|
111
|
+
If `cumulative` is True, the cumulative CRF is calculated. If `cumulative` is False, the marginal CRF is calculated.
|
|
112
|
+
Takes a single row of the TimeSeries Pandas DataFrame (corresponding to a set of (`date`/`amount`/`flow`/`activity`).
|
|
113
|
+
For each year in the given period, the CRF is calculated.
|
|
114
|
+
Units are watts/square meter/kilogram of CO2.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
A TimeSeries dataframe with the following columns:
|
|
119
|
+
- date: datetime64[s]
|
|
120
|
+
- amount: float
|
|
121
|
+
- flow: str
|
|
122
|
+
- activity: str
|
|
123
|
+
|
|
124
|
+
See also
|
|
125
|
+
--------
|
|
126
|
+
Joos2013: Relevant scientific publication on CRF: https://doi.org/10.5194/acp-13-2793-2013
|
|
127
|
+
Schivley2015: Relevant scientific publication on the numerical calculation of CRF: https://doi.org/10.1021/acs.est.5b01118
|
|
128
|
+
Forster2023: Updated numerical values from IPCC AR6 Chapter 7 (Table 7.15): https://doi.org/10.1017/9781009157896.009
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
# functional variables and units (from publications listed in docstring)
|
|
132
|
+
radiative_efficiency_ppb = (
|
|
133
|
+
1.33e-5 # W/m2/ppb; 2019 background co2 concentration; IPCC AR6 Table 7.15
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# for conversion from ppb to kg-CO2
|
|
137
|
+
M_co2 = 44.01 # g/mol
|
|
138
|
+
M_air = 28.97 # g/mol, dry air
|
|
139
|
+
m_atmosphere = 5.135e18 # kg [Trenberth and Smith, 2005]
|
|
140
|
+
|
|
141
|
+
radiative_efficiency_kg = (
|
|
142
|
+
radiative_efficiency_ppb * M_air / M_co2 * 1e9 / m_atmosphere
|
|
143
|
+
) # W/m2/kg-CO2
|
|
144
|
+
|
|
145
|
+
date_beginning: np.datetime64 = series.date.to_numpy()
|
|
146
|
+
dates_characterized: np.ndarray = date_beginning + np.arange(
|
|
147
|
+
start=0, stop=period, dtype="timedelta64[Y]"
|
|
148
|
+
).astype("timedelta64[s]")
|
|
149
|
+
|
|
150
|
+
decay_multipliers: np.ndarray = np.array(
|
|
151
|
+
[radiative_efficiency_kg * IRF_co2(year) for year in range(period)]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
forcing = np.array(series.amount * decay_multipliers, dtype="float64")
|
|
155
|
+
|
|
156
|
+
# flip the sign of the characterization function for CO2 uptake and not release
|
|
157
|
+
forcing = -forcing
|
|
158
|
+
|
|
159
|
+
if not cumulative:
|
|
160
|
+
forcing = np.diff(forcing, prepend=0)
|
|
161
|
+
|
|
162
|
+
return CharacterizedRow(
|
|
163
|
+
date=np.array(dates_characterized, dtype="datetime64[s]"),
|
|
164
|
+
amount=forcing,
|
|
165
|
+
flow=series.flow,
|
|
166
|
+
activity=series.activity,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def characterize_co(
|
|
171
|
+
series,
|
|
172
|
+
period: int | None = 100,
|
|
173
|
+
cumulative: bool | None = False,
|
|
174
|
+
) -> CharacterizedRow:
|
|
175
|
+
"""
|
|
176
|
+
Calculate the cumulative or marginal radiative forcing (CRF) from CO for each year in a given period.
|
|
177
|
+
|
|
178
|
+
This is exactly the same function as for CO2, it's just scaled by the ratio of molar masses of CO and CO2. This is because CO is very short-lived (lifetime ~2 months) and we assume that it completely reacts to CO2 within the first year.
|
|
179
|
+
|
|
180
|
+
Based on characterize_co2 from bw_temporalis, but updated numerical values from IPCC AR6 Ch7 & SM.
|
|
181
|
+
|
|
182
|
+
Calculate the cumulative or marginal radiative forcing (CRF) from CO2 for each year in a given period.
|
|
183
|
+
|
|
184
|
+
If `cumulative` is True, the cumulative CRF is calculated. If `cumulative` is False, the marginal CRF is calculated.
|
|
185
|
+
Takes a single row of the TimeSeries Pandas DataFrame (corresponding to a set of (`date`/`amount`/`flow`/`activity`).
|
|
186
|
+
For each year in the given period, the CRF is calculated.
|
|
187
|
+
Units are watts/square meter/kilogram of CO2.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
A TimeSeries dataframe with the following columns:
|
|
192
|
+
- date: datetime64[s]
|
|
193
|
+
- amount: float
|
|
194
|
+
- flow: str
|
|
195
|
+
- activity: str
|
|
196
|
+
|
|
197
|
+
See also
|
|
198
|
+
--------
|
|
199
|
+
Joos2013: Relevant scientific publication on CRF: https://doi.org/10.5194/acp-13-2793-2013
|
|
200
|
+
Schivley2015: Relevant scientific publication on the numerical calculation of CRF: https://doi.org/10.1021/acs.est.5b01118
|
|
201
|
+
Forster2023: Updated numerical values from IPCC AR6 Chapter 7 (Table 7.15): https://doi.org/10.1017/9781009157896.009
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
# functional variables and units (from publications listed in docstring)
|
|
205
|
+
radiative_efficiency_ppb = (
|
|
206
|
+
1.33e-5 # W/m2/ppb; 2019 background co2 concentration; IPCC AR6 Table 7.15
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# for conversion from ppb to kg-CO2
|
|
210
|
+
M_co2 = 44.01 # g/mol
|
|
211
|
+
M_co = 28.01 # g/mol
|
|
212
|
+
M_air = 28.97 # g/mol, dry air
|
|
213
|
+
m_atmosphere = 5.135e18 # kg [Trenberth and Smith, 2005]
|
|
214
|
+
|
|
215
|
+
radiative_efficiency_kg = (
|
|
216
|
+
radiative_efficiency_ppb * M_air / M_co2 * 1e9 / m_atmosphere
|
|
217
|
+
) # W/m2/kg-CO2
|
|
218
|
+
|
|
219
|
+
date_beginning: np.datetime64 = series.date.to_numpy()
|
|
220
|
+
dates_characterized: np.ndarray = date_beginning + np.arange(
|
|
221
|
+
start=0, stop=period, dtype="timedelta64[Y]"
|
|
222
|
+
).astype("timedelta64[s]")
|
|
223
|
+
|
|
224
|
+
decay_multipliers: np.ndarray = np.array(
|
|
225
|
+
[
|
|
226
|
+
M_co2 / M_co * radiative_efficiency_kg * IRF_co2(year)
|
|
227
|
+
for year in range(period)
|
|
228
|
+
] # <-- Scaling from co2 to co is done here
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
forcing = np.array(series.amount * decay_multipliers, dtype="float64")
|
|
232
|
+
|
|
233
|
+
if not cumulative:
|
|
234
|
+
forcing = np.diff(forcing, prepend=0)
|
|
235
|
+
|
|
236
|
+
return CharacterizedRow(
|
|
237
|
+
date=np.array(dates_characterized, dtype="datetime64[s]"),
|
|
238
|
+
amount=forcing,
|
|
239
|
+
flow=series.flow,
|
|
240
|
+
activity=series.activity,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def characterize_ch4(
|
|
245
|
+
series,
|
|
246
|
+
period: int = 100,
|
|
247
|
+
cumulative=False,
|
|
248
|
+
) -> CharacterizedRow:
|
|
249
|
+
"""
|
|
250
|
+
Calculate the cumulative or marginal radiative forcing (CRF) from CH4 for each year in a given period.
|
|
251
|
+
|
|
252
|
+
Based on characterize_methane from bw_temporalis, but updated numerical values from IPCC AR6 Ch7 & SM.
|
|
253
|
+
|
|
254
|
+
This DOES include indirect effects of CH4 on ozone and water vapor, but DOES NOT include the decay to CO2.
|
|
255
|
+
For more info on that, see the deprecated version of bw_temporalis.
|
|
256
|
+
|
|
257
|
+
If `cumulative` is True, the cumulative CRF is calculated. If `cumulative` is False, the marginal CRF is calculated.
|
|
258
|
+
Takes a single row of the TimeSeries Pandas DataFrame (corresponding to a set of (`date`/`amount`/`flow`/`activity`).
|
|
259
|
+
For earch year in the given period, the CRF is calculated.
|
|
260
|
+
Units are watts/square meter/kilogram of CH4.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
series : array-like
|
|
265
|
+
A single row of the TimeSeries dataframe.
|
|
266
|
+
period : int, optional
|
|
267
|
+
Time period for calculation (number of years), by default 100
|
|
268
|
+
cumulative : bool, optional
|
|
269
|
+
Should the RF amounts be summed over time?
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
A TimeSeries dataframe with the following columns:
|
|
274
|
+
- date: datetime64[s]
|
|
275
|
+
- amount: float
|
|
276
|
+
- flow: str
|
|
277
|
+
- activity: str
|
|
278
|
+
|
|
279
|
+
See also
|
|
280
|
+
--------
|
|
281
|
+
Joos2013: Relevant scientific publication on CRF: https://doi.org/10.5194/acp-13-2793-2013
|
|
282
|
+
Schivley2015: Relevant scientific publication on the numerical calculation of CRF: https://doi.org/10.1021/acs.est.5b01118
|
|
283
|
+
Forster2023: Updated numerical values from IPCC AR6 Chapter 7 (Table 7.15): https://doi.org/10.1017/9781009157896.009
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
# functional variables and units (from publications listed in docstring)
|
|
287
|
+
radiative_efficiency_ppb = 5.7e-4 # # W/m2/ppb; 2019 background cch4 concentration; IPCC AR6 Table 7.15. This number includes indirect effects.
|
|
288
|
+
|
|
289
|
+
# for conversion from ppb to kg-CH4
|
|
290
|
+
M_ch4 = 16.04 # g/mol
|
|
291
|
+
M_air = 28.97 # g/mol, dry air
|
|
292
|
+
m_atmosphere = 5.135e18 # kg [Trenberth and Smith, 2005]
|
|
293
|
+
|
|
294
|
+
radiative_efficiency_kg = (
|
|
295
|
+
radiative_efficiency_ppb * M_air / M_ch4 * 1e9 / m_atmosphere
|
|
296
|
+
) # W/m2/kg-CH4
|
|
297
|
+
|
|
298
|
+
tau = 11.8 # Lifetime (years)
|
|
299
|
+
|
|
300
|
+
date_beginning: np.datetime64 = series.date.to_numpy()
|
|
301
|
+
dates_characterized: np.ndarray = date_beginning + np.arange(
|
|
302
|
+
start=0, stop=period, dtype="timedelta64[Y]"
|
|
303
|
+
).astype("timedelta64[s]")
|
|
304
|
+
|
|
305
|
+
decay_multipliers: list = np.array(
|
|
306
|
+
[
|
|
307
|
+
radiative_efficiency_kg * tau * (1 - np.exp(-year / tau))
|
|
308
|
+
for year in range(period)
|
|
309
|
+
]
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
forcing = np.array(series.amount * decay_multipliers, dtype="float64")
|
|
313
|
+
|
|
314
|
+
if not cumulative:
|
|
315
|
+
forcing = np.diff(forcing, prepend=0)
|
|
316
|
+
|
|
317
|
+
return CharacterizedRow(
|
|
318
|
+
date=np.array(dates_characterized, dtype="datetime64[s]"),
|
|
319
|
+
amount=forcing,
|
|
320
|
+
flow=series.flow,
|
|
321
|
+
activity=series.activity,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def characterize_n2o(
|
|
326
|
+
series,
|
|
327
|
+
period: int = 100,
|
|
328
|
+
cumulative=False,
|
|
329
|
+
) -> CharacterizedRow:
|
|
330
|
+
"""
|
|
331
|
+
Calculate the cumulative or marginal radiative forcing (CRF) from N2O for each year in a given period.
|
|
332
|
+
|
|
333
|
+
Based on characterize_methane from bw_temporalis, but updated numerical values from IPCC AR6 Ch7 & SM.
|
|
334
|
+
|
|
335
|
+
If `cumulative` is True, the cumulative CRF is calculated. If `cumulative` is False, the marginal CRF is calculated.
|
|
336
|
+
Takes a single row of the TimeSeries Pandas DataFrame (corresponding to a set of (`date`/`amount`/`flow`/`activity`).
|
|
337
|
+
For earch year in the given period, the CRF is calculated.
|
|
338
|
+
Units are watts/square meter/kilogram of N2O.
|
|
339
|
+
|
|
340
|
+
Parameters
|
|
341
|
+
----------
|
|
342
|
+
series : array-like
|
|
343
|
+
A single row of the TimeSeries dataframe.
|
|
344
|
+
period : int, optional
|
|
345
|
+
Time period for calculation (number of years), by default 100
|
|
346
|
+
cumulative : bool, optional
|
|
347
|
+
Should the RF amounts be summed over time?
|
|
348
|
+
|
|
349
|
+
Returns
|
|
350
|
+
-------
|
|
351
|
+
A TimeSeries dataframe with the following columns:
|
|
352
|
+
- date: datetime64[s]
|
|
353
|
+
- amount: float
|
|
354
|
+
- flow: str
|
|
355
|
+
- activity: str
|
|
356
|
+
|
|
357
|
+
See also
|
|
358
|
+
--------
|
|
359
|
+
Joos2013: Relevant scientific publication on CRF: https://doi.org/10.5194/acp-13-2793-2013
|
|
360
|
+
Schivley2015: Relevant scientific publication on the numerical calculation of CRF: https://doi.org/10.1021/acs.est.5b01118
|
|
361
|
+
Forster2023: Updated numerical values from IPCC AR6 Chapter 7 (Table 7.15): https://doi.org/10.1017/9781009157896.009
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
# functional variables and units (from publications listed in docstring)
|
|
365
|
+
radiative_efficiency_ppb = 2.8e-3 # # W/m2/ppb; 2019 background cch4 concentration; IPCC AR6 Table 7.15. This number includes indirect effects.
|
|
366
|
+
|
|
367
|
+
# for conversion from ppb to kg-CH4
|
|
368
|
+
M_n2o = 44.01 # g/mol
|
|
369
|
+
M_air = 28.97 # g/mol, dry air
|
|
370
|
+
m_atmosphere = 5.135e18 # kg [Trenberth and Smith, 2005]
|
|
371
|
+
|
|
372
|
+
radiative_efficiency_kg = (
|
|
373
|
+
radiative_efficiency_ppb * M_air / M_n2o * 1e9 / m_atmosphere
|
|
374
|
+
) # W/m2/kg-N2O
|
|
375
|
+
|
|
376
|
+
tau = 109 # Lifetime (years)
|
|
377
|
+
|
|
378
|
+
date_beginning: np.datetime64 = series.date.to_numpy()
|
|
379
|
+
dates_characterized: np.ndarray = date_beginning + np.arange(
|
|
380
|
+
start=0, stop=period, dtype="timedelta64[Y]"
|
|
381
|
+
).astype("timedelta64[s]")
|
|
382
|
+
|
|
383
|
+
decay_multipliers: list = np.array(
|
|
384
|
+
[
|
|
385
|
+
radiative_efficiency_kg * tau * (1 - np.exp(-year / tau))
|
|
386
|
+
for year in range(period)
|
|
387
|
+
]
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
forcing = np.array(series.amount * decay_multipliers, dtype="float64")
|
|
391
|
+
if not cumulative:
|
|
392
|
+
forcing = np.diff(forcing, prepend=0)
|
|
393
|
+
|
|
394
|
+
return CharacterizedRow(
|
|
395
|
+
date=np.array(dates_characterized, dtype="datetime64[s]"),
|
|
396
|
+
amount=forcing,
|
|
397
|
+
flow=series.flow,
|
|
398
|
+
activity=series.activity,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def create_generic_characterization_function(decay_series) -> CharacterizedRow:
|
|
403
|
+
"""
|
|
404
|
+
Creates a characterization function for a GHG based on a decay series, by calling the nested method `characterize_generic()`.
|
|
405
|
+
|
|
406
|
+
Parameters
|
|
407
|
+
----------
|
|
408
|
+
decay_series : np.ndarray
|
|
409
|
+
A decay series for a specific GHG. This is retrieved from `../data/decay_multipliers.pkl`
|
|
410
|
+
|
|
411
|
+
Returns
|
|
412
|
+
-------
|
|
413
|
+
A function called `characterize_generic`, which in turn returns a TimeSeries dataframe that contains the forcing of the emission of the row over the given period based on the decay series of that biosphere flow.
|
|
414
|
+
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
def characterize_generic(
|
|
418
|
+
series,
|
|
419
|
+
period: int = 100,
|
|
420
|
+
cumulative=False,
|
|
421
|
+
) -> CharacterizedRow:
|
|
422
|
+
"""
|
|
423
|
+
Uses lookup generated in /dev/calculate_metrics.ipynb
|
|
424
|
+
Data originates from https://doi.org/10.1029/2019RG000691
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
series : array-like
|
|
429
|
+
A single row of the dynamic inventory dataframe.
|
|
430
|
+
period : int, optional
|
|
431
|
+
Time period for calculation (number of years), by default 100
|
|
432
|
+
cumulative : bool,
|
|
433
|
+
cumulative impact
|
|
434
|
+
|
|
435
|
+
Returns
|
|
436
|
+
-------
|
|
437
|
+
A TimeSeries dataframe that contains the forcing of the point emission from the row for each year in the given period.
|
|
438
|
+
date: datetime64[s]
|
|
439
|
+
amount: float (forcing at this timestep)
|
|
440
|
+
flow: str
|
|
441
|
+
activity: str
|
|
442
|
+
|
|
443
|
+
See also
|
|
444
|
+
--------
|
|
445
|
+
Joos2013: Relevant scientific publication on CRF: https://doi.org/10.5194/acp-13-2793-2013
|
|
446
|
+
Schivley2015: Relevant scientific publication on the numerical calculation of CRF: https://doi.org/10.1021/acs.est.5b01118
|
|
447
|
+
Forster2023: Updated numerical values from IPCC AR6 Chapter 7 (Table 7.15): https://doi.org/10.1017/9781009157896.009
|
|
448
|
+
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
date_beginning: np.datetime64 = series.date.to_numpy()
|
|
452
|
+
|
|
453
|
+
dates_characterized: np.ndarray = date_beginning + np.arange(
|
|
454
|
+
start=0, stop=period, dtype="timedelta64[Y]"
|
|
455
|
+
).astype("timedelta64[s]")
|
|
456
|
+
|
|
457
|
+
decay_multipliers = decay_series[:period]
|
|
458
|
+
|
|
459
|
+
forcing = np.array(series.amount * decay_multipliers, dtype="float64")
|
|
460
|
+
|
|
461
|
+
if not cumulative:
|
|
462
|
+
forcing = np.diff(forcing, prepend=0)
|
|
463
|
+
|
|
464
|
+
return CharacterizedRow(
|
|
465
|
+
date=np.array(dates_characterized, dtype="datetime64[s]"),
|
|
466
|
+
amount=forcing,
|
|
467
|
+
flow=series.flow,
|
|
468
|
+
activity=series.activity,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
return characterize_generic
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dynamic_characterization
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Collection of dynamic characterization functions for life cycle inventories with temporal information
|
|
5
5
|
Author-email: Timo Diepers <timo.diepers@ltt.rwth-aachen.de>
|
|
6
6
|
Maintainer-email: Timo Diepers <timo.diepers@ltt.rwth-aachen.de>
|
|
@@ -20,6 +20,9 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
20
20
|
Requires-Python: >=3.9
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pandas
|
|
24
|
+
Requires-Dist: numpy
|
|
25
|
+
Requires-Dist: bw2data
|
|
23
26
|
Provides-Extra: testing
|
|
24
27
|
Requires-Dist: dynamic_characterization; extra == "testing"
|
|
25
28
|
Requires-Dist: pytest; extra == "testing"
|
|
@@ -76,11 +79,12 @@ Here's an example of what such a function could look like:
|
|
|
76
79
|
|
|
77
80
|
```python
|
|
78
81
|
def characterize_something(series, period: int = 100, cumulative=False) -> pd.DataFrame:
|
|
79
|
-
date_beginning: np.datetime64 = series
|
|
80
|
-
|
|
82
|
+
date_beginning: np.datetime64 = series.date.to_numpy()
|
|
83
|
+
dates_characterized: np.ndarray = date_beginning + np.arange(
|
|
81
84
|
start=0, stop=period, dtype="timedelta64[Y]"
|
|
82
85
|
).astype("timedelta64[s]")
|
|
83
86
|
|
|
87
|
+
# let's assume some simple decay function
|
|
84
88
|
decay_multipliers: list = np.array(
|
|
85
89
|
[
|
|
86
90
|
1.234 * (1 - np.exp(-year / 56.789))
|
|
@@ -88,13 +92,14 @@ def characterize_something(series, period: int = 100, cumulative=False) -> pd.Da
|
|
|
88
92
|
]
|
|
89
93
|
)
|
|
90
94
|
|
|
91
|
-
forcing =
|
|
95
|
+
forcing = np.array(series.amount * decay_multipliers, dtype="float64")
|
|
96
|
+
|
|
92
97
|
if not cumulative:
|
|
93
|
-
forcing =
|
|
98
|
+
forcing = np.diff(forcing, prepend=0)
|
|
94
99
|
|
|
95
100
|
return pd.DataFrame(
|
|
96
101
|
{
|
|
97
|
-
"date":
|
|
102
|
+
"date": np.array(dates_characterized, dtype="datetime64[s]"),
|
|
98
103
|
"amount": forcing,
|
|
99
104
|
"flow": series.flow,
|
|
100
105
|
"activity": series.activity,
|
|
@@ -131,6 +136,9 @@ _dynamic_characterization_ is free and open source software.
|
|
|
131
136
|
If you encounter any problems,
|
|
132
137
|
please [file an issue][Issue Tracker] along with a detailed description.
|
|
133
138
|
|
|
139
|
+
## Support
|
|
140
|
+
|
|
141
|
+
If you have any questions or need help, do not hesitate to contact Timo Diepers ([timo.diepers@ltt.rwth-aachen.de](mailto:timo.diepers@ltt.rwth-aachen.de))
|
|
134
142
|
|
|
135
143
|
<!-- github-only -->
|
|
136
144
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
dynamic_characterization/__init__.py
|
|
6
|
+
dynamic_characterization/classes.py
|
|
7
|
+
dynamic_characterization/dynamic_characterization.py
|
|
8
|
+
dynamic_characterization.egg-info/PKG-INFO
|
|
9
|
+
dynamic_characterization.egg-info/SOURCES.txt
|
|
10
|
+
dynamic_characterization.egg-info/dependency_links.txt
|
|
11
|
+
dynamic_characterization.egg-info/requires.txt
|
|
12
|
+
dynamic_characterization.egg-info/top_level.txt
|
|
13
|
+
dynamic_characterization/temporalis/__init__.py
|
|
14
|
+
dynamic_characterization/temporalis/radiative_forcing.py
|
|
15
|
+
dynamic_characterization/timex/__init__.py
|
|
16
|
+
dynamic_characterization/timex/radiative_forcing.py
|
|
@@ -31,6 +31,9 @@ requires-python = ">=3.9"
|
|
|
31
31
|
dependencies = [
|
|
32
32
|
# dependencies as strings with quotes, e.g. "foo"
|
|
33
33
|
# You can add version requirements like "foo>2.0"
|
|
34
|
+
"pandas",
|
|
35
|
+
"numpy",
|
|
36
|
+
"bw2data",
|
|
34
37
|
]
|
|
35
38
|
|
|
36
39
|
[project.urls]
|
|
@@ -60,7 +63,11 @@ dev = [
|
|
|
60
63
|
[tool.setuptools]
|
|
61
64
|
license-files = ["LICENSE"]
|
|
62
65
|
include-package-data = true
|
|
63
|
-
packages = [
|
|
66
|
+
packages = [
|
|
67
|
+
"dynamic_characterization",
|
|
68
|
+
"dynamic_characterization.temporalis",
|
|
69
|
+
"dynamic_characterization.timex",
|
|
70
|
+
]
|
|
64
71
|
|
|
65
72
|
[tool.setuptools.dynamic]
|
|
66
73
|
version = {attr = "dynamic_characterization.__version__"}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
MANIFEST.in
|
|
3
|
-
README.md
|
|
4
|
-
pyproject.toml
|
|
5
|
-
dynamic_characterization/__init__.py
|
|
6
|
-
dynamic_characterization.egg-info/PKG-INFO
|
|
7
|
-
dynamic_characterization.egg-info/SOURCES.txt
|
|
8
|
-
dynamic_characterization.egg-info/dependency_links.txt
|
|
9
|
-
dynamic_characterization.egg-info/requires.txt
|
|
10
|
-
dynamic_characterization.egg-info/top_level.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|