shrecc 0.1.0.dev3__tar.gz → 0.1.2.dev1__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.
Files changed (53) hide show
  1. {shrecc-0.1.0.dev3/shrecc.egg-info → shrecc-0.1.2.dev1}/PKG-INFO +28 -7
  2. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/README.md +27 -4
  3. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/pyproject.toml +1 -3
  4. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/__init__.py +3 -1
  5. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/activity_mapping.py +2 -0
  6. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/analysis.py +3 -2
  7. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/database.py +203 -108
  8. shrecc-0.1.2.dev1/shrecc/lcia.py +644 -0
  9. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/mapping.py +170 -35
  10. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/pipeline.py +233 -25
  11. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/premise_mapping.py +41 -37
  12. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/tyndp.py +22 -4
  13. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1/shrecc.egg-info}/PKG-INFO +28 -7
  14. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc.egg-info/SOURCES.txt +3 -0
  15. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc.egg-info/requires.txt +0 -2
  16. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/tests/test_analysis.py +4 -1
  17. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/tests/test_database.py +120 -129
  18. shrecc-0.1.2.dev1/tests/test_lcia.py +331 -0
  19. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/tests/test_mapping.py +84 -0
  20. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/tests/test_pipeline.py +251 -0
  21. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/tests/test_premise_mapping.py +18 -0
  22. shrecc-0.1.2.dev1/tests/test_tyndp_paths_download.py +125 -0
  23. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/LICENSE +0 -0
  24. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/MANIFEST.in +0 -0
  25. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/setup.cfg +0 -0
  26. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/_legacy_database.py +0 -0
  27. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/_legacy_treatment.py +0 -0
  28. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/_legacy_tyndp.py +0 -0
  29. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/data/__init__.py +0 -0
  30. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/data/el_map_all_norm.csv +0 -0
  31. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/data/el_map_all_norm_w_ned.csv +0 -0
  32. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/data/generation_units_by_country.csv +0 -0
  33. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/data/techs_agg.json +0 -0
  34. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/data/tyndp/__init__.py +0 -0
  35. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/data/tyndp/remind-eu-topology.json +0 -0
  36. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/data/tyndp/tyndp_activities.csv +0 -0
  37. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/data/tyndp/tyndp_connections.csv +0 -0
  38. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/data/tyndp/tyndp_countries.csv +0 -0
  39. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/download.py +0 -0
  40. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/energy_charts.py +0 -0
  41. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/result_store.py +0 -0
  42. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/solver.py +0 -0
  43. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/treatment.py +0 -0
  44. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc/treatment_fiona.py +0 -0
  45. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc.egg-info/dependency_links.txt +0 -0
  46. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/shrecc.egg-info/top_level.txt +0 -0
  47. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/tests/test_architecture.py +0 -0
  48. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/tests/test_energy_charts.py +0 -0
  49. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/tests/test_energy_charts_api.py +0 -0
  50. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/tests/test_result_store.py +0 -0
  51. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/tests/test_solver.py +0 -0
  52. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/tests/test_treatment.py +0 -0
  53. {shrecc-0.1.0.dev3 → shrecc-0.1.2.dev1}/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.dev3
3
+ Version: 0.1.2.dev1
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>
@@ -55,13 +55,11 @@ Requires-Dist: jupyterlab; extra == "notebooks"
55
55
  Requires-Dist: jupyter-black>=0.4.0; extra == "notebooks"
56
56
  Requires-Dist: matplotlib>=3.10.9; extra == "notebooks"
57
57
  Provides-Extra: docs
58
- Requires-Dist: furo==2024.1.29; extra == "docs"
59
58
  Requires-Dist: myst_parser==2.0.0; extra == "docs"
60
59
  Requires-Dist: sphinx==7.2.6; extra == "docs"
61
60
  Requires-Dist: sphinx-autobuild>=2024.10.3; extra == "docs"
62
61
  Requires-Dist: ipython>=8.37.0; extra == "docs"
63
62
  Requires-Dist: sphinx-rtd-theme>=3.0.2; extra == "docs"
64
- Requires-Dist: myst-parser>=2.0.0; extra == "docs"
65
63
  Requires-Dist: sphinx-design>=0.6.1; extra == "docs"
66
64
  Requires-Dist: sphinx-copybutton>=0.5.2; extra == "docs"
67
65
  Requires-Dist: sphinx-autoapi>=3.5.0; extra == "docs"
@@ -85,7 +83,8 @@ scenario data for prospective years, while exposing the same workflow for both.
85
83
  - Hourly production, trade, consumption-volume, and consumption-mix results.
86
84
  - One reduced country-network solver shared by Energy Charts and TYNDP data.
87
85
  - Historical ecoinvent allocation and prospective premise/IAM mapping.
88
- - Hourly, daily, weekly, monthly, and explicit timestamp selections.
86
+ - Annual or monthly Brightway inventories from range-based or explicit
87
+ timestamp selections, plus hourly LCIA without hourly foreground activities.
89
88
  - Inspectable intermediate results before any Brightway database is changed.
90
89
  - Resumable source acquisition and compressed, time-chunked result caches.
91
90
  - Brightway 2 and 2.5 database writing through `NewDatabase`.
@@ -153,6 +152,7 @@ electricity = NewDatabase(
153
152
  hour_range=[10, 14],
154
153
  project_name="SHRECCei311",
155
154
  source="auto",
155
+ inventory_resolution="annual",
156
156
  )
157
157
 
158
158
  electricity.create()
@@ -162,10 +162,30 @@ electricity.write()
162
162
  For a multi-year run, the month/day/time selection is reused for each year and
163
163
  `write()` creates one foreground database per year. A year suffix is added to
164
164
  `my_db_name` automatically unless explicit year-specific names are supplied.
165
- Foreground activity names and metadata also record their model year.
165
+ Foreground activity names and metadata also record their modeled period.
166
166
 
167
- Intermediate canonical results and mapped tables remain available on the class
168
- instance through `results(year)` and `table(year)`. Use
167
+ `inventory_resolution` controls how many foreground activities are written:
168
+ `"annual"` creates one activity per country and year, `"monthly"` creates one
169
+ per country and selected calendar month. Both resolutions apply
170
+ `consumption_profile` within each period. Hourly inventories are not written
171
+ as Brightway activities.
172
+
173
+ Hourly LCIA is calculated directly from the retained hourly mixes. By default,
174
+ SHRECC selects every installed EF v3.1 impact category, scores each unique
175
+ background input once, and combines those scores with the hourly coefficients:
176
+
177
+ ```python
178
+ lcia_results = electricity.lcia()
179
+ hourly_intensity = lcia_results.hourly(2040)["intensity"]
180
+ annual_intensity = lcia_results.annual()
181
+ ```
182
+
183
+ Use `electricity.lcia(methods=..., engine="multilca")` to validate the
184
+ optimized calculation with composite Brightway MultiLCA functional units.
185
+ Neither engine writes hourly foreground activities.
186
+
187
+ Intermediate canonical results and annual/monthly mapped tables remain
188
+ available on the class instance through `results(year)` and `table(year)`. Use
169
189
  `mapping_report(year)` to inspect any historical technologies assigned to a
170
190
  country-specific high-voltage fallback activity.
171
191
 
@@ -173,6 +193,7 @@ country-specific high-voltage fallback activity.
173
193
  ## Contributing
174
194
 
175
195
  Please take a look at the [DEVELOPPING.md](https://git.list.lu/shrecc_project/shrecc/-/blob/main/DEVELOPPING.md) file for details on how to contribute code to the repository.
196
+ Notably for the versioning.
176
197
 
177
198
  ## License
178
199
 
@@ -13,7 +13,8 @@ scenario data for prospective years, while exposing the same workflow for both.
13
13
  - Hourly production, trade, consumption-volume, and consumption-mix results.
14
14
  - One reduced country-network solver shared by Energy Charts and TYNDP data.
15
15
  - Historical ecoinvent allocation and prospective premise/IAM mapping.
16
- - Hourly, daily, weekly, monthly, and explicit timestamp selections.
16
+ - Annual or monthly Brightway inventories from range-based or explicit
17
+ timestamp selections, plus hourly LCIA without hourly foreground activities.
17
18
  - Inspectable intermediate results before any Brightway database is changed.
18
19
  - Resumable source acquisition and compressed, time-chunked result caches.
19
20
  - Brightway 2 and 2.5 database writing through `NewDatabase`.
@@ -81,6 +82,7 @@ electricity = NewDatabase(
81
82
  hour_range=[10, 14],
82
83
  project_name="SHRECCei311",
83
84
  source="auto",
85
+ inventory_resolution="annual",
84
86
  )
85
87
 
86
88
  electricity.create()
@@ -90,10 +92,30 @@ electricity.write()
90
92
  For a multi-year run, the month/day/time selection is reused for each year and
91
93
  `write()` creates one foreground database per year. A year suffix is added to
92
94
  `my_db_name` automatically unless explicit year-specific names are supplied.
93
- Foreground activity names and metadata also record their model year.
95
+ Foreground activity names and metadata also record their modeled period.
94
96
 
95
- Intermediate canonical results and mapped tables remain available on the class
96
- instance through `results(year)` and `table(year)`. Use
97
+ `inventory_resolution` controls how many foreground activities are written:
98
+ `"annual"` creates one activity per country and year, `"monthly"` creates one
99
+ per country and selected calendar month. Both resolutions apply
100
+ `consumption_profile` within each period. Hourly inventories are not written
101
+ as Brightway activities.
102
+
103
+ Hourly LCIA is calculated directly from the retained hourly mixes. By default,
104
+ SHRECC selects every installed EF v3.1 impact category, scores each unique
105
+ background input once, and combines those scores with the hourly coefficients:
106
+
107
+ ```python
108
+ lcia_results = electricity.lcia()
109
+ hourly_intensity = lcia_results.hourly(2040)["intensity"]
110
+ annual_intensity = lcia_results.annual()
111
+ ```
112
+
113
+ Use `electricity.lcia(methods=..., engine="multilca")` to validate the
114
+ optimized calculation with composite Brightway MultiLCA functional units.
115
+ Neither engine writes hourly foreground activities.
116
+
117
+ Intermediate canonical results and annual/monthly mapped tables remain
118
+ available on the class instance through `results(year)` and `table(year)`. Use
97
119
  `mapping_report(year)` to inspect any historical technologies assigned to a
98
120
  country-specific high-voltage fallback activity.
99
121
 
@@ -101,6 +123,7 @@ country-specific high-voltage fallback activity.
101
123
  ## Contributing
102
124
 
103
125
  Please take a look at the [DEVELOPPING.md](https://git.list.lu/shrecc_project/shrecc/-/blob/main/DEVELOPPING.md) file for details on how to contribute code to the repository.
126
+ Notably for the versioning.
104
127
 
105
128
  ## License
106
129
 
@@ -79,20 +79,18 @@ notebooks = [
79
79
  "matplotlib>=3.10.9",
80
80
  ]
81
81
  docs = [
82
- "furo==2024.1.29",
83
82
  "myst_parser==2.0.0",
84
83
  "sphinx==7.2.6",
85
84
  "sphinx-autobuild>=2024.10.3",
86
85
  "ipython>=8.37.0",
87
86
  "sphinx-rtd-theme>=3.0.2",
88
- "myst-parser>=2.0.0",
89
87
  "sphinx-design>=0.6.1",
90
88
  "sphinx-copybutton>=0.5.2",
91
89
  "sphinx-autoapi>=3.5.0",
92
90
  "astroid<4",
93
91
  "sphinx-notfound-page>=1.1.0",
94
92
  "myst-nb>=1.2.0",
95
- ]
93
+ ]
96
94
 
97
95
  [tool.setuptools]
98
96
  include-package-data = true
@@ -8,6 +8,7 @@
8
8
  __all__ = (
9
9
  "__version__",
10
10
  "NewDatabase",
11
+ "LCIAResults",
11
12
  "create_database",
12
13
  "filt_cutoff",
13
14
  "get_data",
@@ -15,6 +16,7 @@ __all__ = (
15
16
  )
16
17
  from .database import create_database, filt_cutoff
17
18
  from .energy_charts import data_processing, get_data
19
+ from .lcia import LCIAResults
18
20
  from .pipeline import NewDatabase
19
21
 
20
- __version__ = "0.1.0.dev3"
22
+ __version__ = "0.1.2.dev1"
@@ -13,6 +13,7 @@ from shrecc.mapping import (
13
13
  map_consumption_mix_to_ecoinvent_activities,
14
14
  mapping_gap_to_report,
15
15
  validate_consumption_profile,
16
+ validate_inventory_resolution,
16
17
  )
17
18
 
18
19
  __all__ = (
@@ -25,4 +26,5 @@ __all__ = (
25
26
  "map_consumption_mix_to_ecoinvent_activities",
26
27
  "mapping_gap_to_report",
27
28
  "validate_consumption_profile",
29
+ "validate_inventory_resolution",
28
30
  )
@@ -176,7 +176,8 @@ def compare_electricity_generation_mixes(
176
176
  Args:
177
177
  foreground_databases: Mapping from display label to ``{year:
178
178
  database_name}`` mappings. Foreground databases are expected to
179
- contain activities named ``"Electricity mix in {country}, {year}"``.
179
+ contain activities named
180
+ ``"electricity, consumption mix, {year}"``.
180
181
  background_databases: Mapping from year to ecoinvent or premise
181
182
  background database name.
182
183
  countries: Consumer-country codes to compare.
@@ -396,7 +397,7 @@ def _comparison_roots(
396
397
  roots = {
397
398
  label: resolver.find_unique(
398
399
  databases[year],
399
- f"Electricity mix in {country}, {year}",
400
+ f"electricity, consumption mix, {year}",
400
401
  country,
401
402
  )
402
403
  for label, databases in foreground_databases.items()
@@ -2,6 +2,8 @@
2
2
  # Licensed under the MIT License (see LICENSE file for details).
3
3
  # Authors: [Sabina Bednářová, Thomas Gibon]
4
4
 
5
+ from collections import defaultdict
6
+ from dataclasses import dataclass
5
7
  from datetime import datetime
6
8
  from importlib.resources import files
7
9
  from packaging.version import parse as vparse
@@ -10,7 +12,6 @@ import re
10
12
 
11
13
  import bw2data as bd
12
14
  import pandas as pd
13
- from bw2data.query import Filter, Query
14
15
 
15
16
  from shrecc._legacy_database import (
16
17
  apply_mapping,
@@ -25,6 +26,7 @@ from shrecc.mapping import (
25
26
  activity_mix_to_database_table,
26
27
  load_ecoinvent_mapping as load_mapping_data,
27
28
  map_consumption_mix_to_ecoinvent_activities,
29
+ validate_inventory_resolution,
28
30
  )
29
31
  from shrecc.result_store import (
30
32
  MANIFEST_FILENAME,
@@ -34,6 +36,65 @@ from shrecc.result_store import (
34
36
 
35
37
  UNUSED_SOURCE = "Import balance (physical)"
36
38
  DEFAULT_ACTIVITY_FALLBACK_LOCATIONS = ("RER", "RoW", "GLO")
39
+ FOREGROUND_ACTIVITY_NAME = "electricity, consumption mix"
40
+ FOREGROUND_REFERENCE_PRODUCT = "electricity"
41
+ FOREGROUND_UNIT = "kilowatt hour"
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class BackgroundActivityIndex:
46
+ """Indexed Brightway activity keys for one background database."""
47
+
48
+ database_name: str
49
+ by_name_location_unit: dict
50
+ by_name_location: dict
51
+
52
+
53
+ def build_background_activity_index(eidb_name):
54
+ """Load and index one Brightway background database for fast matching."""
55
+ database = bd.Database(eidb_name)
56
+ by_name_location_unit = defaultdict(list)
57
+ by_name_location = defaultdict(list)
58
+ if hasattr(database, "nodes_to_dataframe"):
59
+ nodes = database.nodes_to_dataframe(
60
+ columns=["database", "code", "name", "location", "unit"],
61
+ return_sorted=False,
62
+ )
63
+ records = (
64
+ (
65
+ (row.database, row.code),
66
+ row.name,
67
+ row.location,
68
+ row.unit,
69
+ )
70
+ for row in nodes.itertuples(index=False)
71
+ )
72
+ else:
73
+ records = (
74
+ (
75
+ key,
76
+ activity.get("name"),
77
+ activity.get("location"),
78
+ activity.get("unit"),
79
+ )
80
+ for key, activity in database.load().items()
81
+ )
82
+
83
+ for key, name, location, unit in records:
84
+ if name is None or location is None:
85
+ continue
86
+ by_name_location[(name, location)].append(key)
87
+ if unit is not None:
88
+ by_name_location_unit[(name, location, unit)].append(key)
89
+ return BackgroundActivityIndex(
90
+ database_name=eidb_name,
91
+ by_name_location_unit={
92
+ key: tuple(values) for key, values in by_name_location_unit.items()
93
+ },
94
+ by_name_location={
95
+ key: tuple(values) for key, values in by_name_location.items()
96
+ },
97
+ )
37
98
 
38
99
 
39
100
  def filt_cutoff(
@@ -199,6 +260,7 @@ def map_known_inputs(
199
260
  strict=False,
200
261
  include_network=True,
201
262
  fallback_locations=DEFAULT_ACTIVITY_FALLBACK_LOCATIONS,
263
+ background_index=None,
202
264
  ):
203
265
  """
204
266
  Maps known inputs from the ecoinvent database to the filtered dataframe.
@@ -210,12 +272,19 @@ def map_known_inputs(
210
272
  include_network (bool): Also resolve the fixed electricity-network inputs.
211
273
  fallback_locations: Ordered fallback geographies tried when the requested
212
274
  activity name and unit do not exist at the requested geography.
275
+ background_index: Optional prebuilt index for ``eidb_name``. Reuse this
276
+ when resolving several tables against the same background database.
213
277
 
214
278
  Returns:
215
279
  dict: A dictionary mapping known inputs to their corresponding entries in the ecoinvent database.
216
280
  """
217
- ei_db = bd.Database(eidb_name)
218
- ei_db_data = ei_db.load()
281
+ if background_index is None:
282
+ background_index = build_background_activity_index(eidb_name)
283
+ if background_index.database_name != eidb_name:
284
+ raise ValueError(
285
+ "Background activity index belongs to "
286
+ f"{background_index.database_name!r}, not {eidb_name!r}"
287
+ )
219
288
  known_inputs = {}
220
289
  missing_inputs = []
221
290
  country_to_code = {
@@ -247,20 +316,19 @@ def map_known_inputs(
247
316
  for fallback_loc in fallback_locations or ()
248
317
  if fallback_loc != loc
249
318
  )
250
- results = []
319
+ results = ()
251
320
  resolved_loc = loc
252
321
  for candidate_loc in candidate_locations:
253
- q = Query()
254
- q.add(Filter("name", "is", name))
255
- q.add(Filter("location", "is", candidate_loc))
256
- q.add(Filter("unit", "is", "kilowatt hour"))
257
- results = q(ei_db_data)
322
+ results = background_index.by_name_location_unit.get(
323
+ (name, candidate_loc, "kilowatt hour"),
324
+ (),
325
+ )
258
326
  resolved_loc = candidate_loc
259
327
  if results:
260
328
  break
261
329
 
262
330
  if len(results) == 1:
263
- exchange = list(results).pop()
331
+ exchange = results[0]
264
332
  known_inputs[(original_loc, original_name, unit)] = exchange
265
333
  known_inputs[(loc, name, unit)] = exchange
266
334
  known_inputs[(resolved_loc, name, unit)] = exchange
@@ -278,14 +346,9 @@ def map_known_inputs(
278
346
  for act in network:
279
347
  loc = act["loc"]
280
348
  name = act["name"]
281
- q = Query()
282
- filter_name = Filter("name", "is", name)
283
- filter_loc = Filter("location", "is", loc)
284
- q.add(filter_name)
285
- q.add(filter_loc)
286
- results = q(ei_db_data)
349
+ results = background_index.by_name_location.get((name, loc), ())
287
350
  if len(results) == 1:
288
- known_inputs_network[(loc, name)] = list(results).pop()
351
+ known_inputs_network[(loc, name)] = results[0]
289
352
  else:
290
353
  print("Couldnt find activity:" + name)
291
354
  missing_inputs.append((loc, name, len(results)))
@@ -359,6 +422,71 @@ def get_network_activities(eidb_name):
359
422
  return network_act
360
423
 
361
424
 
425
+ def get_country_network_activities(eidb_name, country):
426
+ """Return the fixed network exchanges included for one consumer country."""
427
+ specific_network = {
428
+ "land": (
429
+ "market for transmission network, electricity, high voltage "
430
+ "direct current land cable"
431
+ ),
432
+ "subsea": (
433
+ "market for transmission network, electricity, high voltage "
434
+ "direct current subsea cable"
435
+ ),
436
+ "construction": (
437
+ "transmission network construction, electricity, high voltage"
438
+ ),
439
+ }
440
+ land_cable_countries = {
441
+ "AT",
442
+ "BE",
443
+ "DK",
444
+ "EE",
445
+ "ES",
446
+ "FI",
447
+ "FR",
448
+ "GR",
449
+ "HR",
450
+ "IT",
451
+ "LT",
452
+ "LV",
453
+ "ME",
454
+ "MT",
455
+ "NO",
456
+ "SE",
457
+ "TR",
458
+ "UK",
459
+ }
460
+ subsea_cable_countries = {
461
+ "DK",
462
+ "EE",
463
+ "ES",
464
+ "FI",
465
+ "FR",
466
+ "GR",
467
+ "HR",
468
+ "IT",
469
+ "LT",
470
+ "ME",
471
+ "MT",
472
+ "NO",
473
+ "PL",
474
+ "UK",
475
+ }
476
+ selected = []
477
+ for exchange in get_network_activities(eidb_name):
478
+ name = exchange["name"]
479
+ if name not in specific_network.values():
480
+ selected.append(exchange)
481
+ elif name == specific_network["land"] and country in land_cable_countries:
482
+ selected.append(exchange)
483
+ elif name == specific_network["subsea"] and country in subsea_cable_countries:
484
+ selected.append(exchange)
485
+ elif name == specific_network["construction"] and country == "CH":
486
+ selected.append(exchange)
487
+ return selected
488
+
489
+
362
490
  def create_activity_dict(
363
491
  dataframe_filt,
364
492
  known_inputs,
@@ -367,6 +495,7 @@ def create_activity_dict(
367
495
  eidb_name=None,
368
496
  year=None,
369
497
  consumption_profile=None,
498
+ inventory_resolution=None,
370
499
  ):
371
500
  """
372
501
  Creates a dictionary of activities for the BW database based on the filtered dataframe and known inputs.
@@ -383,24 +512,30 @@ def create_activity_dict(
383
512
  name is left unchanged to avoid repeating it.
384
513
  consumption_profile (str, optional): Temporal weighting represented by
385
514
  the foreground inventory, stored as activity metadata.
515
+ inventory_resolution (str, optional): ``"annual"`` or ``"monthly"``.
516
+ When omitted, time-indexed columns are treated as monthly and
517
+ country-only columns as annual for compatibility.
386
518
 
387
519
  Returns:
388
520
  dict: A dictionary containing activities to be written to the BW2 database.
389
521
  """
390
522
  activities = {}
391
523
  activity_year = int(year) if year is not None else None
524
+ resolution = (
525
+ validate_inventory_resolution(inventory_resolution)
526
+ if inventory_resolution is not None
527
+ else ("monthly" if dataframe_filt.columns.nlevels > 1 else "annual")
528
+ )
392
529
  for i, col in enumerate(dataframe_filt.columns):
393
530
  if dataframe_filt.columns.nlevels > 1:
394
531
  time, country = col
395
- activity = f"{time} Electricity mix"
396
- name = f"{activity} in {country}"
397
- name_contains_year = str(activity_year) in str(time)
532
+ period = _format_inventory_period(time, resolution)
398
533
  else:
399
534
  country = col
400
- name = f"Electricity mix in {country}"
401
- name_contains_year = False
402
- if activity_year is not None and not name_contains_year:
403
- name = f"{name}, {activity_year}"
535
+ period = str(activity_year) if activity_year is not None else None
536
+ name = FOREGROUND_ACTIVITY_NAME
537
+ if period is not None:
538
+ name = f"{name}, {period}"
404
539
  code = f"electricity {i}"
405
540
  bd_version = bd.__version__
406
541
  if not isinstance(bd_version, str):
@@ -414,11 +549,17 @@ def create_activity_dict(
414
549
  prod_exchange_type = bd.labels.production_edge_default
415
550
  act = {
416
551
  "name": name,
417
- "unit": "kWh",
552
+ "unit": FOREGROUND_UNIT,
418
553
  "code": code,
419
554
  "location": str(country),
420
- "reference product": "Electricity mix",
555
+ "reference product": FOREGROUND_REFERENCE_PRODUCT,
421
556
  "type": act_type,
557
+ "inventory_resolution": resolution,
558
+ "comment": _inventory_comment(
559
+ resolution,
560
+ period,
561
+ consumption_profile,
562
+ ),
422
563
  "exchanges": [],
423
564
  }
424
565
  if activity_year is not None:
@@ -431,7 +572,7 @@ def create_activity_dict(
431
572
  "input": (db_name, code),
432
573
  "name": name,
433
574
  "location": str(country),
434
- "unit": "kWh",
575
+ "unit": FOREGROUND_UNIT,
435
576
  "amount": 1,
436
577
  "type": prod_exchange_type,
437
578
  }
@@ -447,91 +588,16 @@ def create_activity_dict(
447
588
  "type": "technosphere",
448
589
  }
449
590
  act["exchanges"].append(new_exchange)
450
- network = get_network_activities(eidb_name or db_name)
451
- specific_network = [
452
- "market for transmission network, electricity, high voltage direct current land cable",
453
- "market for transmission network, electricity, high voltage direct current subsea cable",
454
- "transmission network construction, electricity, high voltage",
455
- ]
456
- land_cable = [
457
- "FR",
458
- "IT",
459
- "GR",
460
- "DK",
461
- "AT",
462
- "BE",
463
- "EE",
464
- "ES",
465
- "FI",
466
- "HR",
467
- "IT",
468
- "LT",
469
- "LV",
470
- "ME",
471
- "MT",
472
- "NO",
473
- "SE",
474
- "TR",
475
- "UK",
476
- ]
477
- subsea_cable = [
478
- "FR",
479
- "IT",
480
- "GR",
481
- "DK",
482
- "EE",
483
- "ES",
484
- "FI",
485
- "HR",
486
- "IT",
487
- "LT",
488
- "ME",
489
- "MT",
490
- "NO",
491
- "PL",
492
- "UK",
493
- ]
494
- for exch_net in network:
495
- if exch_net["name"] not in specific_network:
496
- exchange = known_inputs_network.get((exch_net["loc"], exch_net["name"]))
497
- if exchange:
498
- new_exchange = {
591
+ for exch_net in get_country_network_activities(eidb_name or db_name, country):
592
+ exchange = known_inputs_network.get((exch_net["loc"], exch_net["name"]))
593
+ if exchange:
594
+ act["exchanges"].append(
595
+ {
499
596
  "input": exchange,
500
597
  "amount": float(exch_net["val"]),
501
598
  "type": "technosphere",
502
599
  }
503
- act["exchanges"].append(new_exchange)
504
- if (
505
- country in land_cable and exch_net["name"] == specific_network[0]
506
- ): # writing the land cable
507
- exchange = known_inputs_network.get((exch_net["loc"], exch_net["name"]))
508
- if exchange:
509
- new_exchange = {
510
- "input": exchange,
511
- "amount": float(exch_net["val"]),
512
- "type": "technosphere",
513
- }
514
- act["exchanges"].append(new_exchange)
515
- if (
516
- country in subsea_cable and exch_net["name"] == specific_network[1]
517
- ): # writing the subsea cable
518
- exchange = known_inputs_network.get((exch_net["loc"], exch_net["name"]))
519
- if exchange:
520
- new_exchange = {
521
- "input": exchange,
522
- "amount": float(exch_net["val"]),
523
- "type": "technosphere",
524
- }
525
- act["exchanges"].append(new_exchange)
526
- if country == "CH" and exch_net["name"] == specific_network[2]:
527
- exchange = known_inputs_network.get((exch_net["loc"], exch_net["name"]))
528
- if exchange:
529
- new_exchange = {
530
- "input": exchange,
531
- "amount": float(exch_net["val"]),
532
- "type": "technosphere",
533
- }
534
- act["exchanges"].append(new_exchange)
600
+ )
535
601
  activities[(db_name, code)] = act
536
602
  return activities
537
603
 
@@ -545,6 +611,7 @@ def create_database(
545
611
  strict=False,
546
612
  year=None,
547
613
  consumption_profile=None,
614
+ inventory_resolution=None,
548
615
  ):
549
616
  """
550
617
  Creates an "ecoinvent-like" BW database based on a previously filtered dataframe.
@@ -563,6 +630,8 @@ def create_database(
563
630
  and stored as activity metadata.
564
631
  consumption_profile (str, optional): Temporal weighting stored as
565
632
  foreground activity metadata.
633
+ inventory_resolution (str, optional): Temporal resolution stored in
634
+ activity names, metadata, and comments.
566
635
 
567
636
  Returns:
568
637
  None
@@ -585,6 +654,32 @@ def create_database(
585
654
  eidb_name=eidb_name,
586
655
  year=year,
587
656
  consumption_profile=consumption_profile,
657
+ inventory_resolution=inventory_resolution,
588
658
  )
589
659
  elec_db = setup_database(project_name, db_name)
590
660
  elec_db.write(activities)
661
+
662
+
663
+ def _format_inventory_period(time, inventory_resolution):
664
+ """Format a table-column timestamp for an activity name."""
665
+ timestamp = pd.Timestamp(time)
666
+ if inventory_resolution == "monthly":
667
+ return timestamp.strftime("%Y-%m")
668
+ return str(timestamp.year)
669
+
670
+
671
+ def _inventory_comment(
672
+ inventory_resolution,
673
+ period,
674
+ consumption_profile,
675
+ ):
676
+ """Build ActivityBrowser documentation for a foreground inventory."""
677
+ parts = [
678
+ "SHRECC electricity consumption mix.",
679
+ f"Inventory resolution: {inventory_resolution}.",
680
+ ]
681
+ if period is not None:
682
+ parts.append(f"Inventory period: {period}.")
683
+ if consumption_profile is not None:
684
+ parts.append(f"Consumption profile: {consumption_profile}.")
685
+ return " ".join(parts)