shrecc 0.0.4__tar.gz → 0.0.5.dev2__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.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright © 2025 Luxembourg Institute of Science and Technology
3
+ Copyright © 2024,2025,2026 Luxembourg Institute of Science and Technology
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shrecc
3
- Version: 0.0.4
3
+ Version: 0.0.5.dev2
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>
@@ -16,4 +16,4 @@ from .database import create_database, filt_cutoff
16
16
  from .download import get_data
17
17
  from .treatment import data_processing
18
18
 
19
- __version__ = "0.0.4"
19
+ __version__ = "0.0.5.dev2"
@@ -1,9 +1,10 @@
1
- # Copyright © 2024 Luxembourg Institute of Science and Technology
1
+ # Copyright © 2024,2025,2026 Luxembourg Institute of Science and Technology
2
2
  # Licensed under the MIT License (see LICENSE file for details).
3
3
  # Authors: [Sabina Bednářová, Thomas Gibon]
4
4
 
5
5
  from datetime import datetime
6
6
  from importlib.resources import files
7
+ from packaging.version import parse as vparse
7
8
  from pathlib import Path
8
9
  import re
9
10
 
@@ -14,10 +15,12 @@ from bw2data.query import Filter, Query
14
15
 
15
16
  from shrecc.treatment import load_from_pickle, save_to_pickle
16
17
 
18
+ UNUSED_SOURCE = "Import balance (physical)"
19
+
17
20
 
18
21
  def filt_cutoff(
19
22
  countries,
20
- times=0,
23
+ times=[],
21
24
  general_range=0,
22
25
  refined_range=0,
23
26
  freq=0,
@@ -56,8 +59,9 @@ def filt_cutoff(
56
59
 
57
60
  if general_range:
58
61
  year = datetime.strptime(general_range[0], "%Y-%m-%d %H:%M:%S").year
59
- elif times:
60
- year = datetime.strptime(times[0], "%Y-%m-%d %H:%M:%S").year
62
+ elif len(times):
63
+ times = pd.to_datetime(times)
64
+ year = times[0].year
61
65
  else:
62
66
  raise ValueError("Either `times` or `general_range` must be provided")
63
67
 
@@ -67,7 +71,9 @@ def filt_cutoff(
67
71
  dataframe = dataframe.droplevel("source", axis=1)
68
72
  dataframe = filter_by_countries(dataframe, countries)
69
73
 
70
- if times:
74
+ if len(times):
75
+ # For backwards compatibility and making sure datetime is used in the filtering
76
+ times = pd.to_datetime(times)
71
77
  dataframe = filter_by_times(dataframe, times)
72
78
  if general_range:
73
79
  dataframe = filter_by_range(dataframe, general_range, refined_range, freq)
@@ -142,11 +148,13 @@ def prepare_consumption_data(Z_cons):
142
148
  pd.DataFrame: The prepared consumption data, with the trade data removed and indices swapped.
143
149
  """
144
150
  Z_cons = Z_cons.sort_index()
151
+
145
152
  if "trade" in Z_cons.index.get_level_values("source"):
146
153
  Z_cons_to_multiply = Z_cons.drop("trade", axis=0).copy()
147
154
  else:
148
155
  Z_cons_to_multiply = Z_cons.copy()
149
156
  Z_cons_to_multiply.index.names = ["source", "geography_mix"]
157
+
150
158
  return Z_cons_to_multiply.swaplevel()
151
159
 
152
160
 
@@ -390,11 +398,13 @@ def map_known_inputs(eidb_name, dataframe_filt):
390
398
  # Only UK seems concerned but consider using a dictionary
391
399
  if loc == "UK":
392
400
  loc = "GB"
401
+
393
402
  # For ecoinvent > 3.10, the activity names have changed
394
403
  # They now use a country code, instead of a full name
395
404
  # We deal with them here:
396
405
  def repl(match):
397
406
  return f"from {country_to_code[match.group(1)]}"
407
+
398
408
  if any(v in eidb_name for v in ("3.11", "3.12")):
399
409
  pattern = re.compile(r"from (Germany|France)")
400
410
  name = pattern.sub(repl, name)
@@ -497,15 +507,36 @@ def create_activity_dict(dataframe_filt, known_inputs, known_inputs_network, db_
497
507
  country = col
498
508
  name = f"Electricity mix in {country}"
499
509
  code = f"electricity {i}"
510
+ bd_version = bd.__version__
511
+ if not isinstance(bd_version, str):
512
+ bd_version = ".".join(map(str, bd_version))
513
+ BW2 = vparse(bd_version) < vparse("4")
514
+ if BW2:
515
+ act_type = "process"
516
+ prod_exchange_type = "production"
517
+ else:
518
+ act_type = bd.labels.process_node_default
519
+ prod_exchange_type = bd.labels.production_edge_default
500
520
  act = {
501
521
  "name": name,
502
522
  "unit": "kWh",
503
523
  "code": code,
504
524
  "location": str(country),
505
525
  "reference product": "Electricity mix",
506
- "type": "process",
526
+ "type": act_type,
507
527
  "exchanges": [],
508
528
  }
529
+ # Add the production exchange
530
+ act["exchanges"].append(
531
+ {
532
+ "input": (db_name, code),
533
+ "name": name,
534
+ "location": str(country),
535
+ "unit": "kWh",
536
+ "amount": 1,
537
+ "type": prod_exchange_type,
538
+ }
539
+ )
509
540
  for idx in dataframe_filt.index:
510
541
  source, exch_name, prod, unit = idx
511
542
  if float(dataframe_filt.loc[idx, col]) != 0:
@@ -75,6 +75,7 @@ def get_prod(start, end, country, cumul=False, rolling=False):
75
75
  def get_trade(start, end, country):
76
76
  """
77
77
  Downloads trade data from the Energy Charts API. Gets called from `get_data()`.
78
+ Uses the "cbpf" endpoint of the API.
78
79
 
79
80
  Args:
80
81
  start (int): Start of the download period (output of `year_to_unix()`) in unix seconds.
@@ -295,6 +296,7 @@ def cleaning_data(data, data_dir):
295
296
  scale_dict = {"production mix": 1, "trade": 1000, "load": 1}
296
297
 
297
298
  for country in data.keys():
299
+ print(f"Processing country: {country}")
298
300
  data_clean[country.upper()] = {}
299
301
  for k, v in data[country].items():
300
302
  if type(v) is pd.DataFrame: # axis = 1 will soon be depreciated
@@ -1,4 +1,4 @@
1
- # Copyright © 2024 Luxembourg Institute of Science and Technology
1
+ # Copyright © 2024,2025,2026 Luxembourg Institute of Science and Technology
2
2
  # Licensed under the MIT License (see LICENSE file for details).
3
3
  # Authors: [Sabina Bednářová, Thomas Gibon]
4
4
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shrecc
3
- Version: 0.0.4
3
+ Version: 0.0.5.dev2
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>
@@ -1,6 +1,8 @@
1
1
  import pytest
2
2
  import pandas as pd
3
+ import bw2data as bd
3
4
  import numpy as np
5
+ from packaging.version import parse as vparse
4
6
  from pytest import approx
5
7
  from unittest.mock import patch, MagicMock
6
8
  import types
@@ -913,11 +915,55 @@ def test_filter_by_times_no_matching_times():
913
915
  assert result.shape[1] == 0
914
916
 
915
917
 
918
+ @pytest.mark.parametrize(
919
+ "times_input",
920
+ [
921
+ pytest.param(
922
+ ["2023-06-01 08:00:00", "2023-06-01 10:00:00"],
923
+ id="list_of_strings",
924
+ ),
925
+ pytest.param(
926
+ pd.to_datetime(["2023-06-01 08:00:00", "2023-06-01 10:00:00"]),
927
+ id="datetime_index",
928
+ ),
929
+ ],
930
+ )
931
+ def test_filter_by_times_accepts_strings_and_datetimeindex(times_input):
932
+ """filter_by_times should accept either a list of strings or a pd.DatetimeIndex
933
+ and produce the same filtered DataFrame, regardless of whether the columns'
934
+ `time` level is stored as strings or as datetime objects.
935
+ """
936
+ time_values = pd.to_datetime(
937
+ [
938
+ "2023-06-01 08:00:00",
939
+ "2023-06-01 09:00:00",
940
+ "2023-06-01 10:00:00",
941
+ "2023-06-01 11:00:00",
942
+ ]
943
+ )
944
+ countries = ["FR", "DE", "IT", "FR"]
945
+ columns = pd.MultiIndex.from_arrays(
946
+ [time_values, countries], names=["time", "country"]
947
+ )
948
+ index = pd.MultiIndex.from_tuples(
949
+ [("FR", "tech1", "electricity, high voltage", "kWh")],
950
+ names=["geography", "activityName", "prod", "unit"],
951
+ )
952
+ df = pd.DataFrame([[1, 2, 3, 4]], index=index, columns=columns)
953
+
954
+ result = filter_by_times(df, times_input)
955
+
956
+ expected_times = pd.to_datetime(["2023-06-01 08:00:00", "2023-06-01 10:00:00"])
957
+ assert isinstance(result, pd.DataFrame)
958
+ assert result.shape == (1, 2)
959
+ assert set(result.columns.get_level_values("time")) == set(expected_times)
960
+ assert list(result.values.flatten()) == [1, 3]
961
+
962
+
916
963
  # ────────────────────────────────────────────────────────────────
917
964
  # Tests for: filter_by_range() — requires multiple sub-tests
918
965
  # ────────────────────────────────────────────────────────────────
919
966
  def test_filter_by_range_basic():
920
-
921
967
  time_range = pd.date_range(
922
968
  start="2023-06-01 00:00:00", end="2023-08-31 23:00:00", freq="h"
923
969
  )
@@ -1185,7 +1231,6 @@ def dummy_dataframe_for_filt_cutoff():
1185
1231
  def test_filt_cutoff_filtering_by_times(
1186
1232
  mock_df_tech_mapping, dummy_dataframe_for_filt_cutoff
1187
1233
  ):
1188
-
1189
1234
  df = dummy_dataframe_for_filt_cutoff
1190
1235
  mock_df_tech_mapping.return_value = df
1191
1236
 
@@ -1222,7 +1267,6 @@ def test_filt_cutoff_filtering_by_times(
1222
1267
  def test_filt_cutoff_filtering_by_range(
1223
1268
  mock_df_tech_mapping, dummy_dataframe_for_filt_cutoff
1224
1269
  ):
1225
-
1226
1270
  df = dummy_dataframe_for_filt_cutoff
1227
1271
  mock_df_tech_mapping.return_value = df
1228
1272
 
@@ -1338,7 +1382,6 @@ def test_setup_database_handles_multiple_calls(monkeypatch):
1338
1382
  # Tests for: tech_mapping() — requires multiple sub-tests
1339
1383
  # ──────────────────────────────────────────────────────────
1340
1384
  def test_get_network_activities_basic():
1341
-
1342
1385
  eidb_name = "ecoinvent-3.8-cutoff"
1343
1386
  result = get_network_activities(eidb_name)
1344
1387
  assert isinstance(result, list)
@@ -1377,7 +1420,6 @@ def test_get_network_activities_basic():
1377
1420
 
1378
1421
 
1379
1422
  def test_get_network_activities_311():
1380
-
1381
1423
  eidb_name = "ecoinvent-3.11-cutoff"
1382
1424
  result = get_network_activities(eidb_name)
1383
1425
  assert isinstance(result, list)
@@ -1388,7 +1430,6 @@ def test_get_network_activities_311():
1388
1430
 
1389
1431
 
1390
1432
  def test_get_network_activities_other_versions():
1391
-
1392
1433
  # Should not match "3.11" if not present
1393
1434
  for eidb_name in ["ecoinvent 3.7", "ei3.8.1", "eco3.9", ""]:
1394
1435
  result = get_network_activities(eidb_name)
@@ -1671,9 +1712,19 @@ def test_create_activity_dict_basic(
1671
1712
  (db_name, "electricity 1"),
1672
1713
  }
1673
1714
  # Check structure of activity dict
1715
+ bd_version = bd.__version__
1716
+ if not isinstance(bd_version, str):
1717
+ bd_version = ".".join(map(str, bd_version))
1718
+ BW2 = vparse(bd_version) < vparse("4")
1719
+ if BW2:
1720
+ act_type = "process"
1721
+ prod_exchange_type = "production"
1722
+ else:
1723
+ act_type = bd.labels.process_node_default
1724
+ prod_exchange_type = bd.labels.production_edge_default
1674
1725
  for act in activities.values():
1675
1726
  assert act["unit"] == "kWh"
1676
- assert act["type"] == "process"
1727
+ assert act["type"] == act_type
1677
1728
  assert isinstance(act["exchanges"], list)
1678
1729
  # Should have at least one exchange (the technology)
1679
1730
  assert any(e["type"] == "technosphere" for e in act["exchanges"])
@@ -1686,7 +1737,9 @@ def test_create_activity_dict_basic(
1686
1737
  # Check that the correct technology exchange is present for each activity
1687
1738
  # act0 is for ("2023-06-01 08:00:00", "FR")
1688
1739
  act0 = activities[(db_name, "electricity 0")]
1689
- inputs0 = [e["input"] for e in act0["exchanges"]]
1740
+ inputs0 = [
1741
+ e["input"] for e in act0["exchanges"] if e["type"] != prod_exchange_type
1742
+ ]
1690
1743
  assert set(inputs0) == {
1691
1744
  "fr_hydro_ei",
1692
1745
  "net_trans_subsea",
@@ -1703,7 +1756,7 @@ def test_create_activity_dict_basic(
1703
1756
  assert tech_ex1[0]["amount"] == 5.5 # specific network
1704
1757
  # act1 is for ("2023-06-01 09:00:00", "DE")
1705
1758
  act1 = activities[(db_name, "electricity 1")]
1706
- inputs1 = [e["input"] for e in act1["exchanges"]]
1759
+ inputs1 = [e["input"] for e in act1["exchanges"] if e["type"] != prod_exchange_type]
1707
1760
  assert set(inputs1) == {"de_wind_ei", "net_dist_lv", "net_trans_mv"}
1708
1761
  assert "net_trans_subsea" not in inputs1
1709
1762
  assert "net_trans_ch" not in inputs1
@@ -1716,6 +1769,14 @@ def test_create_activity_dict_basic(
1716
1769
  def test_create_activity_dict_no_network_exchanges(
1717
1770
  mock_get_network_activities, sample_dataframe_filt, known_inputs_fixture
1718
1771
  ):
1772
+ bd_version = bd.__version__
1773
+ if not isinstance(bd_version, str):
1774
+ bd_version = ".".join(map(str, bd_version))
1775
+ BW2 = vparse(bd_version) < vparse("4")
1776
+ if BW2:
1777
+ prod_exchange_type = "production"
1778
+ else:
1779
+ prod_exchange_type = bd.labels.production_edge_default
1719
1780
  # No known_inputs_network, so only technology exchanges
1720
1781
  mock_get_network_activities.return_value = []
1721
1782
  db_name = "test_db"
@@ -1727,8 +1788,11 @@ def test_create_activity_dict_no_network_exchanges(
1727
1788
  )
1728
1789
  for act in activities.values():
1729
1790
  # Only technology exchanges should be present
1791
+ technospere_exchanges = [
1792
+ e for e in act["exchanges"] if e["type"] != prod_exchange_type
1793
+ ]
1730
1794
  assert all(
1731
- e["input"] in known_inputs_fixture.values() for e in act["exchanges"]
1795
+ e["input"] in known_inputs_fixture.values() for e in technospere_exchanges
1732
1796
  )
1733
1797
 
1734
1798
 
File without changes
File without changes
File without changes
File without changes