Skip to content

Commit 500c756

Browse files
authored
Merge pull request #863 from mlco2/feat/custom-carbon-intensity
Allow custom carbon intensity configuration
2 parents 164ffcb + 1b0fba9 commit 500c756

5 files changed

Lines changed: 186 additions & 4 deletions

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+
force_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._force_carbon_intensity_g_co2e_kwh = force_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._force_carbon_intensity_g_co2e_kwh is not None:
56+
logger.info(
57+
f"Using forced carbon intensity for cloud emissions: {self._force_carbon_intensity_g_co2e_kwh} gCO2e/kWh"
58+
)
59+
return energy.kWh * (self._force_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._force_carbon_intensity_g_co2e_kwh is not None:
150+
logger.debug(
151+
f"Using forced carbon intensity for private infrastructure emissions: {self._force_carbon_intensity_g_co2e_kwh} gCO2e/kWh"
152+
)
153+
return energy.kWh * (self._force_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: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def __init__(
195195
force_ram_power: Optional[int] = _sentinel,
196196
pue: Optional[float] = _sentinel,
197197
wue: Optional[float] = _sentinel,
198+
force_carbon_intensity_g_co2e_kwh: Optional[float] = _sentinel,
198199
force_mode_cpu_load: Optional[bool] = _sentinel,
199200
allow_multiple_runs: Optional[bool] = _sentinel,
200201
rapl_include_dram: Optional[bool] = _sentinel,
@@ -259,11 +260,13 @@ def __init__(
259260
then RAM power (W) = Number of RAM Slots × 5 Watts.
260261
:param pue: PUE (Power Usage Effectiveness) of the data center where the
261262
experiment is being run.
263+
:param wue: WUE (Water Usage Effectiveness) of the data center. Units of L/kWh:
264+
litres of water consumed per kilowatt-hour of electricity consumed.
265+
:param force_carbon_intensity_g_co2e_kwh: Override grid carbon intensity
266+
in gCO2e/kWh for emissions calculations.
262267
:param force_mode_cpu_load: Force the addition of a CPU in MODE_CPU_LOAD
263268
:param allow_multiple_runs: Allow multiple CodeCarbon instances on the same machine.
264269
Defaults to True since v3 (was False in v2).
265-
:param wue: WUE (Water Usage Effectiveness) of the data center. Units of L/kWh:
266-
litres of water consumed per kilowatt-hour of electricity consumed.
267270
:param rapl_include_dram: Include DRAM (memory) power in RAPL measurements on Linux,
268271
defaults to False. When True, measures CPU package + DRAM.
269272
Only affects systems where RAPL exposes separate DRAM domains.
@@ -276,6 +279,31 @@ def __init__(
276279

277280
# logger.info("base tracker init")
278281
self._external_conf = get_hierarchical_config()
282+
self._set_from_conf(
283+
force_carbon_intensity_g_co2e_kwh,
284+
"force_carbon_intensity_g_co2e_kwh",
285+
None,
286+
float,
287+
)
288+
parsed_intensity = None
289+
if self._force_carbon_intensity_g_co2e_kwh is not None:
290+
try:
291+
value = float(self._force_carbon_intensity_g_co2e_kwh)
292+
if value >= 0:
293+
parsed_intensity = value
294+
else:
295+
logger.warning(
296+
f"Invalid value for force_carbon_intensity_g_co2e_kwh: '{self._force_carbon_intensity_g_co2e_kwh}'. "
297+
"It must be a non-negative number. Using default calculation methods."
298+
)
299+
except (ValueError, TypeError):
300+
logger.warning(
301+
f"Invalid value for force_carbon_intensity_g_co2e_kwh: '{self._force_carbon_intensity_g_co2e_kwh}'. "
302+
"It must be a numeric value. Using default calculation methods."
303+
)
304+
self._force_carbon_intensity_g_co2e_kwh = parsed_intensity
305+
self._conf["force_carbon_intensity_g_co2e_kwh"] = parsed_intensity
306+
self.force_carbon_intensity_g_co2e_kwh = parsed_intensity
279307
self._set_from_conf(allow_multiple_runs, "allow_multiple_runs", True, bool)
280308
if self._allow_multiple_runs:
281309
logger.warning(
@@ -353,6 +381,11 @@ def __init__(
353381
experiment_id, "experiment_id", "5b0fa12a-3dd7-45bb-9766-cc326314d9f1"
354382
)
355383

384+
if self.force_carbon_intensity_g_co2e_kwh is not None:
385+
logger.info(
386+
f"Using forced carbon intensity: {self.force_carbon_intensity_g_co2e_kwh} gCO2e/kWh."
387+
)
388+
356389
assert self._tracking_mode in ["machine", "process"]
357390
set_logger_level(self._log_level)
358391
set_logger_format(self._logger_preamble)
@@ -446,7 +479,9 @@ def __init__(
446479
self._conf["provider"] = cloud.provider
447480

448481
self._emissions: Emissions = Emissions(
449-
self._data_source, self._electricitymaps_api_token
482+
self._data_source,
483+
self._electricitymaps_api_token,
484+
force_carbon_intensity_g_co2e_kwh=self.force_carbon_intensity_g_co2e_kwh,
450485
)
451486
self._init_output_methods(api_key=self._api_key)
452487

@@ -1310,6 +1345,7 @@ def track_emissions(
13101345
force_ram_power: Optional[int] = _sentinel,
13111346
pue: Optional[float] = _sentinel,
13121347
wue: Optional[float] = _sentinel,
1348+
force_carbon_intensity_g_co2e_kwh: Optional[float] = _sentinel,
13131349
allow_multiple_runs: Optional[bool] = _sentinel,
13141350
rapl_include_dram: Optional[bool] = _sentinel,
13151351
rapl_prefer_psys: Optional[bool] = _sentinel,
@@ -1392,6 +1428,8 @@ def track_emissions(
13921428
:param pue: PUE (Power Usage Effectiveness) of the data center.
13931429
:param wue: WUE (Water Usage Effectiveness) of the data center. Units of L/kWh:
13941430
litres of water consumed per kilowatt-hour of electricity consumed.
1431+
:param force_carbon_intensity_g_co2e_kwh: Override grid carbon intensity
1432+
in gCO2e/kWh for emissions calculations.
13951433
:param rapl_include_dram: Include DRAM in RAPL measurements on Linux (default: False).
13961434
When True, measures CPU package + DRAM.
13971435
:param rapl_prefer_psys: Prefer psys over package domains for RAPL on Linux
@@ -1447,6 +1485,7 @@ def wrapped_fn(*args, **kwargs):
14471485
force_ram_power=force_ram_power,
14481486
pue=pue,
14491487
wue=wue,
1488+
force_carbon_intensity_g_co2e_kwh=force_carbon_intensity_g_co2e_kwh,
14501489
allow_multiple_runs=allow_multiple_runs,
14511490
rapl_include_dram=rapl_include_dram,
14521491
rapl_prefer_psys=rapl_prefer_psys,
@@ -1481,6 +1520,7 @@ def wrapped_fn(*args, **kwargs):
14811520
force_ram_power=force_ram_power,
14821521
pue=pue,
14831522
wue=wue,
1523+
force_carbon_intensity_g_co2e_kwh=force_carbon_intensity_g_co2e_kwh,
14841524
allow_multiple_runs=allow_multiple_runs,
14851525
rapl_include_dram=rapl_include_dram,
14861526
rapl_prefer_psys=rapl_prefer_psys,

examples/pue.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
@track_emissions(
77
measure_power_secs=3,
88
pue=2,
9+
force_carbon_intensity_g_co2e_kwh=1,
910
log_level="DEBUG",
1011
)
1112
def train_model():

tests/test_config.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
parse_env_config,
1212
parse_gpu_ids,
1313
)
14-
from codecarbon.emissions_tracker import EmissionsTracker
14+
from codecarbon.emissions_tracker import (
15+
EmissionsTracker,
16+
OfflineEmissionsTracker,
17+
track_emissions,
18+
)
1519
from codecarbon.external.hardware import GPU
1620
from tests.testutils import get_custom_mock_open
1721

@@ -199,6 +203,7 @@ def test_full_hierarchy(self):
199203
force_ram_power=50.5
200204
output_dir=ERROR:not overwritten
201205
save_to_file=ERROR:not overwritten
206+
force_carbon_intensity_g_co2e_kwh=123.4
202207
"""
203208
)
204209
local_conf = dedent(
@@ -225,9 +230,105 @@ def test_full_hierarchy(self):
225230
self.assertEqual(tracker._emissions_endpoint, "http://testhost:2000")
226231
self.assertEqual(tracker._gpu_ids, ["0", "1"])
227232
self.assertEqual(tracker._electricitymaps_api_token, "signal-token")
233+
self.assertEqual(tracker.force_carbon_intensity_g_co2e_kwh, 123.4)
228234
self.assertEqual(tracker._project_name, "test-project")
229235
self.assertTrue(tracker._save_to_file)
230236

237+
def test_force_carbon_intensity_constructor_overrides_config(self):
238+
global_conf = dedent(
239+
"""\
240+
[codecarbon]
241+
force_carbon_intensity_g_co2e_kwh=123.4
242+
"""
243+
)
244+
245+
with patch("builtins.open", new_callable=get_custom_mock_open(global_conf, "")):
246+
with patch("os.path.exists", return_value=True):
247+
tracker = EmissionsTracker(
248+
force_carbon_intensity_g_co2e_kwh=456.7,
249+
save_to_file=False,
250+
allow_multiple_runs=True,
251+
)
252+
253+
self.assertEqual(tracker.force_carbon_intensity_g_co2e_kwh, 456.7)
254+
self.assertEqual(tracker._conf["force_carbon_intensity_g_co2e_kwh"], 456.7)
255+
256+
def test_offline_tracker_accepts_force_carbon_intensity_parameter(self):
257+
with patch("builtins.open", new_callable=get_custom_mock_open("", "")):
258+
with patch("os.path.exists", return_value=True):
259+
tracker = OfflineEmissionsTracker(
260+
country_iso_code="FRA",
261+
force_carbon_intensity_g_co2e_kwh=0,
262+
save_to_file=False,
263+
allow_multiple_runs=True,
264+
)
265+
266+
self.assertEqual(tracker.force_carbon_intensity_g_co2e_kwh, 0.0)
267+
268+
def test_force_carbon_intensity_rejects_negative_parameter(self):
269+
with patch("builtins.open", new_callable=get_custom_mock_open("", "")):
270+
with patch("os.path.exists", return_value=True):
271+
tracker = EmissionsTracker(
272+
force_carbon_intensity_g_co2e_kwh=-1,
273+
save_to_file=False,
274+
allow_multiple_runs=True,
275+
)
276+
277+
self.assertIsNone(tracker.force_carbon_intensity_g_co2e_kwh)
278+
self.assertIsNone(tracker._conf["force_carbon_intensity_g_co2e_kwh"])
279+
280+
def test_force_carbon_intensity_rejects_non_numeric_parameter(self):
281+
with patch("builtins.open", new_callable=get_custom_mock_open("", "")):
282+
with patch("os.path.exists", return_value=True):
283+
tracker = EmissionsTracker(
284+
force_carbon_intensity_g_co2e_kwh="invalid",
285+
save_to_file=False,
286+
allow_multiple_runs=True,
287+
)
288+
289+
self.assertIsNone(tracker.force_carbon_intensity_g_co2e_kwh)
290+
self.assertIsNone(tracker._conf["force_carbon_intensity_g_co2e_kwh"])
291+
292+
def test_track_emissions_forwards_force_carbon_intensity_parameter(self):
293+
with patch("codecarbon.emissions_tracker.EmissionsTracker") as tracker_class:
294+
295+
@track_emissions(
296+
force_carbon_intensity_g_co2e_kwh=321.0,
297+
save_to_file=False,
298+
)
299+
def tracked_function():
300+
return "success"
301+
302+
self.assertEqual(tracked_function(), "success")
303+
304+
tracker_class.assert_called_once()
305+
self.assertEqual(
306+
tracker_class.call_args.kwargs["force_carbon_intensity_g_co2e_kwh"],
307+
321.0,
308+
)
309+
310+
def test_track_emissions_forwards_force_carbon_intensity_to_offline_tracker(self):
311+
with patch(
312+
"codecarbon.emissions_tracker.OfflineEmissionsTracker"
313+
) as tracker_class:
314+
315+
@track_emissions(
316+
offline=True,
317+
country_iso_code="FRA",
318+
force_carbon_intensity_g_co2e_kwh=321.0,
319+
save_to_file=False,
320+
)
321+
def tracked_function():
322+
return "success"
323+
324+
self.assertEqual(tracked_function(), "success")
325+
326+
tracker_class.assert_called_once()
327+
self.assertEqual(
328+
tracker_class.call_args.kwargs["force_carbon_intensity_g_co2e_kwh"],
329+
321.0,
330+
)
331+
231332
@mock.patch.dict(
232333
os.environ,
233334
{

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_forced_intensity_when_set(self, mocked_get_emissions):
179+
emissions_calculator = Emissions(
180+
self._data_source, force_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_forced_intensity_when_set(self):
192+
emissions_calculator = Emissions(
193+
self._data_source, force_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)