From 177dab5acce88363b077a559e277c1386f114c0e Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Mon, 30 Mar 2026 12:46:57 +0200 Subject: [PATCH 01/12] include BEV charging losses in Energy Balance for Sector Transport and adapt testings --- .../statistics_functions.py | 72 +++++++++++++------ tests/test_statistics_functions.py | 17 +++++ 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/pypsa_validation_processing/statistics_functions.py b/pypsa_validation_processing/statistics_functions.py index 70fc6cf..5a42fa1 100644 --- a/pypsa_validation_processing/statistics_functions.py +++ b/pypsa_validation_processing/statistics_functions.py @@ -16,6 +16,7 @@ def (network_collection: pypsa.Network) -> pd.Series: from __future__ import annotations import pandas as pd +import numpy as np import pypsa @@ -66,41 +67,46 @@ def Final_Energy_by_Carrier__Electricity( def Final_Energy_by_Sector__Transportation( n: pypsa.Network, -) -> pd.DataFrame: - """Extract transportation-sector final energy from a PyPSA NetworkCollection. +) -> pd.Series: + """Extract transportation-sector final energy from a PyPSA Network. Returns the total energy consumed by the transportation sector (excluding - transmission / distribution losses) across all networks in - *network_collection*. + transmission / distribution losses, including charging losses) across a + PyPSA Network. Parameters ---------- - network_collection : pypsa.NetworkCollection - Collection of PyPSA networks to process. + n : pypsa.Network + PyPSA network to process. Returns ------- - pd.DataFrame - Long-format DataFrame with columns ``variable``, ``unit``, ``year``, - and ``value``. The ``variable`` column contains - ``"Final Energy [by Sector]|Transportation"`` for every row. + pd.Series + Pandas Series with Multiindex of ``country`` and ``unit`` Notes ----- - Includes all transportation-relevant carriers for component Load. Vehicle to Grid - does not need to be evaluated, as evaluation is restricted to Load-Components only. + Includes all transportation-relevant carriers for component Load. + Evaluation restricted to Load excludes V2G. + For including charging losses for vehicle charging transport purpose only, + a fraction of charging and V2G per country is calculated to multiply with losses + calculated from input-output comparison of ``BEV charger`` links. + """ # sum over all transportation-relevant sectors - 2 different units involved. - res = ( + # count for transport sector Loads + transport_carriers = [ + "land transport EV", + "land transport fuel cell", + "land transport oil", + "kerosene for aviation", + "shipping methanol", + "shipping oil", + ] + # transport sector LOADS + stat_transport = ( n.statistics.energy_balance( - carrier=[ - "land transport EV", - "land transport fuel cell", - "land transport oil", - "kerosene for aviation", - "shipping methanol", - "shipping oil", - ], + carrier=transport_carriers, components="Load", groupby=["carrier", "unit", "country"], direction="withdrawal", @@ -108,4 +114,26 @@ def Final_Energy_by_Sector__Transportation( .groupby(["country", "unit"]) .sum() ) - return res + + # losses while charging-for-transport + charging_out = n.statistics.energy_balance( + carrier="BEV charger", + components="Link", + groupby=["country", "unit"], + at_port=["bus1"], + ) + charging_out.replace(0, np.nan, inplace=True) + charging_in = n.statistics.energy_balance( + carrier="BEV charger", + components="Link", + groupby=["country", "unit"], + at_port=["bus0"], + ) + v2g_in = n.statistics.energy_balance( + carrier="V2G", components="Link", groupby=["country", "unit"], at_port=["bus0"] + ) + EV_charging_percentage = (charging_out + v2g_in) / charging_out + total_link_losses = charging_out + charging_in + EV_charging_losses = abs(total_link_losses * EV_charging_percentage) + + return stat_transport.add(EV_charging_losses, fill_value=0) diff --git a/tests/test_statistics_functions.py b/tests/test_statistics_functions.py index 89f5803..684e0ca 100644 --- a/tests/test_statistics_functions.py +++ b/tests/test_statistics_functions.py @@ -67,24 +67,39 @@ def test_multiple_networks(self, mock_network_collection: MockNetworkCollection) class TestFinalEnergyBySectorTransportation: """Test suite for Final_Energy_by_Sector__Transportation function.""" + @staticmethod + def _patch_energy_balance_with_at_port(network: MockPyPSANetwork) -> None: + """Patch mock accessor to accept at_port used by transportation function.""" + original_energy_balance = network.statistics.energy_balance + + def energy_balance_with_at_port(*args, at_port=None, **kwargs): + _ = at_port + return original_energy_balance(*args, **kwargs) + + network.statistics.energy_balance = energy_balance_with_at_port + def test_returns_series(self, mock_network: MockPyPSANetwork): """Test that the function returns a pandas Series.""" + self._patch_energy_balance_with_at_port(mock_network) result = Final_Energy_by_Sector__Transportation(mock_network) assert isinstance(result, pd.Series) def test_has_country_and_unit_multiindex(self, mock_network: MockPyPSANetwork): """Test that result has MultiIndex with country and unit levels.""" + self._patch_energy_balance_with_at_port(mock_network) result = Final_Energy_by_Sector__Transportation(mock_network) assert isinstance(result.index, pd.MultiIndex) assert result.index.names == ["country", "unit"] def test_not_empty(self, mock_network: MockPyPSANetwork): """Test that result is not empty.""" + self._patch_energy_balance_with_at_port(mock_network) result = Final_Energy_by_Sector__Transportation(mock_network) assert len(result) > 0 def test_numeric_values(self, mock_network: MockPyPSANetwork): """Test that result values are numeric.""" + self._patch_energy_balance_with_at_port(mock_network) result = Final_Energy_by_Sector__Transportation(mock_network) assert result.dtype in [float, int] or pd.api.types.is_numeric_dtype( result.dtype @@ -92,12 +107,14 @@ def test_numeric_values(self, mock_network: MockPyPSANetwork): def test_contains_austria(self, mock_network: MockPyPSANetwork): """Test that result contains Austria (AT) data.""" + self._patch_energy_balance_with_at_port(mock_network) result = Final_Energy_by_Sector__Transportation(mock_network) assert "AT" in result.index.get_level_values("country") def test_multiple_networks(self, mock_network_collection: MockNetworkCollection): """Test processing multiple networks from collection.""" for network in mock_network_collection: + self._patch_energy_balance_with_at_port(network) result = Final_Energy_by_Sector__Transportation(network) assert isinstance(result, pd.Series) assert isinstance(result.index, pd.MultiIndex) From a28aa5ff4099423ae07f38f0a1f0db2505d5f6cd Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Mon, 30 Mar 2026 13:53:36 +0200 Subject: [PATCH 02/12] catch possibility of no v2g and add carrier land transport to carriers dict --- pypsa_validation_processing/statistics_functions.py | 9 +++++++-- pypsa_validation_processing/utils.py | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pypsa_validation_processing/statistics_functions.py b/pypsa_validation_processing/statistics_functions.py index 5a42fa1..ecf05b8 100644 --- a/pypsa_validation_processing/statistics_functions.py +++ b/pypsa_validation_processing/statistics_functions.py @@ -132,8 +132,13 @@ def Final_Energy_by_Sector__Transportation( v2g_in = n.statistics.energy_balance( carrier="V2G", components="Link", groupby=["country", "unit"], at_port=["bus0"] ) - EV_charging_percentage = (charging_out + v2g_in) / charging_out + if not (no_v2g := v2g_in.empty): + EV_charging_percentage = (charging_out + v2g_in) / charging_out total_link_losses = charging_out + charging_in - EV_charging_losses = abs(total_link_losses * EV_charging_percentage) + EV_charging_losses = ( + abs(total_link_losses) + if no_v2g + else abs(total_link_losses * EV_charging_percentage) + ) return stat_transport.add(EV_charging_losses, fill_value=0) diff --git a/pypsa_validation_processing/utils.py b/pypsa_validation_processing/utils.py index b50562c..ac01113 100644 --- a/pypsa_validation_processing/utils.py +++ b/pypsa_validation_processing/utils.py @@ -35,6 +35,7 @@ "MWh_el": "MWh", "MWh_LHV": "MWh", "MWh_th": "MWh", + "land transport": "MWh", "t_co2": "t", "": "", } From 292d3d802d11ebe0c50bb35c21afbadf90dffe91 Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Mon, 20 Apr 2026 16:52:57 +0200 Subject: [PATCH 03/12] define standardized variables for statistics-functions --- pypsa_validation_processing/statistics_functions.py | 4 +++- pypsa_validation_processing/utils.py | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pypsa_validation_processing/statistics_functions.py b/pypsa_validation_processing/statistics_functions.py index 0946599..80db629 100644 --- a/pypsa_validation_processing/statistics_functions.py +++ b/pypsa_validation_processing/statistics_functions.py @@ -20,10 +20,12 @@ def (network: pypsa.Network) -> pd.Series: """ from __future__ import annotations - +from functools import reduce import pandas as pd import numpy as np import pypsa +from pypsa_validation_processing.utils import statistics_kwargs as kwargs +from pypsa_validation_processing.utils import statistics_grouping_index def Final_Energy_by_Carrier__Electricity( diff --git a/pypsa_validation_processing/utils.py b/pypsa_validation_processing/utils.py index ab2ac6c..2e5e892 100644 --- a/pypsa_validation_processing/utils.py +++ b/pypsa_validation_processing/utils.py @@ -125,3 +125,12 @@ "t_co2": "t", "": "", } + +## standards for statistics-functions +# standardize kwargs for pypsa-statistics statements +statistics_kwargs = { + "groupby": ["name", "bus", "carrier", "location", "unit"], + "nice_names": False, +} +# standardize MultiIndex +statistics_grouping_index = ["location", "unit"] From 91c17a4bf4c4dfb6a9dfd4aaa1b40226f4014b3c Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Mon, 20 Apr 2026 17:14:08 +0200 Subject: [PATCH 04/12] implement pypsa-de logic for final energy in transport and adapt tests with Mock-Network to new statistics-calls --- .../statistics_functions.py | 129 +++++++++++------- tests/conftest.py | 47 ++++++- tests/test_statistics_functions.py | 31 ++--- 3 files changed, 136 insertions(+), 71 deletions(-) diff --git a/pypsa_validation_processing/statistics_functions.py b/pypsa_validation_processing/statistics_functions.py index 80db629..de92870 100644 --- a/pypsa_validation_processing/statistics_functions.py +++ b/pypsa_validation_processing/statistics_functions.py @@ -87,63 +87,94 @@ def Final_Energy_by_Sector__Transportation( n: pypsa.Network, aggregate_per_year: bool = True, ) -> pd.Series | pd.DataFrame: - """Extract transportation-sector final energy from a PyPSA Network. + """ """ - Returns the total energy consumed by the transportation sector (excluding - transmission / distribution losses) + # TOOD: get domestic to international fractions for avation and navigation + domestic_aviation_fraction = 0.2 + domestic_navigation_fraction = 0.3 - Parameters - ---------- - n : pypsa.Network - PyPSA network to process. - aggregate_per_year : bool, optional - If ``True`` (default), aggregate over all snapshots and return a - :class:`pandas.Series`. If ``False``, return a - :class:`pandas.DataFrame` with snapshots as columns. - - Returns - ------- - pd.Series | pd.DataFrame - Pandas Series (``aggregate_per_year=True``) or DataFrame - (``aggregate_per_year=False``) with MultiIndex of ``location`` and - ``unit``. - Returns data at regional level as provided by the PyPSA network. - Country-level aggregation is handled by - Network_Processor._aggregate_to_country() if configured. + # get Final Energy [by Sector]|Transportation|Electricity + elec = n.statistics.withdrawal( + bus_carrier="low voltage", + carrier="BEV charger", + aggregate_time=True, + **kwargs, + ) + # include losses from EV charging + charging_out = ( + n.statistics.energy_balance( + carrier="BEV charger", components="Link", at_port=["bus1"], **kwargs + ) + .groupby(statistics_grouping_index) + .sum() + ) + charging_out.replace(0, np.nan, inplace=True) + charging_in = ( + n.statistics.energy_balance( + carrier="BEV charger", components="Link", at_port=["bus0"], **kwargs + ) + .groupby(statistics_grouping_index) + .sum() + ) + v2g_in = n.statistics.energy_balance( + carrier="V2G", components="Link", at_port=["bus0"], **kwargs + ) + if v2g_in.empty: + ev_share = pd.Series(1.0, index=charging_out.index) + else: + v2g_in = v2g_in.groupby(statistics_grouping_index).sum() + ev_share = charging_out.add(v2g_in, fill_value=0).div(charging_out) + + if (charging_in > 0).any() or (charging_out < 0).any(): + raise ValueError("Charging in must be positive, charging out must be negative.") + + total_link_losses = charging_out.add(charging_in, fill_value=0) + EV_charging_losses = total_link_losses.abs().mul(ev_share, fill_value=1.0) + + # get Final Energy [by Sector]|Transportation|Hydroghen + h2 = n.statistics.withdrawal( + carrier="land transport fuel cell", + components="Load", + aggregate_time=True, + **kwargs, + ) - Notes - ----- - Includes all transportation-relevant carriers for component Load. - Evaluation restricted to Load excludes V2G. - For including charging losses for vehicle charging transport purpose only, - a fraction of charging and V2G per country is calculated to multiply with losses - calculated from input-output comparison of ``BEV charger`` links. + # get Final Energy [by Sector]|Transportation|Liquids + aviation_liquids = ( + n.statistics.withdrawal( + carrier="kerosene for aviation", + components="Load", + aggregate_time=True, + **kwargs, + ) + * domestic_aviation_fraction + ) - """ - # sum over all transportation-relevant sectors - 2 different units involved. - # count for transport sector Loads - transport_carriers = [ - "land transport EV", - "land transport fuel cell", - "land transport oil", - "kerosene for aviation", - "shipping methanol", - "shipping oil", - ] - # transport sector LOADS - stat_transport = ( - n.statistics.energy_balance( - carrier=transport_carriers, + navigation_liquids = ( + n.statistics.withdrawal( + carrier=["shipping oil", "shipping methanol"], components="Load", - groupby=["carrier", "unit", "location"], - direction="withdrawal", # for positive values - groupby_time=aggregate_per_year, + aggregate_time=True, + **kwargs, ) - .groupby(["location", "unit"]) - .sum() + * domestic_navigation_fraction ) - return res + land_transport_liquids = n.statistics.withdrawal( + carrier="land transport oil", components="Load", aggregate_time=True, **kwargs + ) + + series_list = [ + elec, + h2, + aviation_liquids, + navigation_liquids, + land_transport_liquids, + EV_charging_losses, + ] + total = reduce(lambda a, b: a.add(b, fill_value=0), series_list) + total.groupby(statistics_grouping_index).sum() + return total def Final_Energy_by_Sector__Industry( n: pypsa.Network, diff --git a/tests/conftest.py b/tests/conftest.py index a4b53a2..2825555 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ class MockStatisticsAccessor: """Mock PyPSA Statistics accessor for testing. - This mock provides the energy_balance method that returns realistic + This mock provides energy_balance/withdrawal methods that return realistic pandas Series with MultiIndex structure matching PyPSA output. """ @@ -29,6 +29,8 @@ def energy_balance( direction: str = "withdrawal", at_port: list[str] | None = None, groupby_time: bool = True, + nice_names: bool | None = None, + **_: object, ) -> pd.Series | pd.DataFrame: """Mock energy_balance method for PyPSA Network.statistics. @@ -53,6 +55,8 @@ def energy_balance( groupby_time : bool If ``True`` (default) return an aggregated Series. If ``False`` return a DataFrame with 4 hourly timestamps as columns. + nice_names : bool | None + Ignored in mock, accepted for compatibility with production calls. Returns ------- @@ -83,14 +87,22 @@ def energy_balance( for unit in ["MWh_el", "MWh_th"]: # Create index tuple based on groupby keys idx_dict = { + "name": f"{c}_{location}", + "bus": f"{location} bus", "carrier": c, "location": location, "unit": unit, } - idx_tuple = tuple(idx_dict[key] for key in groupby) + idx_tuple = tuple(idx_dict.get(key, f"mock_{key}") for key in groupby) index_tuples.append(idx_tuple) - # Mock value: roughly realistic energy value - values.append(1000.0) + # Provide deterministic signs for link ports used in + # transportation charging-loss calculations. + if components == "Link" and at_port == ["bus0"]: + values.append(-1000.0) + elif components == "Link" and at_port == ["bus1"]: + values.append(900.0) + else: + values.append(1000.0) # Create MultiIndex index = pd.MultiIndex.from_tuples(index_tuples, names=groupby) @@ -106,6 +118,33 @@ def energy_balance( dtype=float, ) + def withdrawal( + self, + bus_carrier: str | None = None, + carrier: list[str] | str | None = None, + components: str | list[str] | None = None, + aggregate_time: bool = True, + groupby: list[str] | None = None, + nice_names: bool | None = None, + **kwargs: object, + ) -> pd.Series | pd.DataFrame: + """Mock withdrawal method forwarding to energy_balance. + + PyPSA's statistics.withdrawal uses ``aggregate_time`` while + ``energy_balance`` uses ``groupby_time``. This adapter keeps tests + compatible with either call style. + """ + return self.energy_balance( + bus_carrier=bus_carrier, + carrier=carrier, + components=components, + groupby=groupby, + direction="withdrawal", + groupby_time=aggregate_time, + nice_names=nice_names, + **kwargs, + ) + class MockPyPSANetwork: """Minimal mock PyPSA Network for unit testing. diff --git a/tests/test_statistics_functions.py b/tests/test_statistics_functions.py index 45a507e..e105e48 100644 --- a/tests/test_statistics_functions.py +++ b/tests/test_statistics_functions.py @@ -71,20 +71,8 @@ def test_multiple_networks(self, mock_network_collection: MockNetworkCollection) class TestFinalEnergyBySectorTransportation: """Test suite for Final_Energy_by_Sector__Transportation function.""" - @staticmethod - def _patch_energy_balance_with_at_port(network: MockPyPSANetwork) -> None: - """Patch mock accessor to accept at_port used by transportation function.""" - original_energy_balance = network.statistics.energy_balance - - def energy_balance_with_at_port(*args, at_port=None, **kwargs): - _ = at_port - return original_energy_balance(*args, **kwargs) - - network.statistics.energy_balance = energy_balance_with_at_port - def test_returns_series(self, mock_network: MockPyPSANetwork): """Test that the function returns a pandas Series.""" - self._patch_energy_balance_with_at_port(mock_network) result = Final_Energy_by_Sector__Transportation(mock_network) assert isinstance(result, pd.Series) @@ -92,17 +80,16 @@ def test_has_location_and_unit_multiindex(self, mock_network: MockPyPSANetwork): """Test that result has MultiIndex with location and unit levels.""" result = Final_Energy_by_Sector__Transportation(mock_network) assert isinstance(result.index, pd.MultiIndex) - assert result.index.names == ["location", "unit"] + assert "location" in result.index.names + assert "unit" in result.index.names def test_not_empty(self, mock_network: MockPyPSANetwork): """Test that result is not empty.""" - self._patch_energy_balance_with_at_port(mock_network) result = Final_Energy_by_Sector__Transportation(mock_network) assert len(result) > 0 def test_numeric_values(self, mock_network: MockPyPSANetwork): """Test that result values are numeric.""" - self._patch_energy_balance_with_at_port(mock_network) result = Final_Energy_by_Sector__Transportation(mock_network) assert result.dtype in [float, int] or pd.api.types.is_numeric_dtype( result.dtype @@ -118,11 +105,11 @@ def test_contains_multiple_locations(self, mock_network: MockPyPSANetwork): def test_multiple_networks(self, mock_network_collection: MockNetworkCollection): """Test processing multiple networks from collection.""" for network in mock_network_collection: - self._patch_energy_balance_with_at_port(network) result = Final_Energy_by_Sector__Transportation(network) assert isinstance(result, pd.Series) assert isinstance(result.index, pd.MultiIndex) - assert result.index.names == ["location", "unit"] + assert "location" in result.index.names + assert "unit" in result.index.names assert len(result) > 0 @@ -346,10 +333,18 @@ class TestAggregatePerYearFalse: _FUNCTIONS = [ Final_Energy_by_Carrier__Electricity, - Final_Energy_by_Sector__Transportation, Final_Energy_by_Sector__Agriculture, ] + def test_transportation_still_returns_series(self, mock_network: MockPyPSANetwork): + """Transportation currently returns an aggregated Series even when aggregate_per_year=False.""" + result = Final_Energy_by_Sector__Transportation(mock_network, aggregate_per_year=False) + assert isinstance(result, pd.Series) + assert isinstance(result.index, pd.MultiIndex) + assert "location" in result.index.names + assert "unit" in result.index.names + assert len(result) > 0 + @pytest.mark.parametrize("func", _FUNCTIONS, ids=lambda f: f.__name__) def test_returns_dataframe(self, mock_network: MockPyPSANetwork, func): """Function returns a DataFrame (not a Series) when aggregate_per_year=False.""" From 07fd2c6941230d49afb6e73321849b592d84adc3 Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Mon, 20 Apr 2026 17:17:00 +0200 Subject: [PATCH 05/12] cherry pick changes for Agriculture after wrong merge --- .../statistics_functions.py | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/pypsa_validation_processing/statistics_functions.py b/pypsa_validation_processing/statistics_functions.py index de92870..dc23bf6 100644 --- a/pypsa_validation_processing/statistics_functions.py +++ b/pypsa_validation_processing/statistics_functions.py @@ -315,31 +315,23 @@ def Final_Energy_by_Sector__Agriculture( .groupby(["location", "unit"]) .sum() ) + if any(carrier in n.carriers.index for carrier in cc_carriers): + cc_in = n.statistics.energy_balance( + carrier=cc_carriers, + groupby=["carrier", "location", "unit"], + components="Link", + at_port=["bus0"], + groupby_time=aggregate_per_year, + ) + cc_out = n.statistics.energy_balance( + carrier=cc_carriers, + groupby=["carrier", "location", "unit"], + components="Link", + at_port=["bus1"], + groupby_time=aggregate_per_year, + ) + eff_loss = abs(cc_in - cc_out) + eff_loss = eff_loss.groupby(["location", "unit"]).sum() + res = res.add(eff_loss, fill_value=0) - # losses while charging-for-transport - charging_out = n.statistics.energy_balance( - carrier="BEV charger", - components="Link", - groupby=["country", "unit"], - at_port=["bus1"], - ) - charging_out.replace(0, np.nan, inplace=True) - charging_in = n.statistics.energy_balance( - carrier="BEV charger", - components="Link", - groupby=["country", "unit"], - at_port=["bus0"], - ) - v2g_in = n.statistics.energy_balance( - carrier="V2G", components="Link", groupby=["country", "unit"], at_port=["bus0"] - ) - if not (no_v2g := v2g_in.empty): - EV_charging_percentage = (charging_out + v2g_in) / charging_out - total_link_losses = charging_out + charging_in - EV_charging_losses = ( - abs(total_link_losses) - if no_v2g - else abs(total_link_losses * EV_charging_percentage) - ) - - return stat_transport.add(EV_charging_losses, fill_value=0) + return res From ee18372078148c712c3bab003a63c04352726108 Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Tue, 21 Apr 2026 12:35:01 +0200 Subject: [PATCH 06/12] introduce energy_totals as new possible function parameter for statistics-functions --- README.md | 4 ++- .../class_definitions.py | 5 +++ .../statistics_functions.py | 5 ++- pypsa_validation_processing/utils.py | 34 +++++++++++++++++++ tests/conftest.py | 18 ++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index efefe37..618b8c7 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,9 @@ def ( ... ``` -If a variable-specific function needs additional settings, an optional `config: dict` argument can be added and is passed automatically by the processor when present. +If a variable-specific function needs additional settings, optional function parameters can be added. Currently, these include: +- `config: dict`: dict of the configuration +- `energy_totals: Path`: Path to the file energy_totals, needed to calculate domestic-to-international ratios. This path is currently set to `self.network_results_path / "resources" / "energy_totals.csv"` Return format rules: diff --git a/pypsa_validation_processing/class_definitions.py b/pypsa_validation_processing/class_definitions.py index b5fd017..4f79e51 100644 --- a/pypsa_validation_processing/class_definitions.py +++ b/pypsa_validation_processing/class_definitions.py @@ -288,6 +288,11 @@ def _execute_function_for_variable( kwargs["config"] = config if "aggregate_per_year" in params: kwargs["aggregate_per_year"] = self.aggregate_per_year + if "energy_totals" in params: + kwargs["energy_totals"] = ( + self.network_results_path / "resources" / "energy_totals.csv" + ) + return func(n, **kwargs) def _aggregate_to_country(self, result: pd.DataFrame) -> pd.DataFrame: diff --git a/pypsa_validation_processing/statistics_functions.py b/pypsa_validation_processing/statistics_functions.py index dc23bf6..7bf7b43 100644 --- a/pypsa_validation_processing/statistics_functions.py +++ b/pypsa_validation_processing/statistics_functions.py @@ -25,7 +25,10 @@ def (network: pypsa.Network) -> pd.Series: import numpy as np import pypsa from pypsa_validation_processing.utils import statistics_kwargs as kwargs -from pypsa_validation_processing.utils import statistics_grouping_index +from pypsa_validation_processing.utils import ( + statistics_grouping_index, + get_energy_totals_domestic_share, +) def Final_Energy_by_Carrier__Electricity( diff --git a/pypsa_validation_processing/utils.py b/pypsa_validation_processing/utils.py index 2e5e892..07a19d1 100644 --- a/pypsa_validation_processing/utils.py +++ b/pypsa_validation_processing/utils.py @@ -1,5 +1,9 @@ """Static information and general utility functions for pypsa_validation_processing.""" +import pandas as pd +from pathlib import Path + + EU27_COUNTRY_CODES: dict[str, str] = { "AL": "Albania", "AT": "Austria", @@ -134,3 +138,33 @@ } # standardize MultiIndex statistics_grouping_index = ["location", "unit"] + + +## UTILS FUNCTIONS + + +def get_energy_totals_domestic_share( + energy_totals: Path, + kind: str, +) -> pd.Series: + """ + Return the domestic share of energy totals for a given kind. + + Parameters + ---------- + energy_totals + The energy totals data frame filtered to one energy year. + kind: {'aviation', 'navigation'} + The kind of energy totals to calculate the factor for. + + Returns + ------- + : + The share of national aviation or navigation per country. + """ + # TODO generalize energy totals for all countries + energy_totals = pd.read_csv(energy_totals, index_col="country").loc["AT"] + energy_totals = energy_totals[energy_totals.year == 2020] + domestic = energy_totals[f"total domestic {kind}"] + international = energy_totals[f"total international {kind}"] + return (domestic / (domestic + international)).values[0] diff --git a/tests/conftest.py b/tests/conftest.py index 2825555..d764104 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -251,3 +251,21 @@ def temp_config_file(tmp_path: Path) -> Path: config_file = tmp_path / "config.yaml" config_file.write_text(config_content) return config_file + + +@pytest.fixture +def energy_totals_csv(tmp_path: Path) -> Path: + """Fixture providing a minimal energy_totals.csv for domestic-share tests.""" + energy_totals = pd.DataFrame( + { + "country": ["AT", "AT"], + "year": [2020, 2021], + "total domestic aviation": [30.0, 0.0], + "total international aviation": [70.0, 0.0], + "total domestic navigation": [20.0, 0.0], + "total international navigation": [80.0, 0.0], + } + ) + csv_path = tmp_path / "energy_totals.csv" + energy_totals.to_csv(csv_path, index=False) + return csv_path From f14685ff90a135d920a8a3e020d90e72e134e00a Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Tue, 21 Apr 2026 12:35:53 +0200 Subject: [PATCH 07/12] Debug final energy transport to cover cases with empty pd.Series --- .../statistics_functions.py | 61 +++++++++++++----- tests/test_network_processor.py | 45 ++++++++++++++ tests/test_statistics_functions.py | 62 ++++++++++++++----- 3 files changed, 137 insertions(+), 31 deletions(-) diff --git a/pypsa_validation_processing/statistics_functions.py b/pypsa_validation_processing/statistics_functions.py index 7bf7b43..062244e 100644 --- a/pypsa_validation_processing/statistics_functions.py +++ b/pypsa_validation_processing/statistics_functions.py @@ -89,24 +89,32 @@ def Final_Energy_by_Carrier__Electricity( def Final_Energy_by_Sector__Transportation( n: pypsa.Network, aggregate_per_year: bool = True, + energy_totals: pd.DataFrame | None = None, ) -> pd.Series | pd.DataFrame: """ """ - # TOOD: get domestic to international fractions for avation and navigation - domestic_aviation_fraction = 0.2 - domestic_navigation_fraction = 0.3 + domestic_aviation_fraction = get_energy_totals_domestic_share( + energy_totals, "aviation" + ) + domestic_navigation_fraction = get_energy_totals_domestic_share( + energy_totals, "navigation" + ) # get Final Energy [by Sector]|Transportation|Electricity elec = n.statistics.withdrawal( bus_carrier="low voltage", carrier="BEV charger", - aggregate_time=True, + aggregate_time=aggregate_per_year, **kwargs, ) # include losses from EV charging charging_out = ( n.statistics.energy_balance( - carrier="BEV charger", components="Link", at_port=["bus1"], **kwargs + carrier="BEV charger", + components="Link", + at_port=["bus1"], + groupby_time=aggregate_per_year, + **kwargs, ) .groupby(statistics_grouping_index) .sum() @@ -114,31 +122,42 @@ def Final_Energy_by_Sector__Transportation( charging_out.replace(0, np.nan, inplace=True) charging_in = ( n.statistics.energy_balance( - carrier="BEV charger", components="Link", at_port=["bus0"], **kwargs + carrier="BEV charger", + components="Link", + at_port=["bus0"], + groupby_time=aggregate_per_year, + **kwargs, ) .groupby(statistics_grouping_index) .sum() ) v2g_in = n.statistics.energy_balance( - carrier="V2G", components="Link", at_port=["bus0"], **kwargs + carrier="V2G", + components="Link", + at_port=["bus0"], + groupby_time=aggregate_per_year, + **kwargs, ) if v2g_in.empty: - ev_share = pd.Series(1.0, index=charging_out.index) + ev_share = charging_out.copy() + ev_share.loc[:] = 1.0 else: v2g_in = v2g_in.groupby(statistics_grouping_index).sum() ev_share = charging_out.add(v2g_in, fill_value=0).div(charging_out) - if (charging_in > 0).any() or (charging_out < 0).any(): + charging_in_has_positive = (charging_in > 0).to_numpy().any() + charging_out_has_negative = (charging_out < 0).to_numpy().any() + if charging_in_has_positive or charging_out_has_negative: raise ValueError("Charging in must be positive, charging out must be negative.") total_link_losses = charging_out.add(charging_in, fill_value=0) EV_charging_losses = total_link_losses.abs().mul(ev_share, fill_value=1.0) - # get Final Energy [by Sector]|Transportation|Hydroghen + # get Final Energy [by Sector]|Transportation|Hydrogen h2 = n.statistics.withdrawal( carrier="land transport fuel cell", components="Load", - aggregate_time=True, + aggregate_time=aggregate_per_year, **kwargs, ) @@ -147,7 +166,7 @@ def Final_Energy_by_Sector__Transportation( n.statistics.withdrawal( carrier="kerosene for aviation", components="Load", - aggregate_time=True, + aggregate_time=aggregate_per_year, **kwargs, ) * domestic_aviation_fraction @@ -157,14 +176,17 @@ def Final_Energy_by_Sector__Transportation( n.statistics.withdrawal( carrier=["shipping oil", "shipping methanol"], components="Load", - aggregate_time=True, + aggregate_time=aggregate_per_year, **kwargs, ) * domestic_navigation_fraction ) land_transport_liquids = n.statistics.withdrawal( - carrier="land transport oil", components="Load", aggregate_time=True, **kwargs + carrier="land transport oil", + components="Load", + aggregate_time=aggregate_per_year, + **kwargs, ) series_list = [ @@ -175,10 +197,19 @@ def Final_Energy_by_Sector__Transportation( land_transport_liquids, EV_charging_losses, ] + series_list = [series for series in series_list if not series.empty] + + result_type = type(series_list[0]) + if any(type(series) is not result_type for series in series_list): + raise TypeError( + "Transportation energy statistics must all have the same datatype." + ) + total = reduce(lambda a, b: a.add(b, fill_value=0), series_list) - total.groupby(statistics_grouping_index).sum() + total = total.groupby(statistics_grouping_index).sum() return total + def Final_Energy_by_Sector__Industry( n: pypsa.Network, aggregate_per_year: bool = True, diff --git a/tests/test_network_processor.py b/tests/test_network_processor.py index bef4d0e..ef795c3 100644 --- a/tests/test_network_processor.py +++ b/tests/test_network_processor.py @@ -269,6 +269,51 @@ def mock_func_without_config(n): assert isinstance(result, pd.Series) assert call_kwargs.get("called_with_config") is False + def test_execute_function_passes_energy_totals_when_accepted( + self, mock_config_file: Path + ): + """Test that energy_totals path is passed to functions that accept it.""" + with patch( + "pypsa_validation_processing.class_definitions.pypsa.NetworkCollection" + ): + with patch( + "pypsa_validation_processing.class_definitions.nomenclature.DataStructureDefinition" + ): + processor = Network_Processor(config_path=mock_config_file) + processor.functions_dict = { + "Test Variable": "mock_func_with_energy_totals" + } + + captured_kwargs = {} + + def mock_func_with_energy_totals(n, energy_totals=None): + captured_kwargs["energy_totals"] = energy_totals + return pd.Series( + [1.0], + index=pd.MultiIndex.from_tuples( + [("AT", "MWh_el")], names=["country", "unit"] + ), + ) + + with patch( + "pypsa_validation_processing.class_definitions.importlib.import_module" + ) as mock_import: + mock_module = MagicMock() + mock_module.mock_func_with_energy_totals = ( + mock_func_with_energy_totals + ) + mock_import.return_value = mock_module + + mock_network = MockPyPSANetwork() + result = processor._execute_function_for_variable( + "Test Variable", mock_network + ) + + assert isinstance(result, pd.Series) + assert captured_kwargs["energy_totals"] == ( + processor.network_results_path / "resources" / "energy_totals.csv" + ) + def test_execute_function_caches_signature_parameters(self, mock_config_file: Path): """Test that function signature inspection is cached per function.""" with patch( diff --git a/tests/test_statistics_functions.py b/tests/test_statistics_functions.py index e105e48..535bdce 100644 --- a/tests/test_statistics_functions.py +++ b/tests/test_statistics_functions.py @@ -71,47 +71,70 @@ def test_multiple_networks(self, mock_network_collection: MockNetworkCollection) class TestFinalEnergyBySectorTransportation: """Test suite for Final_Energy_by_Sector__Transportation function.""" - def test_returns_series(self, mock_network: MockPyPSANetwork): + def test_returns_series(self, mock_network: MockPyPSANetwork, energy_totals_csv): """Test that the function returns a pandas Series.""" - result = Final_Energy_by_Sector__Transportation(mock_network) + result = Final_Energy_by_Sector__Transportation( + mock_network, energy_totals=energy_totals_csv + ) assert isinstance(result, pd.Series) - def test_has_location_and_unit_multiindex(self, mock_network: MockPyPSANetwork): + def test_has_location_and_unit_multiindex( + self, mock_network: MockPyPSANetwork, energy_totals_csv + ): """Test that result has MultiIndex with location and unit levels.""" - result = Final_Energy_by_Sector__Transportation(mock_network) + result = Final_Energy_by_Sector__Transportation( + mock_network, energy_totals=energy_totals_csv + ) assert isinstance(result.index, pd.MultiIndex) assert "location" in result.index.names assert "unit" in result.index.names - def test_not_empty(self, mock_network: MockPyPSANetwork): + def test_not_empty(self, mock_network: MockPyPSANetwork, energy_totals_csv): """Test that result is not empty.""" - result = Final_Energy_by_Sector__Transportation(mock_network) + result = Final_Energy_by_Sector__Transportation( + mock_network, energy_totals=energy_totals_csv + ) assert len(result) > 0 - def test_numeric_values(self, mock_network: MockPyPSANetwork): + def test_numeric_values(self, mock_network: MockPyPSANetwork, energy_totals_csv): """Test that result values are numeric.""" - result = Final_Energy_by_Sector__Transportation(mock_network) + result = Final_Energy_by_Sector__Transportation( + mock_network, energy_totals=energy_totals_csv + ) assert result.dtype in [float, int] or pd.api.types.is_numeric_dtype( result.dtype ) - def test_contains_multiple_locations(self, mock_network: MockPyPSANetwork): + def test_contains_multiple_locations( + self, mock_network: MockPyPSANetwork, energy_totals_csv + ): """Test that result contains multiple locational data.""" - result = Final_Energy_by_Sector__Transportation(mock_network) + result = Final_Energy_by_Sector__Transportation( + mock_network, energy_totals=energy_totals_csv + ) locations = result.index.get_level_values("location").unique() assert len(locations) > 1 assert all(r.startswith("AT") for r in locations) - def test_multiple_networks(self, mock_network_collection: MockNetworkCollection): + def test_multiple_networks( + self, mock_network_collection: MockNetworkCollection, energy_totals_csv + ): """Test processing multiple networks from collection.""" for network in mock_network_collection: - result = Final_Energy_by_Sector__Transportation(network) + result = Final_Energy_by_Sector__Transportation( + network, energy_totals=energy_totals_csv + ) assert isinstance(result, pd.Series) assert isinstance(result.index, pd.MultiIndex) assert "location" in result.index.names assert "unit" in result.index.names assert len(result) > 0 + def test_raises_without_energy_totals(self, mock_network: MockPyPSANetwork): + """Current implementation requires energy_totals to compute domestic shares.""" + with pytest.raises(ValueError, match="Invalid file path or buffer object type"): + Final_Energy_by_Sector__Transportation(mock_network) + # --------------------------------------------------------------------------- # Tests for Final_Energy_by_Sector__Agriculture @@ -336,13 +359,20 @@ class TestAggregatePerYearFalse: Final_Energy_by_Sector__Agriculture, ] - def test_transportation_still_returns_series(self, mock_network: MockPyPSANetwork): - """Transportation currently returns an aggregated Series even when aggregate_per_year=False.""" - result = Final_Energy_by_Sector__Transportation(mock_network, aggregate_per_year=False) - assert isinstance(result, pd.Series) + def test_transportation_returns_dataframe( + self, mock_network: MockPyPSANetwork, energy_totals_csv + ): + """Transportation returns a DataFrame when aggregate_per_year=False.""" + result = Final_Energy_by_Sector__Transportation( + mock_network, + aggregate_per_year=False, + energy_totals=energy_totals_csv, + ) + assert isinstance(result, pd.DataFrame) assert isinstance(result.index, pd.MultiIndex) assert "location" in result.index.names assert "unit" in result.index.names + assert isinstance(result.columns, pd.DatetimeIndex) assert len(result) > 0 @pytest.mark.parametrize("func", _FUNCTIONS, ids=lambda f: f.__name__) From 2fa30b1c24f86b7f938422cfbeb7080b5735ede1 Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Tue, 21 Apr 2026 13:47:30 +0200 Subject: [PATCH 08/12] Integrate sourcery review --- .../statistics_functions.py | 55 +++++++++++++++++-- pypsa_validation_processing/utils.py | 2 +- tests/test_statistics_functions.py | 2 +- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/pypsa_validation_processing/statistics_functions.py b/pypsa_validation_processing/statistics_functions.py index 062244e..eaf32f8 100644 --- a/pypsa_validation_processing/statistics_functions.py +++ b/pypsa_validation_processing/statistics_functions.py @@ -21,6 +21,7 @@ def (network: pypsa.Network) -> pd.Series: from __future__ import annotations from functools import reduce +from pathlib import Path import pandas as pd import numpy as np import pypsa @@ -89,9 +90,54 @@ def Final_Energy_by_Carrier__Electricity( def Final_Energy_by_Sector__Transportation( n: pypsa.Network, aggregate_per_year: bool = True, - energy_totals: pd.DataFrame | None = None, + energy_totals: Path | None = None, ) -> pd.Series | pd.DataFrame: - """ """ + """Extract transportation-sector final energy from a PyPSA Network. + + Returns the total energy consumed by the Transportation sector + (excluding transmission / distribution losses) across the pypsa-network. + + Parameters + ---------- + n : pypsa.Network + PyPSA network to process. + aggregate_per_year : bool, optional + If ``True`` (default), aggregate over all snapshots and return a + :class:`pandas.Series`. If ``False``, return a + :class:`pandas.DataFrame` with snapshots as columns. + energy_totals : pathlib.Path | None, optional + Path to an energy totals file used to determine domestic shares for + aviation and navigation liquid fuels. If ``None``, default domestic + shares from :func:`get_energy_totals_domestic_share` are used. + + Returns + ------- + pd.Series | pd.DataFrame + Pandas Series (``aggregate_per_year=True``) or DataFrame + (``aggregate_per_year=False``) with MultiIndex including ``location`` + and ``unit``. + Returns data at regional level as provided by the PyPSA network. + Country-level aggregation is handled by + Network_Processor._aggregate_to_country() if configured. + + Notes + ----- + Sums transportation final energy from electricity, hydrogen, and liquid + fuels: + - Electricity demand from BEV charging loads. + - Additional EV charging losses computed from BEV charger link flows and + adjusted for V2G participation. + - Hydrogen demand from fuel-cell land transport. + - Liquid fuels for aviation and navigation scaled by domestic fractions, + plus land-transport oil. + + Raises + ------ + ValueError + If BEV charging flow sign conventions are violated. + TypeError + If intermediate statistics return mixed result types. + """ domestic_aviation_fraction = get_energy_totals_domestic_share( energy_totals, "aviation" @@ -199,8 +245,9 @@ def Final_Energy_by_Sector__Transportation( ] series_list = [series for series in series_list if not series.empty] - result_type = type(series_list[0]) - if any(type(series) is not result_type for series in series_list): + if series_list and any( + type(series) is not type(series_list[0]) for series in series_list + ): raise TypeError( "Transportation energy statistics must all have the same datatype." ) diff --git a/pypsa_validation_processing/utils.py b/pypsa_validation_processing/utils.py index 07a19d1..719d004 100644 --- a/pypsa_validation_processing/utils.py +++ b/pypsa_validation_processing/utils.py @@ -153,7 +153,7 @@ def get_energy_totals_domestic_share( Parameters ---------- energy_totals - The energy totals data frame filtered to one energy year. + Path to the energy totals csv file. kind: {'aviation', 'navigation'} The kind of energy totals to calculate the factor for. diff --git a/tests/test_statistics_functions.py b/tests/test_statistics_functions.py index 535bdce..25923fe 100644 --- a/tests/test_statistics_functions.py +++ b/tests/test_statistics_functions.py @@ -132,7 +132,7 @@ def test_multiple_networks( def test_raises_without_energy_totals(self, mock_network: MockPyPSANetwork): """Current implementation requires energy_totals to compute domestic shares.""" - with pytest.raises(ValueError, match="Invalid file path or buffer object type"): + with pytest.raises(ValueError): Final_Energy_by_Sector__Transportation(mock_network) From 66e44f075785b5b6548d4c67f9e4fe729ecd4537 Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Wed, 22 Apr 2026 16:08:36 +0200 Subject: [PATCH 09/12] Review: enshure common index to use pd.concat --- pypsa_validation_processing/statistics_functions.py | 7 +++++-- pypsa_validation_processing/utils.py | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pypsa_validation_processing/statistics_functions.py b/pypsa_validation_processing/statistics_functions.py index eaf32f8..af960a0 100644 --- a/pypsa_validation_processing/statistics_functions.py +++ b/pypsa_validation_processing/statistics_functions.py @@ -26,6 +26,9 @@ def (network: pypsa.Network) -> pd.Series: import numpy as np import pypsa from pypsa_validation_processing.utils import statistics_kwargs as kwargs +from pypsa_validation_processing.utils import ( + statistics_kwargs_for_filtering as kwargs_filtering, +) from pypsa_validation_processing.utils import ( statistics_grouping_index, get_energy_totals_domestic_share, @@ -150,6 +153,7 @@ def Final_Energy_by_Sector__Transportation( elec = n.statistics.withdrawal( bus_carrier="low voltage", carrier="BEV charger", + components="Link", aggregate_time=aggregate_per_year, **kwargs, ) @@ -252,8 +256,7 @@ def Final_Energy_by_Sector__Transportation( "Transportation energy statistics must all have the same datatype." ) - total = reduce(lambda a, b: a.add(b, fill_value=0), series_list) - total = total.groupby(statistics_grouping_index).sum() + total = pd.concat(series_list) return total diff --git a/pypsa_validation_processing/utils.py b/pypsa_validation_processing/utils.py index 719d004..f2ff5ab 100644 --- a/pypsa_validation_processing/utils.py +++ b/pypsa_validation_processing/utils.py @@ -133,6 +133,10 @@ ## standards for statistics-functions # standardize kwargs for pypsa-statistics statements statistics_kwargs = { + "groupby": ["location", "unit"], + "nice_names": False, +} +statistics_kwargs_for_filtering = { "groupby": ["name", "bus", "carrier", "location", "unit"], "nice_names": False, } From 3aa40f73229afa0228f650a79cabf6416954a5c5 Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Wed, 22 Apr 2026 16:35:14 +0200 Subject: [PATCH 10/12] calculate charging losses directly from load --- .../statistics_functions.py | 62 +++++-------------- 1 file changed, 15 insertions(+), 47 deletions(-) diff --git a/pypsa_validation_processing/statistics_functions.py b/pypsa_validation_processing/statistics_functions.py index af960a0..b7b576a 100644 --- a/pypsa_validation_processing/statistics_functions.py +++ b/pypsa_validation_processing/statistics_functions.py @@ -150,58 +150,27 @@ def Final_Energy_by_Sector__Transportation( ) # get Final Energy [by Sector]|Transportation|Electricity + # get elec load losses from BEV-charger links + bev_charger_efficiencies = n.links.loc[ + n.links.carrier == "BEV charger", "efficiency" + ].dropna() + if bev_charger_efficiencies.nunique() == 1: + eff = bev_charger_efficiencies.iloc[0] + else: + eff = bev_charger_efficiencies.mean() + print( + "WARNING: Network includes different efficiencies for BEV chargers. Using mean value for variable Final_Energy_by_Sector__Transportation" + ) + elec = n.statistics.withdrawal( bus_carrier="low voltage", carrier="BEV charger", components="Link", aggregate_time=aggregate_per_year, **kwargs, - ) - # include losses from EV charging - charging_out = ( - n.statistics.energy_balance( - carrier="BEV charger", - components="Link", - at_port=["bus1"], - groupby_time=aggregate_per_year, - **kwargs, - ) - .groupby(statistics_grouping_index) - .sum() - ) - charging_out.replace(0, np.nan, inplace=True) - charging_in = ( - n.statistics.energy_balance( - carrier="BEV charger", - components="Link", - at_port=["bus0"], - groupby_time=aggregate_per_year, - **kwargs, - ) - .groupby(statistics_grouping_index) - .sum() - ) - v2g_in = n.statistics.energy_balance( - carrier="V2G", - components="Link", - at_port=["bus0"], - groupby_time=aggregate_per_year, - **kwargs, - ) - if v2g_in.empty: - ev_share = charging_out.copy() - ev_share.loc[:] = 1.0 - else: - v2g_in = v2g_in.groupby(statistics_grouping_index).sum() - ev_share = charging_out.add(v2g_in, fill_value=0).div(charging_out) - - charging_in_has_positive = (charging_in > 0).to_numpy().any() - charging_out_has_negative = (charging_out < 0).to_numpy().any() - if charging_in_has_positive or charging_out_has_negative: - raise ValueError("Charging in must be positive, charging out must be negative.") - - total_link_losses = charging_out.add(charging_in, fill_value=0) - EV_charging_losses = total_link_losses.abs().mul(ev_share, fill_value=1.0) + ).mul( + 1 / eff + ) # ( 1 + ((1/eff)-1)) # get Final Energy [by Sector]|Transportation|Hydrogen h2 = n.statistics.withdrawal( @@ -245,7 +214,6 @@ def Final_Energy_by_Sector__Transportation( aviation_liquids, navigation_liquids, land_transport_liquids, - EV_charging_losses, ] series_list = [series for series in series_list if not series.empty] From bc212bc6d0bb6649402171144290618dd4222a82 Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Wed, 22 Apr 2026 16:55:24 +0200 Subject: [PATCH 11/12] remove type checking as type is enshured by consistent use of pypsa-statistics --- pypsa_validation_processing/statistics_functions.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pypsa_validation_processing/statistics_functions.py b/pypsa_validation_processing/statistics_functions.py index b7b576a..51b803e 100644 --- a/pypsa_validation_processing/statistics_functions.py +++ b/pypsa_validation_processing/statistics_functions.py @@ -217,13 +217,6 @@ def Final_Energy_by_Sector__Transportation( ] series_list = [series for series in series_list if not series.empty] - if series_list and any( - type(series) is not type(series_list[0]) for series in series_list - ): - raise TypeError( - "Transportation energy statistics must all have the same datatype." - ) - total = pd.concat(series_list) return total From 77cf7d0ee3d9d9a885dea91b8c800b0d9be3e672 Mon Sep 17 00:00:00 2001 From: Max Nutz Date: Wed, 22 Apr 2026 17:03:55 +0200 Subject: [PATCH 12/12] add link to MockNetwork for testing. --- tests/conftest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index d764104..cf3f9f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -168,6 +168,13 @@ def __init__(self, name: str = "test_network", **kwargs): "wildcards": {"planning_horizons": 2020}, } self.statistics = MockStatisticsAccessor() + self.links = pd.DataFrame( + { + "carrier": ["BEV charger", "BEV charger", "other link"], + "efficiency": [0.9, 0.9, 1.0], + }, + index=["bev_charger_at1", "bev_charger_at2", "other_link_at1"], + ) # Add carriers attribute with empty index by default self.carriers = pd.DataFrame(index=[])