diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index 96ed00c91..631e9ea58 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -55,6 +55,14 @@ _sentinel = object() +# One decimal place, so about 11.1 km at the equator. +_COORDINATE_PRECISION = 1 + + +def _round_coordinate(value: Optional[float]) -> Optional[float]: + """Reduce the precision of a latitude or longitude for privacy.""" + return value if value is None else round(value, _COORDINATE_PRECISION) + class BaseEmissionsTracker(ABC): """ @@ -382,8 +390,11 @@ def _ensure_geo_metadata(self) -> None: self._geo = self._get_geo_metadata() cloud: CloudMetadata = self._get_cloud_metadata() if cloud.is_on_private_infra: - self._conf["longitude"] = self._geo.longitude - self._conf["latitude"] = self._geo.latitude + # Reduce precision for privacy, see docs/reference/output.md. + # self._geo keeps the full precision coordinates, they are needed + # for the Electricity Maps carbon intensity lookup. + self._conf["longitude"] = _round_coordinate(self._geo.longitude) + self._conf["latitude"] = _round_coordinate(self._geo.latitude) def __init__( self, diff --git a/tests/test_emissions_tracker.py b/tests/test_emissions_tracker.py index 8ab12e5d8..d75221cb1 100644 --- a/tests/test_emissions_tracker.py +++ b/tests/test_emissions_tracker.py @@ -1108,3 +1108,76 @@ def test_cumulative_emissions_with_varying_intensity( # Verification: If it wasn't cumulative, it would be 3.0 kWh * 300 g/kWh = 0.9 kg self.assertLess(data3.emissions, 0.8) + + @mock.patch("codecarbon.emissions_tracker.EmissionsTracker._get_geo_metadata") + @mock.patch("codecarbon.emissions_tracker.EmissionsTracker._get_cloud_metadata") + @mock.patch("codecarbon.core.electricitymaps_api.requests.get") + @mock.patch("codecarbon.core.resource_tracker.ResourceTracker") + @mock.patch( + "codecarbon.emissions_tracker.BaseEmissionsTracker.get_detected_hardware" + ) + @mock.patch("codecarbon.emissions_tracker.PeriodicScheduler") + def test_coordinates_precision_is_reduced_in_emissions_data( + self, + mock_scheduler, + mock_get_hw, + mock_resource_tracker, + mock_get, + mock_cloud, + mock_geo, + mock_cli_setup, + mock_log_values, + mocked_get_cloud_metadata_class, + mocked_get_gpu_details, + mocked_get_gpu_utilization_list, + mocked_is_gpu_details_available, + mocked_is_nvidia_system, + ): + mock_geo.return_value = mock.MagicMock( + latitude=48.8566, + longitude=2.3522, + country_iso_code="FRA", + country_2letter_iso_code="FR", + ) + mock_cloud.return_value = mock.MagicMock( + is_on_private_infra=True, provider=None, region=None + ) + mock_get_hw.return_value = { + "ram_total_size": 16.0, + "cpu_count": 8, + "cpu_physical_count": 4, + "cpu_model": "Mock CPU", + "gpu_count": 0, + "gpu_model": "None", + "gpu_ids": None, + } + mock_get.return_value = mock.MagicMock( + status_code=200, json=lambda: {"carbonIntensity": 100} + ) + + tracker = EmissionsTracker( + electricitymaps_api_token="test-token", + save_to_file=False, + measure_power_secs=1, + allow_multiple_runs=True, + ) + + mock_cpu = mock.MagicMock() + from codecarbon.external.hardware import CPU + + mock_cpu.__class__ = CPU + mock_cpu.measure_power_and_energy.return_value = ( + Power.from_watts(100), + Energy.from_energy(kWh=1.0), + ) + tracker._hardware = [mock_cpu] + + tracker.start() + data = tracker._prepare_emissions_data() + + # Every output method gets coordinates reduced to one decimal place. + self.assertEqual(data.latitude, 48.9) + self.assertEqual(data.longitude, 2.4) + # The carbon intensity lookup still uses the full precision coordinates. + self.assertEqual(tracker._geo.latitude, 48.8566) + self.assertEqual(tracker._geo.longitude, 2.3522)