Skip to content

Commit 5a523de

Browse files
google-labs-jules[bot]benoit-cty
authored andcommitted
feat: Allow custom carbon intensity configuration
This commit introduces a new configuration option, `custom_carbon_intensity_g_co2e_kwh`, allowing you to specify a direct carbon intensity value (in gCO2e/kWh) for your energy consumption. When this value is provided (either via the `.codecarbon.config` file or the `CODECARBON_CUSTOM_CARBON_INTENSITY_G_CO2E_KWH` environment variable), it overrides all other methods of determining carbon intensity, including cloud provider data, CO2 Signal API, and default geographical energy mixes. Key changes include: - Modifications to `codecarbon/core/config.py` to recognize the new configuration parameter (though existing mechanisms were largely sufficient). - Updates to `codecarbon/emissions_tracker.py` (specifically `BaseEmissionsTracker`) to read, validate (must be a positive float), and pass the custom intensity to the `Emissions` class. - Updates to `codecarbon/core/emissions.py` to use this custom intensity value in `get_cloud_emissions` and `get_private_infra_emissions` if provided, bypassing other data lookups. - Comprehensive unit tests added to: - `tests/core/test_config.py` for configuration loading. - `tests/test_tracker.py` for validation and initialization logic in `BaseEmissionsTracker`. - `tests/core/test_emissions.py` for the `Emissions` class calculation logic using the custom value and fallback mechanisms. This feature provides you with greater flexibility and accuracy in reporting emissions, especially for on-premise setups with specific energy mixes or when default data sources are not representative.
1 parent e3bb7b1 commit 5a523de

4 files changed

Lines changed: 77 additions & 1 deletion

File tree

codecarbon/core/emissions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def __init__(
2525
co2_signal_api_token: Optional[
2626
str
2727
] = None, # Deprecated, for backward compatibility
28+
custom_carbon_intensity_g_co2e_kwh: Optional[float] = None,
2829
):
2930
self._data_source = data_source
3031

@@ -38,6 +39,7 @@ def __init__(
3839
electricitymaps_api_token = co2_signal_api_token
3940

4041
self._electricitymaps_api_token = electricitymaps_api_token
42+
self._custom_carbon_intensity_g_co2e_kwh = custom_carbon_intensity_g_co2e_kwh
4143

4244
def get_cloud_emissions(
4345
self, energy: Energy, cloud: CloudMetadata, geo: GeoMetadata = None
@@ -50,6 +52,12 @@ def get_cloud_emissions(
5052
:return: CO2 emissions in kg
5153
"""
5254

55+
if self._custom_carbon_intensity_g_co2e_kwh is not None:
56+
logger.info(
57+
f"Using custom carbon intensity for cloud emissions: {self._custom_carbon_intensity_g_co2e_kwh} gCO2e/kWh"
58+
)
59+
return energy.kWh * (self._custom_carbon_intensity_g_co2e_kwh / 1000.0)
60+
5361
df: pd.DataFrame = self._data_source.get_cloud_emissions_data()
5462
try:
5563
emissions_per_kWh: EmissionsPerKWh = EmissionsPerKWh.from_g_per_kWh(
@@ -138,6 +146,12 @@ def get_private_infra_emissions(self, energy: Energy, geo: GeoMetadata) -> float
138146
:param geo: Country and region metadata
139147
:return: CO2 emissions in kg
140148
"""
149+
if self._custom_carbon_intensity_g_co2e_kwh is not None:
150+
logger.info(
151+
f"Using custom carbon intensity for private infrastructure emissions: {self._custom_carbon_intensity_g_co2e_kwh} gCO2e/kWh"
152+
)
153+
return energy.kWh * (self._custom_carbon_intensity_g_co2e_kwh / 1000.0)
154+
141155
if self._electricitymaps_api_token:
142156
try:
143157
emissions = electricitymaps_api.get_emissions(

codecarbon/emissions_tracker.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,33 @@ def __init__(
276276

277277
# logger.info("base tracker init")
278278
self._external_conf = get_hierarchical_config()
279+
custom_intensity_str = self._external_conf.get(
280+
"custom_carbon_intensity_g_co2e_kwh"
281+
)
282+
parsed_intensity = None
283+
if custom_intensity_str is not None:
284+
custom_intensity_str_stripped = custom_intensity_str.strip()
285+
if custom_intensity_str_stripped == "":
286+
logger.warning(
287+
f"CODECARBON : Invalid value for custom_carbon_intensity_g_co2e_kwh: '{custom_intensity_str}'. "
288+
"It cannot be empty or whitespace. Using default calculation methods."
289+
)
290+
else:
291+
try:
292+
value = float(custom_intensity_str_stripped)
293+
if value > 0:
294+
parsed_intensity = value
295+
else:
296+
logger.warning(
297+
f"CODECARBON : Invalid value for custom_carbon_intensity_g_co2e_kwh: '{custom_intensity_str_stripped}'. "
298+
"It must be a positive number. Using default calculation methods."
299+
)
300+
except ValueError:
301+
logger.warning(
302+
f"CODECARBON : Invalid value for custom_carbon_intensity_g_co2e_kwh: '{custom_intensity_str_stripped}'. "
303+
"It must be a numeric value. Using default calculation methods."
304+
)
305+
self.custom_carbon_intensity_g_co2e_kwh = parsed_intensity
279306
self._set_from_conf(allow_multiple_runs, "allow_multiple_runs", True, bool)
280307
if self._allow_multiple_runs:
281308
logger.warning(
@@ -353,6 +380,11 @@ def __init__(
353380
experiment_id, "experiment_id", "5b0fa12a-3dd7-45bb-9766-cc326314d9f1"
354381
)
355382

383+
if self.custom_carbon_intensity_g_co2e_kwh is not None:
384+
logger.info(
385+
f"CODECARBON : Using custom carbon intensity: {self.custom_carbon_intensity_g_co2e_kwh} gCO2e/kWh."
386+
)
387+
356388
assert self._tracking_mode in ["machine", "process"]
357389
set_logger_level(self._log_level)
358390
set_logger_format(self._logger_preamble)
@@ -446,7 +478,9 @@ def __init__(
446478
self._conf["provider"] = cloud.provider
447479

448480
self._emissions: Emissions = Emissions(
449-
self._data_source, self._electricitymaps_api_token
481+
self._data_source,
482+
self._electricitymaps_api_token,
483+
custom_carbon_intensity_g_co2e_kwh=self.custom_carbon_intensity_g_co2e_kwh,
450484
)
451485
self._init_output_methods(api_key=self._api_key)
452486

tests/test_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ def test_full_hierarchy(self):
199199
force_ram_power=50.5
200200
output_dir=ERROR:not overwritten
201201
save_to_file=ERROR:not overwritten
202+
custom_carbon_intensity_g_co2e_kwh=123.4
202203
"""
203204
)
204205
local_conf = dedent(
@@ -225,6 +226,7 @@ def test_full_hierarchy(self):
225226
self.assertEqual(tracker._emissions_endpoint, "http://testhost:2000")
226227
self.assertEqual(tracker._gpu_ids, ["0", "1"])
227228
self.assertEqual(tracker._electricitymaps_api_token, "signal-token")
229+
self.assertEqual(tracker.custom_carbon_intensity_g_co2e_kwh, 123.4)
228230
self.assertEqual(tracker._project_name, "test-project")
229231
self.assertTrue(tracker._save_to_file)
230232

tests/test_emissions.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,32 @@ def test_get_emissions_PRIVATE_INFRA_unknown_country(self):
174174
assert isinstance(emissions, float)
175175
self.assertAlmostEqual(emissions, 0.475, places=2)
176176

177+
@patch("codecarbon.core.electricitymaps_api.get_emissions")
178+
def test_private_infra_uses_custom_intensity_when_set(self, mocked_get_emissions):
179+
emissions_calculator = Emissions(
180+
self._data_source, custom_carbon_intensity_g_co2e_kwh=50.0
181+
)
182+
183+
emissions = emissions_calculator.get_private_infra_emissions(
184+
Energy.from_energy(kWh=2),
185+
GeoMetadata(country_iso_code="CAN", country_name="Canada"),
186+
)
187+
188+
self.assertAlmostEqual(emissions, 0.1, places=6)
189+
mocked_get_emissions.assert_not_called()
190+
191+
def test_cloud_uses_custom_intensity_when_set(self):
192+
emissions_calculator = Emissions(
193+
self._data_source, custom_carbon_intensity_g_co2e_kwh=100.0
194+
)
195+
196+
emissions = emissions_calculator.get_cloud_emissions(
197+
Energy.from_energy(kWh=2),
198+
CloudMetadata(provider="aws", region="us-east-1"),
199+
)
200+
201+
self.assertAlmostEqual(emissions, 0.2, places=6)
202+
177203
def test_get_emissions_PRIVATE_INFRA_NORDIC_REGION(self):
178204
# WHEN
179205
# Test Nordic region (Sweden SE2)

0 commit comments

Comments
 (0)