diff --git a/codecarbon/core/emissions.py b/codecarbon/core/emissions.py index 78845f0d8..81eaada7c 100644 --- a/codecarbon/core/emissions.py +++ b/codecarbon/core/emissions.py @@ -155,6 +155,49 @@ 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"] ) 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 diff --git a/codecarbon/input.py b/codecarbon/input.py index 23ede6157..68f3d3b17 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_emissions.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 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"