shrecc 0.1.0.dev2__tar.gz → 0.1.0.dev3__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.
- {shrecc-0.1.0.dev2/shrecc.egg-info → shrecc-0.1.0.dev3}/PKG-INFO +3 -2
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/pyproject.toml +2 -1
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/__init__.py +1 -1
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/activity_mapping.py +4 -0
- shrecc-0.1.0.dev3/shrecc/analysis.py +482 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/database.py +9 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/mapping.py +230 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/pipeline.py +319 -42
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3/shrecc.egg-info}/PKG-INFO +3 -2
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc.egg-info/SOURCES.txt +2 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc.egg-info/requires.txt +2 -1
- shrecc-0.1.0.dev3/tests/test_analysis.py +171 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/tests/test_database.py +8 -2
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/tests/test_mapping.py +162 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/tests/test_pipeline.py +306 -2
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/LICENSE +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/MANIFEST.in +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/README.md +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/setup.cfg +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/_legacy_database.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/_legacy_treatment.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/_legacy_tyndp.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/data/__init__.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/data/el_map_all_norm.csv +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/data/el_map_all_norm_w_ned.csv +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/data/generation_units_by_country.csv +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/data/techs_agg.json +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/data/tyndp/__init__.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/data/tyndp/remind-eu-topology.json +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/data/tyndp/tyndp_activities.csv +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/data/tyndp/tyndp_connections.csv +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/data/tyndp/tyndp_countries.csv +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/download.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/energy_charts.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/premise_mapping.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/result_store.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/solver.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/treatment.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/treatment_fiona.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc/tyndp.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc.egg-info/dependency_links.txt +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/shrecc.egg-info/top_level.txt +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/tests/test_architecture.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/tests/test_energy_charts.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/tests/test_energy_charts_api.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/tests/test_premise_mapping.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/tests/test_result_store.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/tests/test_solver.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/tests/test_treatment.py +0 -0
- {shrecc-0.1.0.dev2 → shrecc-0.1.0.dev3}/tests/test_tyndp_scenario.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shrecc
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev3
|
|
4
4
|
Summary: SHRECC: Smooth Hourly Resolution Electricity Consumption Calculation
|
|
5
5
|
Author-email: Sabina Bednářová <sabina.bednarova@list.lu>
|
|
6
6
|
Maintainer-email: Sabina Bednářová <sabina.bednarova@list.lu>
|
|
@@ -21,13 +21,14 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
+
Requires-Dist: bw2calc
|
|
24
25
|
Requires-Dist: bw2data>=4.4.2
|
|
25
26
|
Requires-Dist: pypardiso
|
|
26
27
|
Requires-Dist: pandas<3,>=2
|
|
27
28
|
Requires-Dist: numpy<3
|
|
28
29
|
Requires-Dist: numpy>2; python_version >= "3.13"
|
|
29
30
|
Requires-Dist: requests>=2.32
|
|
30
|
-
Requires-Dist: scipy
|
|
31
|
+
Requires-Dist: scipy
|
|
31
32
|
Requires-Dist: appdirs
|
|
32
33
|
Requires-Dist: packaging
|
|
33
34
|
Requires-Dist: tqdm
|
|
@@ -31,13 +31,14 @@ requires-python = ">=3.10"
|
|
|
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
|
+
"bw2calc",
|
|
34
35
|
"bw2data>=4.4.2",
|
|
35
36
|
"pypardiso",
|
|
36
37
|
"pandas>=2,<3",
|
|
37
38
|
"numpy<3",
|
|
38
39
|
'numpy>2;python_version>="3.13"',
|
|
39
40
|
"requests>=2.32",
|
|
40
|
-
"scipy
|
|
41
|
+
"scipy",
|
|
41
42
|
"appdirs",
|
|
42
43
|
"packaging",
|
|
43
44
|
"tqdm",
|
|
@@ -7,18 +7,22 @@ from shrecc.mapping import (
|
|
|
7
7
|
DEFAULT_COUNTRY_ALIASES,
|
|
8
8
|
DEFAULT_FALLBACK_ACTIVITY,
|
|
9
9
|
activity_mix_to_database_table,
|
|
10
|
+
aggregate_consumption_mix,
|
|
10
11
|
filter_consumption_mix_time,
|
|
11
12
|
load_ecoinvent_mapping,
|
|
12
13
|
map_consumption_mix_to_ecoinvent_activities,
|
|
13
14
|
mapping_gap_to_report,
|
|
15
|
+
validate_consumption_profile,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
__all__ = (
|
|
17
19
|
"DEFAULT_COUNTRY_ALIASES",
|
|
18
20
|
"DEFAULT_FALLBACK_ACTIVITY",
|
|
19
21
|
"activity_mix_to_database_table",
|
|
22
|
+
"aggregate_consumption_mix",
|
|
20
23
|
"filter_consumption_mix_time",
|
|
21
24
|
"load_ecoinvent_mapping",
|
|
22
25
|
"map_consumption_mix_to_ecoinvent_activities",
|
|
23
26
|
"mapping_gap_to_report",
|
|
27
|
+
"validate_consumption_profile",
|
|
24
28
|
)
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""Brightway analysis helpers for SHRECC electricity inventories."""
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from gc import collect
|
|
6
|
+
|
|
7
|
+
import bw2data as bd
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
DEFAULT_COUNTRY_ALIASES = {"NIE": "GB", "UK": "GB"}
|
|
12
|
+
DEFAULT_BACKGROUND_LABEL = "Background"
|
|
13
|
+
|
|
14
|
+
__all__ = (
|
|
15
|
+
"compare_electricity_generation_mixes",
|
|
16
|
+
"compare_electricity_lcia",
|
|
17
|
+
"resolve_electricity_generation_shares",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _ActivityResolver:
|
|
22
|
+
"""Cache Brightway lookups for one analysis call."""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self._database_indexes = {}
|
|
26
|
+
self._nodes = {}
|
|
27
|
+
self._metadata = {}
|
|
28
|
+
self._electricity_inputs = {}
|
|
29
|
+
|
|
30
|
+
def find_unique(self, database_name, activity_name, location):
|
|
31
|
+
if database_name not in self._database_indexes:
|
|
32
|
+
index = {}
|
|
33
|
+
duplicates = set()
|
|
34
|
+
for activity in bd.Database(database_name):
|
|
35
|
+
key = (activity.get("name"), activity.get("location"))
|
|
36
|
+
if key in index:
|
|
37
|
+
duplicates.add(key)
|
|
38
|
+
else:
|
|
39
|
+
index[key] = activity
|
|
40
|
+
self._database_indexes[database_name] = (index, duplicates)
|
|
41
|
+
|
|
42
|
+
index, duplicates = self._database_indexes[database_name]
|
|
43
|
+
key = (activity_name, location)
|
|
44
|
+
return None if key in duplicates else index.get(key)
|
|
45
|
+
|
|
46
|
+
def node(self, activity_id):
|
|
47
|
+
if activity_id not in self._nodes:
|
|
48
|
+
self._nodes[activity_id] = bd.get_node(id=activity_id)
|
|
49
|
+
return self._nodes[activity_id]
|
|
50
|
+
|
|
51
|
+
def metadata(self, activity_id):
|
|
52
|
+
if activity_id not in self._metadata:
|
|
53
|
+
activity = self.node(activity_id)
|
|
54
|
+
self._metadata[activity_id] = (
|
|
55
|
+
activity.get("name"),
|
|
56
|
+
activity.get("location"),
|
|
57
|
+
)
|
|
58
|
+
return self._metadata[activity_id]
|
|
59
|
+
|
|
60
|
+
def electricity_inputs(self, activity_id):
|
|
61
|
+
if activity_id not in self._electricity_inputs:
|
|
62
|
+
activity = self.node(activity_id)
|
|
63
|
+
self._electricity_inputs[activity_id] = tuple(
|
|
64
|
+
(exchange.input.id, float(exchange["amount"]))
|
|
65
|
+
for exchange in activity.technosphere()
|
|
66
|
+
if float(exchange["amount"]) > 0
|
|
67
|
+
and _is_electricity_activity(exchange.input)
|
|
68
|
+
)
|
|
69
|
+
return self._electricity_inputs[activity_id]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve_electricity_generation_shares(
|
|
73
|
+
root_activity,
|
|
74
|
+
*,
|
|
75
|
+
tolerance=1e-10,
|
|
76
|
+
max_iterations=500,
|
|
77
|
+
resolver=None,
|
|
78
|
+
):
|
|
79
|
+
"""Resolve delivered electricity through wrappers to generation shares.
|
|
80
|
+
|
|
81
|
+
Positive electricity exchanges are propagated through markets, voltage
|
|
82
|
+
transformations, import activities, and IAM-region wrappers. Traversal
|
|
83
|
+
stops at generating activities or at terminal activities without further
|
|
84
|
+
electricity inputs. Activities with the same name are combined across
|
|
85
|
+
locations so foreground and background technology families can be
|
|
86
|
+
compared directly.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
root_activity: Brightway activity supplying delivered electricity.
|
|
90
|
+
tolerance: Remaining wrapper volume below which traversal is complete.
|
|
91
|
+
max_iterations: Maximum wrapper-propagation iterations.
|
|
92
|
+
resolver: Optional run-scoped resolver, primarily for composing
|
|
93
|
+
repeated analyses.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
A pair containing a descending Series of normalized generation shares
|
|
97
|
+
and a diagnostics dictionary.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
RuntimeError: If wrapper traversal does not converge.
|
|
101
|
+
ValueError: If no terminal electricity supply is found.
|
|
102
|
+
"""
|
|
103
|
+
if tolerance <= 0:
|
|
104
|
+
raise ValueError("tolerance must be greater than zero")
|
|
105
|
+
if not isinstance(max_iterations, int) or max_iterations <= 0:
|
|
106
|
+
raise ValueError("max_iterations must be a positive integer")
|
|
107
|
+
|
|
108
|
+
resolver = resolver or _ActivityResolver()
|
|
109
|
+
terminal_volumes = defaultdict(float)
|
|
110
|
+
nonstandard_terminals = defaultdict(float)
|
|
111
|
+
frontier = {root_activity.id: 1.0}
|
|
112
|
+
remaining_volume = 0.0
|
|
113
|
+
|
|
114
|
+
for iteration in range(1, max_iterations + 1):
|
|
115
|
+
next_frontier = defaultdict(float)
|
|
116
|
+
for activity_id, required_volume in frontier.items():
|
|
117
|
+
activity = resolver.node(activity_id)
|
|
118
|
+
inputs = resolver.electricity_inputs(activity_id)
|
|
119
|
+
generation = _is_generation_activity(activity)
|
|
120
|
+
if generation or not inputs:
|
|
121
|
+
name, location = resolver.metadata(activity_id)
|
|
122
|
+
terminal_volumes[name] += required_volume
|
|
123
|
+
if not generation:
|
|
124
|
+
nonstandard_terminals[(name, location)] += required_volume
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
for input_id, amount in inputs:
|
|
128
|
+
next_frontier[input_id] += required_volume * amount
|
|
129
|
+
|
|
130
|
+
remaining_volume = sum(abs(value) for value in next_frontier.values())
|
|
131
|
+
if remaining_volume <= tolerance:
|
|
132
|
+
break
|
|
133
|
+
frontier = dict(next_frontier)
|
|
134
|
+
else:
|
|
135
|
+
largest = sorted(
|
|
136
|
+
(
|
|
137
|
+
(*resolver.metadata(activity_id), volume)
|
|
138
|
+
for activity_id, volume in frontier.items()
|
|
139
|
+
),
|
|
140
|
+
key=lambda row: abs(row[-1]),
|
|
141
|
+
reverse=True,
|
|
142
|
+
)[:5]
|
|
143
|
+
raise RuntimeError(
|
|
144
|
+
"Electricity wrapper traversal did not converge. Largest "
|
|
145
|
+
f"remaining activities: {largest}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
terminal_volumes = pd.Series(terminal_volumes, dtype=float)
|
|
149
|
+
terminal_total = terminal_volumes.sum()
|
|
150
|
+
if terminal_total <= 0:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"No terminal electricity generation found below {root_activity}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
diagnostics = {
|
|
156
|
+
"iterations": iteration,
|
|
157
|
+
"terminal generation volume": terminal_total,
|
|
158
|
+
"remaining wrapper volume": remaining_volume,
|
|
159
|
+
"nonstandard terminal volume": sum(nonstandard_terminals.values()),
|
|
160
|
+
}
|
|
161
|
+
shares = (terminal_volumes / terminal_total).sort_values(ascending=False)
|
|
162
|
+
return shares, diagnostics
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def compare_electricity_generation_mixes(
|
|
166
|
+
foreground_databases,
|
|
167
|
+
background_databases,
|
|
168
|
+
*,
|
|
169
|
+
countries,
|
|
170
|
+
years,
|
|
171
|
+
background_label=DEFAULT_BACKGROUND_LABEL,
|
|
172
|
+
country_aliases=None,
|
|
173
|
+
):
|
|
174
|
+
"""Compare foreground and background delivered-electricity compositions.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
foreground_databases: Mapping from display label to ``{year:
|
|
178
|
+
database_name}`` mappings. Foreground databases are expected to
|
|
179
|
+
contain activities named ``"Electricity mix in {country}, {year}"``.
|
|
180
|
+
background_databases: Mapping from year to ecoinvent or premise
|
|
181
|
+
background database name.
|
|
182
|
+
countries: Consumer-country codes to compare.
|
|
183
|
+
years: Model years to compare.
|
|
184
|
+
background_label: Display label assigned to background rows.
|
|
185
|
+
country_aliases: Optional country-to-background-location aliases.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Three DataFrames: long-form generation shares, missing activity roots,
|
|
189
|
+
and traversal diagnostics.
|
|
190
|
+
"""
|
|
191
|
+
years, countries, foreground_databases, background_databases = (
|
|
192
|
+
_validate_comparison_inputs(
|
|
193
|
+
foreground_databases,
|
|
194
|
+
background_databases,
|
|
195
|
+
countries,
|
|
196
|
+
years,
|
|
197
|
+
background_label,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
resolver = _ActivityResolver()
|
|
201
|
+
aliases = {**DEFAULT_COUNTRY_ALIASES, **(country_aliases or {})}
|
|
202
|
+
mix_rows = []
|
|
203
|
+
gaps = []
|
|
204
|
+
diagnostics_rows = []
|
|
205
|
+
|
|
206
|
+
for year in years:
|
|
207
|
+
for country in countries:
|
|
208
|
+
roots = _comparison_roots(
|
|
209
|
+
foreground_databases,
|
|
210
|
+
background_databases,
|
|
211
|
+
year,
|
|
212
|
+
country,
|
|
213
|
+
background_label,
|
|
214
|
+
aliases,
|
|
215
|
+
resolver,
|
|
216
|
+
)
|
|
217
|
+
for model, root_activity in roots.items():
|
|
218
|
+
if root_activity is None:
|
|
219
|
+
gaps.append(
|
|
220
|
+
{
|
|
221
|
+
"year": year,
|
|
222
|
+
"country": country,
|
|
223
|
+
"model": model,
|
|
224
|
+
"missing": "delivered low-voltage electricity activity",
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
shares, diagnostics = resolve_electricity_generation_shares(
|
|
230
|
+
root_activity,
|
|
231
|
+
resolver=resolver,
|
|
232
|
+
)
|
|
233
|
+
diagnostics_rows.append(
|
|
234
|
+
{
|
|
235
|
+
"year": year,
|
|
236
|
+
"country": country,
|
|
237
|
+
"model": model,
|
|
238
|
+
"root activity": root_activity.get("name"),
|
|
239
|
+
**diagnostics,
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
mix_rows.extend(
|
|
243
|
+
{
|
|
244
|
+
"year": year,
|
|
245
|
+
"country": country,
|
|
246
|
+
"model": model,
|
|
247
|
+
"technology": technology,
|
|
248
|
+
"share": share,
|
|
249
|
+
}
|
|
250
|
+
for technology, share in shares.items()
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
pd.DataFrame(
|
|
255
|
+
mix_rows,
|
|
256
|
+
columns=["year", "country", "model", "technology", "share"],
|
|
257
|
+
),
|
|
258
|
+
pd.DataFrame(
|
|
259
|
+
gaps,
|
|
260
|
+
columns=["year", "country", "model", "missing"],
|
|
261
|
+
),
|
|
262
|
+
pd.DataFrame(diagnostics_rows),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def compare_electricity_lcia(
|
|
267
|
+
foreground_databases,
|
|
268
|
+
background_databases,
|
|
269
|
+
*,
|
|
270
|
+
countries,
|
|
271
|
+
years,
|
|
272
|
+
method,
|
|
273
|
+
background_label=DEFAULT_BACKGROUND_LABEL,
|
|
274
|
+
country_aliases=None,
|
|
275
|
+
verbose=False,
|
|
276
|
+
):
|
|
277
|
+
"""Calculate foreground and background LCIA scores per kWh.
|
|
278
|
+
|
|
279
|
+
Calculations are batched by year to bound Brightway matrix memory.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
foreground_databases: Mapping from display label to ``{year:
|
|
283
|
+
database_name}`` mappings.
|
|
284
|
+
background_databases: Mapping from year to background database name.
|
|
285
|
+
countries: Consumer-country codes to compare.
|
|
286
|
+
years: Model years to compare.
|
|
287
|
+
method: Brightway impact-category tuple.
|
|
288
|
+
background_label: Display label assigned to background rows.
|
|
289
|
+
country_aliases: Optional country-to-background-location aliases.
|
|
290
|
+
verbose: Print each year before its LCIA calculation.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Two DataFrames: long-form LCIA scores and missing activity roots.
|
|
294
|
+
"""
|
|
295
|
+
years, countries, foreground_databases, background_databases = (
|
|
296
|
+
_validate_comparison_inputs(
|
|
297
|
+
foreground_databases,
|
|
298
|
+
background_databases,
|
|
299
|
+
countries,
|
|
300
|
+
years,
|
|
301
|
+
background_label,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
resolver = _ActivityResolver()
|
|
305
|
+
aliases = {**DEFAULT_COUNTRY_ALIASES, **(country_aliases or {})}
|
|
306
|
+
rows = []
|
|
307
|
+
gaps = []
|
|
308
|
+
|
|
309
|
+
for year in years:
|
|
310
|
+
if verbose:
|
|
311
|
+
print(f"Calculating {method} for {year}")
|
|
312
|
+
demands = {}
|
|
313
|
+
metadata = {}
|
|
314
|
+
for country in countries:
|
|
315
|
+
roots = _comparison_roots(
|
|
316
|
+
foreground_databases,
|
|
317
|
+
background_databases,
|
|
318
|
+
year,
|
|
319
|
+
country,
|
|
320
|
+
background_label,
|
|
321
|
+
aliases,
|
|
322
|
+
resolver,
|
|
323
|
+
)
|
|
324
|
+
for model, activity in roots.items():
|
|
325
|
+
if activity is None:
|
|
326
|
+
gaps.append(
|
|
327
|
+
{
|
|
328
|
+
"year": year,
|
|
329
|
+
"country": country,
|
|
330
|
+
"model": model,
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
continue
|
|
334
|
+
label = f"{year}:{country}:{model}"
|
|
335
|
+
demands[label] = {activity.id: 1}
|
|
336
|
+
metadata[label] = (country, model)
|
|
337
|
+
|
|
338
|
+
if not demands:
|
|
339
|
+
continue
|
|
340
|
+
scores = _run_multilca(demands, method)
|
|
341
|
+
rows.extend(
|
|
342
|
+
{
|
|
343
|
+
"year": year,
|
|
344
|
+
"country": country,
|
|
345
|
+
"model": model,
|
|
346
|
+
"score": scores[label],
|
|
347
|
+
}
|
|
348
|
+
for label, (country, model) in metadata.items()
|
|
349
|
+
)
|
|
350
|
+
collect()
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
pd.DataFrame(
|
|
354
|
+
rows,
|
|
355
|
+
columns=["year", "country", "model", "score"],
|
|
356
|
+
),
|
|
357
|
+
pd.DataFrame(
|
|
358
|
+
gaps,
|
|
359
|
+
columns=["year", "country", "model"],
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _run_multilca(demands, method):
|
|
365
|
+
import bw2calc as bc
|
|
366
|
+
|
|
367
|
+
method_config = {"impact_categories": [method]}
|
|
368
|
+
data_objects = bd.get_multilca_data_objs(
|
|
369
|
+
functional_units=demands,
|
|
370
|
+
method_config=method_config,
|
|
371
|
+
)
|
|
372
|
+
lca = bc.MultiLCA(
|
|
373
|
+
demands=demands,
|
|
374
|
+
method_config=method_config,
|
|
375
|
+
data_objs=data_objects,
|
|
376
|
+
)
|
|
377
|
+
lca.lci()
|
|
378
|
+
lca.lcia()
|
|
379
|
+
scores = {
|
|
380
|
+
label: float(np.asarray(lca.scores[(method, label)]).reshape(-1)[0])
|
|
381
|
+
for label in demands
|
|
382
|
+
}
|
|
383
|
+
del lca, data_objects
|
|
384
|
+
return scores
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _comparison_roots(
|
|
388
|
+
foreground_databases,
|
|
389
|
+
background_databases,
|
|
390
|
+
year,
|
|
391
|
+
country,
|
|
392
|
+
background_label,
|
|
393
|
+
aliases,
|
|
394
|
+
resolver,
|
|
395
|
+
):
|
|
396
|
+
roots = {
|
|
397
|
+
label: resolver.find_unique(
|
|
398
|
+
databases[year],
|
|
399
|
+
f"Electricity mix in {country}, {year}",
|
|
400
|
+
country,
|
|
401
|
+
)
|
|
402
|
+
for label, databases in foreground_databases.items()
|
|
403
|
+
}
|
|
404
|
+
roots[background_label] = resolver.find_unique(
|
|
405
|
+
background_databases[year],
|
|
406
|
+
"market for electricity, low voltage",
|
|
407
|
+
aliases.get(country, country),
|
|
408
|
+
)
|
|
409
|
+
return roots
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _validate_comparison_inputs(
|
|
413
|
+
foreground_databases,
|
|
414
|
+
background_databases,
|
|
415
|
+
countries,
|
|
416
|
+
years,
|
|
417
|
+
background_label,
|
|
418
|
+
):
|
|
419
|
+
if not isinstance(foreground_databases, Mapping) or not foreground_databases:
|
|
420
|
+
raise ValueError("foreground_databases must be a non-empty mapping")
|
|
421
|
+
if not isinstance(background_databases, Mapping):
|
|
422
|
+
raise TypeError("background_databases must be a year mapping")
|
|
423
|
+
if not isinstance(background_label, str) or not background_label:
|
|
424
|
+
raise ValueError("background_label must be a non-empty string")
|
|
425
|
+
if background_label in foreground_databases:
|
|
426
|
+
raise ValueError("background_label must differ from foreground database labels")
|
|
427
|
+
|
|
428
|
+
years = tuple(int(year) for year in years)
|
|
429
|
+
countries = tuple(str(country) for country in countries)
|
|
430
|
+
if not years:
|
|
431
|
+
raise ValueError("years cannot be empty")
|
|
432
|
+
if not countries:
|
|
433
|
+
raise ValueError("countries cannot be empty")
|
|
434
|
+
|
|
435
|
+
normalized_foregrounds = {}
|
|
436
|
+
for label, databases in foreground_databases.items():
|
|
437
|
+
if not isinstance(label, str) or not label:
|
|
438
|
+
raise ValueError("foreground database labels must be non-empty strings")
|
|
439
|
+
normalized_foregrounds[label] = _require_year_database_mapping(
|
|
440
|
+
databases,
|
|
441
|
+
years,
|
|
442
|
+
f"foreground_databases[{label!r}]",
|
|
443
|
+
)
|
|
444
|
+
normalized_backgrounds = _require_year_database_mapping(
|
|
445
|
+
background_databases,
|
|
446
|
+
years,
|
|
447
|
+
"background_databases",
|
|
448
|
+
)
|
|
449
|
+
return years, countries, normalized_foregrounds, normalized_backgrounds
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _require_year_database_mapping(databases, years, name):
|
|
453
|
+
if not isinstance(databases, Mapping):
|
|
454
|
+
raise TypeError(f"{name} must be a year mapping")
|
|
455
|
+
missing = set(years).difference(databases)
|
|
456
|
+
if missing:
|
|
457
|
+
raise ValueError(
|
|
458
|
+
f"{name} is missing years: " + ", ".join(map(str, sorted(missing)))
|
|
459
|
+
)
|
|
460
|
+
normalized = {}
|
|
461
|
+
for year in years:
|
|
462
|
+
database_name = databases[year]
|
|
463
|
+
if not isinstance(database_name, str) or not database_name:
|
|
464
|
+
raise ValueError(f"{name}[{year}] must be a non-empty string")
|
|
465
|
+
normalized[year] = database_name
|
|
466
|
+
return normalized
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _is_electricity_activity(activity):
|
|
470
|
+
product = str(activity.get("reference product", "")).lower()
|
|
471
|
+
unit = str(activity.get("unit", "")).lower()
|
|
472
|
+
return "electricity" in product and unit in {"kilowatt hour", "kwh"}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _is_generation_activity(activity):
|
|
476
|
+
name = str(activity.get("name", "")).lower()
|
|
477
|
+
return (
|
|
478
|
+
name.startswith("electricity production")
|
|
479
|
+
or "heat and power co-generation" in name
|
|
480
|
+
or (name.startswith("treatment of ") and "power plant" in name)
|
|
481
|
+
or "electricity generation" in name
|
|
482
|
+
)
|
|
@@ -366,6 +366,7 @@ def create_activity_dict(
|
|
|
366
366
|
db_name,
|
|
367
367
|
eidb_name=None,
|
|
368
368
|
year=None,
|
|
369
|
+
consumption_profile=None,
|
|
369
370
|
):
|
|
370
371
|
"""
|
|
371
372
|
Creates a dictionary of activities for the BW database based on the filtered dataframe and known inputs.
|
|
@@ -380,6 +381,8 @@ def create_activity_dict(
|
|
|
380
381
|
year (int, optional): Model year stored on each activity and included in
|
|
381
382
|
its name. If a time-indexed column already contains the year, the
|
|
382
383
|
name is left unchanged to avoid repeating it.
|
|
384
|
+
consumption_profile (str, optional): Temporal weighting represented by
|
|
385
|
+
the foreground inventory, stored as activity metadata.
|
|
383
386
|
|
|
384
387
|
Returns:
|
|
385
388
|
dict: A dictionary containing activities to be written to the BW2 database.
|
|
@@ -420,6 +423,8 @@ def create_activity_dict(
|
|
|
420
423
|
}
|
|
421
424
|
if activity_year is not None:
|
|
422
425
|
act["year"] = activity_year
|
|
426
|
+
if consumption_profile is not None:
|
|
427
|
+
act["consumption_profile"] = str(consumption_profile)
|
|
423
428
|
# Add the production exchange
|
|
424
429
|
act["exchanges"].append(
|
|
425
430
|
{
|
|
@@ -539,6 +544,7 @@ def create_database(
|
|
|
539
544
|
network=True,
|
|
540
545
|
strict=False,
|
|
541
546
|
year=None,
|
|
547
|
+
consumption_profile=None,
|
|
542
548
|
):
|
|
543
549
|
"""
|
|
544
550
|
Creates an "ecoinvent-like" BW database based on a previously filtered dataframe.
|
|
@@ -555,6 +561,8 @@ def create_database(
|
|
|
555
561
|
cannot be matched uniquely.
|
|
556
562
|
year (int, optional): Model year included in foreground activity names
|
|
557
563
|
and stored as activity metadata.
|
|
564
|
+
consumption_profile (str, optional): Temporal weighting stored as
|
|
565
|
+
foreground activity metadata.
|
|
558
566
|
|
|
559
567
|
Returns:
|
|
560
568
|
None
|
|
@@ -576,6 +584,7 @@ def create_database(
|
|
|
576
584
|
db_name,
|
|
577
585
|
eidb_name=eidb_name,
|
|
578
586
|
year=year,
|
|
587
|
+
consumption_profile=consumption_profile,
|
|
579
588
|
)
|
|
580
589
|
elec_db = setup_database(project_name, db_name)
|
|
581
590
|
elec_db.write(activities)
|