From 9defa401f591e5d24270bcc8135d29b1364d361f Mon Sep 17 00:00:00 2001 From: Hinetziedacted <128074209+Hinetziedacted@users.noreply.github.com> Date: Sat, 24 Jan 2026 09:14:57 +0530 Subject: [PATCH 01/11] Add Nordic region emission factors and update emissions logic - Created nordic_emissions.json with static emission factors (gCO2eq/kWh) for Nordic regions: SE1-4, NO1-5, FI - Updated emissions.py to check for Nordic regions and load static factors from the new JSON file - Sweden/Norway regions use 18 gCO2eq/kWh, Finland uses 72 gCO2eq/kWh based on ENTSO-E data --- codecarbon/core/emissions.py | 22 ++++++ .../data/private_infra/nordic_emissions.json | 69 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 codecarbon/data/private_infra/nordic_emissions.json diff --git a/codecarbon/core/emissions.py b/codecarbon/core/emissions.py index 78845f0d8..a95584ddd 100644 --- a/codecarbon/core/emissions.py +++ b/codecarbon/core/emissions.py @@ -155,6 +155,28 @@ def get_private_infra_emissions(self, energy: Energy, geo: GeoMetadata) -> float + " >>> Using CodeCarbon's data." ) + # Check for Nordic regions (SE1-4, NO1-5, FI) and use static emission factors + nordic_regions = ["SE1", "SE2", "SE3", "SE4", "NO1", "NO2", "NO3", "NO4", "NO5", "FI"] + if geo.region is not None and geo.region.upper() in nordic_regions: + try: + import json + from pathlib import Path + nordic_file = Path(__file__).parent.parent / "data" / "private_infra" / "nordic_emissions.json" + with open(nordic_file, 'r') as f: + nordic_data = json.load(f) + region_data = nordic_data["data"].get(geo.region.upper()) + if region_data: + emission_factor_g = region_data["emission_factor"] # gCO2eq/kWh + emission_factor_kg = emission_factor_g / 1000 # Convert to kgCO2eq/kWh + emissions = emission_factor_kg * energy.kWh # kgCO2eq + logger.debug(f"Nordic region {geo.region}: Retrieved emissions using static factor " + + f"{emission_factor_g} gCO2eq/kWh: {emissions * 1000} g CO2eq" + ) + return emissions + except Exception as e: + logger.warning(f"Error loading Nordic emissions data for {geo.region}: {e}. " + + "Falling back to default emission calculation." + compute_with_regional_data: bool = (geo.region is not None) and ( geo.country_iso_code.upper() in ["USA", "CAN"] ) diff --git a/codecarbon/data/private_infra/nordic_emissions.json b/codecarbon/data/private_infra/nordic_emissions.json new file mode 100644 index 000000000..a49083c23 --- /dev/null +++ b/codecarbon/data/private_infra/nordic_emissions.json @@ -0,0 +1,69 @@ +{ + "data": { + "SE1": { + "emission_factor": 18.0, + "unit": "gCO2eq/kWh", + "description": "Sweden Bidding Zone 1 (Northern Sweden)", + "year": 2024 + }, + "SE2": { + "emission_factor": 18.0, + "unit": "gCO2eq/kWh", + "description": "Sweden Bidding Zone 2 (Central Sweden)", + "year": 2024 + }, + "SE3": { + "emission_factor": 18.0, + "unit": "gCO2eq/kWh", + "description": "Sweden Bidding Zone 3 (Southern Sweden)", + "year": 2024 + }, + "SE4": { + "emission_factor": 18.0, + "unit": "gCO2eq/kWh", + "description": "Sweden Bidding Zone 4 (Stockholm region)", + "year": 2024 + }, + "NO1": { + "emission_factor": 18.0, + "unit": "gCO2eq/kWh", + "description": "Norway Bidding Zone 1 (Oslo)", + "year": 2024 + }, + "NO2": { + "emission_factor": 18.0, + "unit": "gCO2eq/kWh", + "description": "Norway Bidding Zone 2 (Southern Norway)", + "year": 2024 + }, + "NO3": { + "emission_factor": 18.0, + "unit": "gCO2eq/kWh", + "description": "Norway Bidding Zone 3 (Central Norway)", + "year": 2024 + }, + "NO4": { + "emission_factor": 18.0, + "unit": "gCO2eq/kWh", + "description": "Norway Bidding Zone 4 (Northern Norway)", + "year": 2024 + }, + "NO5": { + "emission_factor": 18.0, + "unit": "gCO2eq/kWh", + "description": "Norway Bidding Zone 5 (Western Norway)", + "year": 2024 + }, + "FI": { + "emission_factor": 72.0, + "unit": "gCO2eq/kWh", + "description": "Finland", + "year": 2025 + } + }, + "metadata": { + "source": "Based on historical averages from ENTSO-E data", + "last_updated": "2026-01-24", + "notes": "Static emission factors for Nordic regions. Sweden and Norway have very low carbon intensity due to high renewable energy (primarily hydro and nuclear). Finland has higher emissions due to greater fossil fuel dependency." + } +} \ No newline at end of file From 2ce226a2e255832f58506c9814d60741d090b244 Mon Sep 17 00:00:00 2001 From: Hinetziedacted <128074209+Hinetziedacted@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:38:14 +0530 Subject: [PATCH 02/11] Add comprehensive documentation for Nordic emission factors - Added detailed comments explaining data sources (ENTSO-E, Fingrid) - Included update procedure for annual maintenance - Documented emission values: 18 gCO2eq/kWh (SE/NO), 72 gCO2eq/kWh (FI) - Added direct links to data sources for future updates --- codecarbon/core/emissions.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/codecarbon/core/emissions.py b/codecarbon/core/emissions.py index a95584ddd..b7fa5b3dc 100644 --- a/codecarbon/core/emissions.py +++ b/codecarbon/core/emissions.py @@ -155,6 +155,30 @@ def get_private_infra_emissions(self, energy: Energy, geo: GeoMetadata) -> float + " >>> Using CodeCarbon's data." ) + # NORDIC EMISSION FACTORS DOCUMENTATION + # ========================================== + # Static emission factors for Nordic electricity regions. + # These values represent the carbon intensity (gCO2eq/kWh) of electricity + # production in specific Nordic bidding zones. + # + # DATA SOURCES: + # - Sweden/Norway (SE1-4, NO1-5): 18 gCO2eq/kWh + # Based on Nordic grid average (<60 gCO2eq/kWh per ENTSO-E) + # Source: https://transparency.entsoe.eu/ + # Nordic Energy Research: https://www.nordicenergy.org/indicators/ + # + # - Finland (FI): 72 gCO2eq/kWh + # Source: Fingrid real-time CO2 emissions estimate + # https://www.fingrid.fi/en/electricity-market-information/real-time-co2-emissions-estimate/ + # + # UPDATE PROCEDURE: + # To update these values annually: + # 1. Check latest data from ENTSO-E Transparency Platform + # 2. Check Fingrid for Finnish-specific data + # 3. Update codecarbon/data/private_infra/nordic_emissions.json + # 4. Values should reflect the most recent annual average + # + # Check for Nordic regions (SE1-4, NO1-5, FI) and use static emission factors nordic_regions = ["SE1", "SE2", "SE3", "SE4", "NO1", "NO2", "NO3", "NO4", "NO5", "FI"] if geo.region is not None and geo.region.upper() in nordic_regions: From a5ff78fbafbb92b3afe7afbc67a284ba1ac71219 Mon Sep 17 00:00:00 2001 From: Hinetziedacted <128074209+Hinetziedacted@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:43:27 +0530 Subject: [PATCH 03/11] Fix syntax error: add missing closing parenthesis to logger.warning --- codecarbon/core/emissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecarbon/core/emissions.py b/codecarbon/core/emissions.py index b7fa5b3dc..bbfee33c7 100644 --- a/codecarbon/core/emissions.py +++ b/codecarbon/core/emissions.py @@ -199,7 +199,7 @@ def get_private_infra_emissions(self, energy: Energy, geo: GeoMetadata) -> float return emissions except Exception as e: logger.warning(f"Error loading Nordic emissions data for {geo.region}: {e}. " - + "Falling back to default emission calculation." + + "Falling back to default emission calculation.") compute_with_regional_data: bool = (geo.region is not None) and ( geo.country_iso_code.upper() in ["USA", "CAN"] From 9605e1eba5d0282b82cadcdef609cb04a80276e7 Mon Sep 17 00:00:00 2001 From: Hinetziedacted <128074209+Hinetziedacted@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:59:12 +0530 Subject: [PATCH 04/11] Add caching for Nordic country energy mix data - Load and cache Nordic country energy mix data in _load_static_data() - Add get_nordic_country_energy_mix_data() method to retrieve cached data - This addresses the caching performance request in PR #1039 --- codecarbon/input.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/codecarbon/input.py b/codecarbon/input.py index 23ede6157..0d4015eba 100644 --- a/codecarbon/input.py +++ b/codecarbon/input.py @@ -61,6 +61,11 @@ def _load_static_data() -> None: _CACHE["cpu_power"] = pd.read_csv(path) + # Nordic country energy mix - used for emissions calculations + path = _get_resource_path("data/private_infra/nordic_country_energy_mix.json") + with open(path) as f: + _CACHE["nordic_country_energy_mix"] = json.load(f) + # Load static data at module import _load_static_data() @@ -189,6 +194,13 @@ def get_cpu_power_data(self) -> pd.DataFrame: """ return _CACHE["cpu_power"] + def get_nordic_country_energy_mix_data(self) -> Dict: + """ + Returns Nordic Country Energy Mix Data. + Data is cached on first access per country. + """ + return _CACHE["nordic_country_energy_mix"] + class DataSourceException(Exception): pass From 358226baf5813a06e0db0a5033b97581ab4e67c4 Mon Sep 17 00:00:00 2001 From: Hinetziedacted <128074209+Hinetziedacted@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:04:28 +0530 Subject: [PATCH 05/11] Refactor emissions.py to use cached Nordic energy mix data - Replace direct JSON file loading with cached data retrieval - Use self._data_source.get_nordic_country_energy_mix_data() method - Improves performance by eliminating repeated file I/O operations - Part of implementation for PR #1039 reviewer feedback --- codecarbon/core/emissions.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/codecarbon/core/emissions.py b/codecarbon/core/emissions.py index bbfee33c7..58c3659da 100644 --- a/codecarbon/core/emissions.py +++ b/codecarbon/core/emissions.py @@ -183,11 +183,8 @@ def get_private_infra_emissions(self, energy: Energy, geo: GeoMetadata) -> float nordic_regions = ["SE1", "SE2", "SE3", "SE4", "NO1", "NO2", "NO3", "NO4", "NO5", "FI"] if geo.region is not None and geo.region.upper() in nordic_regions: try: - import json - from pathlib import Path - nordic_file = Path(__file__).parent.parent / "data" / "private_infra" / "nordic_emissions.json" - with open(nordic_file, 'r') as f: - nordic_data = json.load(f) + # Get Nordic energy mix data from cache + nordic_data = self._data_source.get_nordic_country_energy_mix_data() nordic_data = json.load(f) region_data = nordic_data["data"].get(geo.region.upper()) if region_data: emission_factor_g = region_data["emission_factor"] # gCO2eq/kWh From c5590028c91d01cfd76fbc3ab8e20e1e8035ec2b Mon Sep 17 00:00:00 2001 From: Hinetziedacted <128074209+Hinetziedacted@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:07:04 +0530 Subject: [PATCH 06/11] Add unit tests for Nordic emissions functionality - Add test_get_emissions_PRIVATE_INFRA_NORDIC_REGION for Swedish region SE2 - Add test_get_emissions_PRIVATE_INFRA_NORDIC_FINLAND for Finland region FI - Tests verify that Nordic regions use static emission factors correctly - Tests check that emissions are positive and proportional to energy consumed - Implements unit test requirement from PR #1039 reviewer feedback --- tests/test_emissions.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_emissions.py b/tests/test_emissions.py index 517bf3c2a..8c8daa131 100644 --- a/tests/test_emissions.py +++ b/tests/test_emissions.py @@ -172,3 +172,34 @@ def test_get_emissions_PRIVATE_INFRA_unknown_country(self): ) assert isinstance(emissions, float) self.assertAlmostEqual(emissions, 0.475, places=2) + + def test_get_emissions_PRIVATE_INFRA_NORDIC_REGION(self): + # WHEN + # Test Nordic region (Sweden SE2) + + emissions = self._emissions.get_private_infra_emissions( + Energy.from_energy(kWh=1.0), + GeoMetadata(country_iso_code="SWE", country_name="Sweden", region="SE2"), + ) + + # THEN + # Nordic regions use static emission factors from the JSON file + # SE2 has an emission factor specified in nordic_country_energy_mix.json + assert isinstance(emissions, float) + assert emissions > 0, "Nordic region emissions should be positive" + + def test_get_emissions_PRIVATE_INFRA_NORDIC_FINLAND(self): + # WHEN + # Test Nordic region (Finland) + + emissions = self._emissions.get_private_infra_emissions( + Energy.from_energy(kWh=2.5), + GeoMetadata(country_iso_code="FIN", country_name="Finland", region="FI"), + ) + + # THEN + # Finland (FI) should use Nordic static emission factors + assert isinstance(emissions, float) + assert emissions > 0, "Finland emissions should be positive" + # With 2.5 kWh, emissions should be proportional to energy consumed + assert emissions > 0.1, "Expected reasonable emission value for 2.5 kWh" From 0534362d70e68eb41d03554701dc93cd5b64f6e0 Mon Sep 17 00:00:00 2001 From: Hinetziedacted <128074209+Hinetziedacted@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:26:15 +0530 Subject: [PATCH 07/11] Update path for Nordic emissions data file Fix filename mismatch: Changed from 'nordic_country_energy_mix.json' to 'nordic_emissions.json' to match the actual file that was created. --- codecarbon/input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecarbon/input.py b/codecarbon/input.py index 0d4015eba..68f3d3b17 100644 --- a/codecarbon/input.py +++ b/codecarbon/input.py @@ -62,7 +62,7 @@ def _load_static_data() -> None: # Nordic country energy mix - used for emissions calculations - path = _get_resource_path("data/private_infra/nordic_country_energy_mix.json") + path = _get_resource_path("data/private_infra/nordic_emissions.json") with open(path) as f: _CACHE["nordic_country_energy_mix"] = json.load(f) From da1510aa89db35187546bf26da92af8fe3629baa Mon Sep 17 00:00:00 2001 From: Hinetziedacted <128074209+Hinetziedacted@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:35:24 +0530 Subject: [PATCH 08/11] Remove leftover json.load code from line 187 --- codecarbon/core/emissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecarbon/core/emissions.py b/codecarbon/core/emissions.py index 58c3659da..81eaada7c 100644 --- a/codecarbon/core/emissions.py +++ b/codecarbon/core/emissions.py @@ -184,7 +184,7 @@ def get_private_infra_emissions(self, energy: Energy, geo: GeoMetadata) -> float if geo.region is not None and geo.region.upper() in nordic_regions: try: # Get Nordic energy mix data from cache - nordic_data = self._data_source.get_nordic_country_energy_mix_data() nordic_data = json.load(f) + nordic_data = self._data_source.get_nordic_country_energy_mix_data() region_data = nordic_data["data"].get(geo.region.upper()) if region_data: emission_factor_g = region_data["emission_factor"] # gCO2eq/kWh From 1016d4af9a13657d86046bd72163b3a3acd4d63a Mon Sep 17 00:00:00 2001 From: benoit-cty Date: Fri, 30 Jan 2026 20:36:42 +0100 Subject: [PATCH 09/11] doc --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7ea3449b..3be80865f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -248,7 +248,7 @@ flake8...................................................................Passed If any of the linters/formatters fail, check the difference with `git diff`, add the differences if there is no behavior changes (isort and black might have change some coding style or import order, this is expected it is their job) with `git add` and finally try to commit again `git commit ...`. -You can also run `pre-commit` with `uv run pre-commit run -v` if you have some changes staged but you are not ready yet to commit. +You can also run `pre-commit` with `uv run pre-commit run --all-files` if you have some changes staged but you are not ready yet to commit. From e1cb9a76dd8cfc395cae2b06ca9aebe208702aa7 Mon Sep 17 00:00:00 2001 From: benoit-cty Date: Fri, 30 Jan 2026 20:37:24 +0100 Subject: [PATCH 10/11] lint --- codecarbon/core/emissions.py | 58 ++++++++++++++++++++++++------------ codecarbon/input.py | 2 +- tests/test_emissions.py | 6 ++-- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/codecarbon/core/emissions.py b/codecarbon/core/emissions.py index 81eaada7c..af679c760 100644 --- a/codecarbon/core/emissions.py +++ b/codecarbon/core/emissions.py @@ -178,25 +178,45 @@ def get_private_infra_emissions(self, energy: Energy, geo: GeoMetadata) -> float # 3. Update codecarbon/data/private_infra/nordic_emissions.json # 4. Values should reflect the most recent annual average # - - # Check for Nordic regions (SE1-4, NO1-5, FI) and use static emission factors - nordic_regions = ["SE1", "SE2", "SE3", "SE4", "NO1", "NO2", "NO3", "NO4", "NO5", "FI"] - if geo.region is not None and geo.region.upper() in nordic_regions: - try: - # Get Nordic energy mix data from cache - nordic_data = self._data_source.get_nordic_country_energy_mix_data() - region_data = nordic_data["data"].get(geo.region.upper()) - if region_data: - emission_factor_g = region_data["emission_factor"] # gCO2eq/kWh - emission_factor_kg = emission_factor_g / 1000 # Convert to kgCO2eq/kWh - emissions = emission_factor_kg * energy.kWh # kgCO2eq - logger.debug(f"Nordic region {geo.region}: Retrieved emissions using static factor " - + f"{emission_factor_g} gCO2eq/kWh: {emissions * 1000} g CO2eq" - ) - return emissions - except Exception as e: - logger.warning(f"Error loading Nordic emissions data for {geo.region}: {e}. " - + "Falling back to default emission calculation.") + + # Check for Nordic regions (SE1-4, NO1-5, FI) and use static emission factors + nordic_regions = [ + "SE1", + "SE2", + "SE3", + "SE4", + "NO1", + "NO2", + "NO3", + "NO4", + "NO5", + "FI", + ] + if geo.region is not None and geo.region.upper() in nordic_regions: + try: + # Get Nordic energy mix data from cache + nordic_data = ( + self._data_source.get_nordic_country_energy_mix_data() + ) + region_data = nordic_data["data"].get(geo.region.upper()) + if region_data: + emission_factor_g = region_data[ + "emission_factor" + ] # gCO2eq/kWh + emission_factor_kg = ( + emission_factor_g / 1000 + ) # Convert to kgCO2eq/kWh + emissions = emission_factor_kg * energy.kWh # kgCO2eq + logger.debug( + f"Nordic region {geo.region}: Retrieved emissions using static factor " + + f"{emission_factor_g} gCO2eq/kWh: {emissions * 1000} g CO2eq" + ) + return emissions + except Exception as e: + logger.warning( + f"Error loading Nordic emissions data for {geo.region}: {e}. " + + "Falling back to default emission calculation." + ) compute_with_regional_data: bool = (geo.region is not None) and ( geo.country_iso_code.upper() in ["USA", "CAN"] diff --git a/codecarbon/input.py b/codecarbon/input.py index 68f3d3b17..9d7583ad2 100644 --- a/codecarbon/input.py +++ b/codecarbon/input.py @@ -60,12 +60,12 @@ def _load_static_data() -> None: path = _get_resource_path("data/hardware/cpu_power.csv") _CACHE["cpu_power"] = pd.read_csv(path) - # Nordic country energy mix - used for emissions calculations path = _get_resource_path("data/private_infra/nordic_emissions.json") with open(path) as f: _CACHE["nordic_country_energy_mix"] = json.load(f) + # Load static data at module import _load_static_data() diff --git a/tests/test_emissions.py b/tests/test_emissions.py index 8c8daa131..7c1b1427a 100644 --- a/tests/test_emissions.py +++ b/tests/test_emissions.py @@ -173,10 +173,10 @@ def test_get_emissions_PRIVATE_INFRA_unknown_country(self): assert isinstance(emissions, float) self.assertAlmostEqual(emissions, 0.475, places=2) - def test_get_emissions_PRIVATE_INFRA_NORDIC_REGION(self): + def test_get_emissions_PRIVATE_INFRA_NORDIC_REGION(self): # WHEN # Test Nordic region (Sweden SE2) - + emissions = self._emissions.get_private_infra_emissions( Energy.from_energy(kWh=1.0), GeoMetadata(country_iso_code="SWE", country_name="Sweden", region="SE2"), @@ -191,7 +191,7 @@ def test_get_emissions_PRIVATE_INFRA_NORDIC_REGION(self): def test_get_emissions_PRIVATE_INFRA_NORDIC_FINLAND(self): # WHEN # Test Nordic region (Finland) - + emissions = self._emissions.get_private_infra_emissions( Energy.from_energy(kWh=2.5), GeoMetadata(country_iso_code="FIN", country_name="Finland", region="FI"), From 8ccb680764aca18e4e4a8343e5d993e1be0b3cbb Mon Sep 17 00:00:00 2001 From: benoit-cty Date: Sun, 1 Feb 2026 10:52:19 +0100 Subject: [PATCH 11/11] refacto --- codecarbon/core/emissions.py | 121 +++++++++++++++++------------------ tests/test_emissions.py | 25 ++++++-- 2 files changed, 78 insertions(+), 68 deletions(-) diff --git a/codecarbon/core/emissions.py b/codecarbon/core/emissions.py index af679c760..99426b981 100644 --- a/codecarbon/core/emissions.py +++ b/codecarbon/core/emissions.py @@ -155,71 +155,8 @@ def get_private_infra_emissions(self, energy: Energy, geo: GeoMetadata) -> float + " >>> Using CodeCarbon's data." ) - # NORDIC EMISSION FACTORS DOCUMENTATION - # ========================================== - # Static emission factors for Nordic electricity regions. - # These values represent the carbon intensity (gCO2eq/kWh) of electricity - # production in specific Nordic bidding zones. - # - # DATA SOURCES: - # - Sweden/Norway (SE1-4, NO1-5): 18 gCO2eq/kWh - # Based on Nordic grid average (<60 gCO2eq/kWh per ENTSO-E) - # Source: https://transparency.entsoe.eu/ - # Nordic Energy Research: https://www.nordicenergy.org/indicators/ - # - # - Finland (FI): 72 gCO2eq/kWh - # Source: Fingrid real-time CO2 emissions estimate - # https://www.fingrid.fi/en/electricity-market-information/real-time-co2-emissions-estimate/ - # - # UPDATE PROCEDURE: - # To update these values annually: - # 1. Check latest data from ENTSO-E Transparency Platform - # 2. Check Fingrid for Finnish-specific data - # 3. Update codecarbon/data/private_infra/nordic_emissions.json - # 4. Values should reflect the most recent annual average - # - - # Check for Nordic regions (SE1-4, NO1-5, FI) and use static emission factors - nordic_regions = [ - "SE1", - "SE2", - "SE3", - "SE4", - "NO1", - "NO2", - "NO3", - "NO4", - "NO5", - "FI", - ] - if geo.region is not None and geo.region.upper() in nordic_regions: - try: - # Get Nordic energy mix data from cache - nordic_data = ( - self._data_source.get_nordic_country_energy_mix_data() - ) - region_data = nordic_data["data"].get(geo.region.upper()) - if region_data: - emission_factor_g = region_data[ - "emission_factor" - ] # gCO2eq/kWh - emission_factor_kg = ( - emission_factor_g / 1000 - ) # Convert to kgCO2eq/kWh - emissions = emission_factor_kg * energy.kWh # kgCO2eq - logger.debug( - f"Nordic region {geo.region}: Retrieved emissions using static factor " - + f"{emission_factor_g} gCO2eq/kWh: {emissions * 1000} g CO2eq" - ) - return emissions - except Exception as e: - logger.warning( - f"Error loading Nordic emissions data for {geo.region}: {e}. " - + "Falling back to default emission calculation." - ) - compute_with_regional_data: bool = (geo.region is not None) and ( - geo.country_iso_code.upper() in ["USA", "CAN"] + geo.country_iso_code.upper() in ["USA", "CAN", "SWE", "NOR", "FIN"] ) if compute_with_regional_data: @@ -233,16 +170,72 @@ def get_private_infra_emissions(self, energy: Energy, geo: GeoMetadata) -> float ) return self.get_country_emissions(energy, geo) + def _try_get_nordic_region_emissions( + self, energy: Energy, geo: GeoMetadata + ) -> Optional[float]: + nordic_regions = { + "SE1", + "SE2", + "SE3", + "SE4", + "NO1", + "NO2", + "NO3", + "NO4", + "NO5", + "FI", + } + if geo.region is None: + return None + + region_upper = geo.region.upper() + if region_upper not in nordic_regions: + return None + + try: + nordic_data = self._data_source.get_nordic_country_energy_mix_data() + region_data = nordic_data["data"].get(region_upper) + if region_data: + emission_factor_g = region_data["emission_factor"] + emission_factor_kg = emission_factor_g / 1000 + emissions = emission_factor_kg * energy.kWh + logger.debug( + f"Nordic region {geo.region}: Retrieved emissions using static factor " + + f"{emission_factor_g} gCO2eq/kWh: {emissions * 1000} g CO2eq" + ) + return emissions + except Exception as e: + logger.warning( + f"Error loading Nordic emissions data for {geo.region}: {e}. " + + "Falling back to default emission calculation." + ) + return None + def get_region_emissions(self, energy: Energy, geo: GeoMetadata) -> float: """ Computes emissions for a region on private infra. Given an quantity of power consumed, use regional data on emissions per unit power consumed or the mix of energy sources. https://github.com/responsibleproblemsolving/energy-usage#calculating-co2-emissions + + get_private_infra_emissions + ├─ Electricity Maps API (si token) + ├─ get_region_emissions (USA/CAN/SWE/NOR/FIN) + │ └─ _try_get_nordic_region_emissions (pour SWE/NOR/FIN) + │ └─ country_emissions_data (pour USA) + │ └─ country_energy_mix_data (pour CAN) + └─ get_country_emissions (fallback) + :param energy: Mean power consumption of the process (kWh) :param geo: Country and region metadata. :return: CO2 emissions in kg """ + # Handle Nordic regions (Sweden, Norway, Finland electricity bidding zones) + nordic_emissions = self._try_get_nordic_region_emissions(energy, geo) + if nordic_emissions is not None: + return nordic_emissions + + # Handle USA and Canada regional data try: country_emissions_data = self._data_source.get_country_emissions_data( geo.country_iso_code.lower() diff --git a/tests/test_emissions.py b/tests/test_emissions.py index 7c1b1427a..903a49e1a 100644 --- a/tests/test_emissions.py +++ b/tests/test_emissions.py @@ -186,7 +186,7 @@ def test_get_emissions_PRIVATE_INFRA_NORDIC_REGION(self): # Nordic regions use static emission factors from the JSON file # SE2 has an emission factor specified in nordic_country_energy_mix.json assert isinstance(emissions, float) - assert emissions > 0, "Nordic region emissions should be positive" + self.assertAlmostEqual(emissions, 0.018, places=6) def test_get_emissions_PRIVATE_INFRA_NORDIC_FINLAND(self): # WHEN @@ -200,6 +200,23 @@ def test_get_emissions_PRIVATE_INFRA_NORDIC_FINLAND(self): # THEN # Finland (FI) should use Nordic static emission factors assert isinstance(emissions, float) - assert emissions > 0, "Finland emissions should be positive" - # With 2.5 kWh, emissions should be proportional to energy consumed - assert emissions > 0.1, "Expected reasonable emission value for 2.5 kWh" + expected_emissions = 0.072 * 2.5 + self.assertAlmostEqual(emissions, expected_emissions, places=6) + + def test_get_emissions_PRIVATE_INFRA_NORDIC_REGION_uses_static_factor_without_token( + self, + ): + # GIVEN + energy = Energy.from_energy(kWh=1.0) + geo = GeoMetadata(country_iso_code="SWE", country_name="Sweden", region="SE2") + + # WHEN + emissions = self._emissions.get_private_infra_emissions(energy, geo) + + # THEN + expected_country = self._emissions.get_country_emissions(energy, geo) + nordic_data = self._data_source.get_nordic_country_energy_mix_data() + emission_factor_g = nordic_data["data"]["SE2"]["emission_factor"] + expected_nordic = (emission_factor_g / 1000) * energy.kWh + self.assertAlmostEqual(emissions, expected_nordic, places=6) + self.assertNotAlmostEqual(emissions, expected_country, places=4)