Skip to content

Commit e38dfb3

Browse files
authored
Merge pull request #1050 from mlco2/feature/issue-994
Compute emissions based on ongoing carbon intensity
2 parents 3242f27 + cc62d32 commit e38dfb3

2 files changed

Lines changed: 118 additions & 6 deletions

File tree

codecarbon/emissions_tracker.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,8 @@ def __init__(
337337
self._start_time: Optional[float] = None
338338
self._last_measured_time: float = time.perf_counter()
339339
self._total_energy: Energy = Energy.from_energy(kWh=0)
340+
self._total_emissions: float = 0.0
341+
self._last_energy_covered: Energy = Energy.from_energy(kWh=0)
340342
self._total_water: Water = Water.from_litres(litres=0)
341343
# CPU and RAM utilization tracking
342344
self._cpu_utilization_history: List[float] = []
@@ -757,28 +759,43 @@ def _persist_data(
757759
if len(task_emissions_data) > 0:
758760
handler.task_out(task_emissions_data, experiment_name)
759761

762+
def _update_emissions(self) -> None:
763+
"""
764+
Compute emissions for the energy consumed since the last update
765+
and add them to the total emissions.
766+
"""
767+
delta_energy = self._total_energy - self._last_energy_covered
768+
if delta_energy.kWh > 0:
769+
cloud: CloudMetadata = self._get_cloud_metadata()
770+
if cloud.is_on_private_infra:
771+
delta_emissions = self._emissions.get_private_infra_emissions(
772+
delta_energy, self._geo
773+
)
774+
else:
775+
delta_emissions = self._emissions.get_cloud_emissions(
776+
delta_energy, cloud, self._geo
777+
)
778+
self._total_emissions += delta_emissions
779+
self._last_energy_covered = self._total_energy
780+
760781
def _prepare_emissions_data(self) -> EmissionsData:
761782
"""
762783
Prepare the emissions data to be sent to the API or written to a file.
763784
:return: EmissionsData object with the total emissions data.
764785
"""
786+
self._update_emissions()
765787
cloud: CloudMetadata = self._get_cloud_metadata()
766788
duration: Time = Time.from_seconds(time.perf_counter() - self._start_time)
767789

790+
emissions = self._total_emissions
768791
if cloud.is_on_private_infra:
769-
emissions = self._emissions.get_private_infra_emissions(
770-
self._total_energy, self._geo
771-
) # float: kg co2_eq
772792
country_name = self._geo.country_name
773793
country_iso_code = self._geo.country_iso_code
774794
region = self._geo.region
775795
on_cloud = "N"
776796
cloud_provider = ""
777797
cloud_region = ""
778798
else:
779-
emissions = self._emissions.get_cloud_emissions(
780-
self._total_energy, cloud, self._geo
781-
)
782799
# Try to get cloud region metadata, fall back to geo metadata if not found
783800
try:
784801
country_name = self._emissions.get_cloud_country_name(cloud)

tests/test_emissions_tracker.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,3 +656,98 @@ def test_get_detected_hardware(
656656
self.assertIn("gpu_count", hardware_info)
657657
self.assertIn("gpu_model", hardware_info)
658658
self.assertIn("gpu_ids", hardware_info)
659+
660+
@mock.patch("codecarbon.emissions_tracker.EmissionsTracker._get_geo_metadata")
661+
@mock.patch("codecarbon.emissions_tracker.EmissionsTracker._get_cloud_metadata")
662+
@mock.patch("codecarbon.core.electricitymaps_api.requests.get")
663+
@mock.patch("codecarbon.emissions_tracker.ResourceTracker")
664+
@mock.patch(
665+
"codecarbon.emissions_tracker.BaseEmissionsTracker.get_detected_hardware"
666+
)
667+
@mock.patch("codecarbon.emissions_tracker.PeriodicScheduler")
668+
def test_cumulative_emissions_with_varying_intensity(
669+
self,
670+
mock_scheduler,
671+
mock_get_hw,
672+
mock_resource_tracker,
673+
mock_get,
674+
mock_cloud,
675+
mock_geo,
676+
mock_cli_setup,
677+
mock_log_values,
678+
mocked_get_cloud_metadata_class,
679+
mocked_get_gpu_details,
680+
mocked_is_gpu_details_available,
681+
):
682+
# Setup mocks
683+
mock_geo.return_value = mock.MagicMock(
684+
latitude=1.0,
685+
longitude=1.0,
686+
country_iso_code="USA",
687+
country_2letter_iso_code="US",
688+
)
689+
mock_cloud.return_value = mock.MagicMock(
690+
is_on_private_infra=True, provider="azure", region="eastus"
691+
)
692+
mock_get_hw.return_value = {
693+
"ram_total_size": 16.0,
694+
"cpu_count": 8,
695+
"cpu_physical_count": 4,
696+
"cpu_model": "Mock CPU",
697+
"gpu_count": 0,
698+
"gpu_model": "None",
699+
"gpu_ids": None,
700+
}
701+
702+
# Mock Electricity Maps API responses with different intensities
703+
# 1st call: 100 g/kWh, 2nd call: 200 g/kWh, 3rd call: 300 g/kWh
704+
responses = [
705+
mock.MagicMock(status_code=200, json=lambda: {"carbonIntensity": 100}),
706+
mock.MagicMock(status_code=200, json=lambda: {"carbonIntensity": 200}),
707+
mock.MagicMock(status_code=200, json=lambda: {"carbonIntensity": 300}),
708+
]
709+
mock_get.side_effect = responses
710+
711+
tracker = EmissionsTracker(
712+
electricitymaps_api_token="test-token",
713+
save_to_file=False,
714+
measure_power_secs=1,
715+
allow_multiple_runs=True,
716+
)
717+
718+
# Manually inject a mock hardware component
719+
mock_cpu = mock.MagicMock()
720+
from codecarbon.external.hardware import CPU
721+
722+
mock_cpu.__class__ = CPU
723+
# Mock measure_power_and_energy: return 1kWh delta each time
724+
mock_cpu.measure_power_and_energy.return_value = (
725+
Power.from_watts(100),
726+
Energy.from_energy(kWh=1.0),
727+
)
728+
tracker._hardware = [mock_cpu]
729+
730+
# Start tracking
731+
tracker.start()
732+
733+
tracker._measure_power_and_energy()
734+
# total_energy = 1.0, intensity = 100 => emissions = 0.1 kg
735+
data1 = tracker._prepare_emissions_data()
736+
self.assertAlmostEqual(data1.emissions, 0.1)
737+
738+
# Step 2
739+
tracker._measure_power_and_energy()
740+
# total_energy = 2.0, delta_energy = 1.0, intensity = 200 => delta_emissions = 0.2 kg
741+
# total_emissions = 0.3 kg
742+
data2 = tracker._prepare_emissions_data()
743+
self.assertAlmostEqual(data2.emissions, 0.3)
744+
745+
# Step 3
746+
tracker._measure_power_and_energy()
747+
# total_energy = 3.0, delta_energy = 1.0, intensity = 300 => delta_emissions = 0.3 kg
748+
# total_emissions = 0.6 kg
749+
data3 = tracker._prepare_emissions_data()
750+
self.assertAlmostEqual(data3.emissions, 0.6)
751+
752+
# Verification: If it wasn't cumulative, it would be 3.0 kWh * 300 g/kWh = 0.9 kg
753+
self.assertLess(data3.emissions, 0.8)

0 commit comments

Comments
 (0)