codecarbon 3.0.4__tar.gz → 3.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {codecarbon-3.0.4 → codecarbon-3.0.5}/PKG-INFO +1 -1
  2. codecarbon-3.0.5/codecarbon/_version.py +1 -0
  3. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/config.py +21 -12
  4. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/resource_tracker.py +3 -10
  5. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/data/hardware/cpu_power.csv +2 -0
  6. codecarbon-3.0.5/codecarbon/data/private_infra/2023/canada_energy_mix.json +171 -0
  7. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/emissions_tracker.py +1 -1
  8. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/external/hardware.py +34 -16
  9. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/input.py +1 -1
  10. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/output_methods/file.py +6 -2
  11. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon.egg-info/PKG-INFO +1 -1
  12. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon.egg-info/SOURCES.txt +1 -1
  13. {codecarbon-3.0.4 → codecarbon-3.0.5}/pyproject.toml +2 -2
  14. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_config.py +14 -14
  15. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_cpu_load.py +4 -2
  16. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_emissions.py +4 -1
  17. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_emissions_tracker.py +88 -0
  18. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_gpu.py +33 -6
  19. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_unsupported_gpu.py +5 -0
  20. codecarbon-3.0.4/codecarbon/_version.py +0 -1
  21. codecarbon-3.0.4/codecarbon/data/private_infra/2016/canada_energy_mix.json +0 -171
  22. {codecarbon-3.0.4 → codecarbon-3.0.5}/LICENSE +0 -0
  23. {codecarbon-3.0.4 → codecarbon-3.0.5}/README.md +0 -0
  24. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/__init__.py +0 -0
  25. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/cli/__init__.py +0 -0
  26. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/cli/cli_utils.py +0 -0
  27. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/cli/main.py +0 -0
  28. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/__init__.py +0 -0
  29. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/api_client.py +0 -0
  30. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/cloud.py +0 -0
  31. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/co2_signal.py +0 -0
  32. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/cpu.py +0 -0
  33. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/emissions.py +0 -0
  34. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/gpu.py +0 -0
  35. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/measure.py +0 -0
  36. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/powermetrics.py +0 -0
  37. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/rapl.py +0 -0
  38. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/schemas.py +0 -0
  39. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/units.py +0 -0
  40. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/core/util.py +0 -0
  41. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/data/canada_provinces.geojson +0 -0
  42. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/data/cloud/impact.csv +0 -0
  43. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/data/hardware/cpu_dataset_builder/amd_cpu_scrapper.py +0 -0
  44. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/data/hardware/cpu_dataset_builder/intel_cpu_scrapper.py +0 -0
  45. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/data/hardware/cpu_dataset_builder/merge_scrapped_cpu_power.py +0 -0
  46. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/data/private_infra/2016/usa_emissions.json +0 -0
  47. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/data/private_infra/carbon_intensity_per_source.json +0 -0
  48. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/data/private_infra/global_energy_mix.json +0 -0
  49. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/external/__init__.py +0 -0
  50. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/external/geography.py +0 -0
  51. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/external/logger.py +0 -0
  52. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/external/ram.py +0 -0
  53. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/external/scheduler.py +0 -0
  54. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/external/task.py +0 -0
  55. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/lock.py +0 -0
  56. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/output.py +0 -0
  57. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/output_methods/__init__.py +0 -0
  58. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/output_methods/base_output.py +0 -0
  59. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/output_methods/emissions_data.py +0 -0
  60. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/output_methods/http.py +0 -0
  61. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/output_methods/logger.py +0 -0
  62. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/output_methods/metrics/__init__.py +0 -0
  63. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/output_methods/metrics/logfire.py +0 -0
  64. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/output_methods/metrics/metric_docs.py +0 -0
  65. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/output_methods/metrics/prometheus.py +0 -0
  66. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/viz/__init__.py +0 -0
  67. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/viz/assets/__init__.py +0 -0
  68. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/viz/carbonboard.py +0 -0
  69. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/viz/carbonboard_on_api.py +0 -0
  70. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/viz/components.py +0 -0
  71. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/viz/data.py +0 -0
  72. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon/viz/units.py +0 -0
  73. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon.egg-info/dependency_links.txt +0 -0
  74. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon.egg-info/entry_points.txt +0 -0
  75. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon.egg-info/requires.txt +0 -0
  76. {codecarbon-3.0.4 → codecarbon-3.0.5}/codecarbon.egg-info/top_level.txt +0 -0
  77. {codecarbon-3.0.4 → codecarbon-3.0.5}/setup.cfg +0 -0
  78. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_api_call.py +0 -0
  79. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_cli.py +0 -0
  80. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_cloud.py +0 -0
  81. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_co2_signal.py +0 -0
  82. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_core_util.py +0 -0
  83. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_cpu.py +0 -0
  84. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_custom_handler.py +0 -0
  85. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_emissions_tracker_constant.py +0 -0
  86. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_emissions_tracker_flush.py +0 -0
  87. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_energy.py +0 -0
  88. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_geography.py +0 -0
  89. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_lock.py +0 -0
  90. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_logging_output.py +0 -0
  91. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_offline_emissions_tracker.py +0 -0
  92. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_package_integrity.py +0 -0
  93. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_powermetrics.py +0 -0
  94. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_ram.py +0 -0
  95. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_tracking_inference.py +0 -0
  96. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_viz_data.py +0 -0
  97. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/test_viz_units.py +0 -0
  98. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/testdata.py +0 -0
  99. {codecarbon-3.0.4 → codecarbon-3.0.5}/tests/testutils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecarbon
3
- Version: 3.0.4
3
+ Version: 3.0.5
4
4
  Author: Mila, DataForGood, BCG GAMMA, Comet.ml, Haverford College
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://codecarbon.io/
@@ -0,0 +1 @@
1
+ __version__ = "3.0.5"
@@ -1,7 +1,7 @@
1
1
  import configparser
2
2
  import os
3
3
  from pathlib import Path
4
- from typing import List
4
+ from typing import List, Union
5
5
 
6
6
  from codecarbon.external.logger import logger
7
7
 
@@ -44,24 +44,33 @@ def parse_env_config() -> dict:
44
44
  }
45
45
 
46
46
 
47
- def parse_gpu_ids(gpu_ids_str: str) -> List[int]:
47
+ def parse_gpu_ids(gpu_ids: Union[str, List[int]]) -> List[str]:
48
48
  """
49
- Transforms the potential gpu_ids string into a list of int values
49
+ Transforms the potential gpu_ids into a list of string id values.
50
+
50
51
 
51
52
  Args:
52
- gpu_ids_str (str): The config file or environment variable value for `gpu_ids`
53
- which is read as a string and should be parsed into a list of ints
53
+ gpu_ids: The config file or environment variable value for `gpu_ids`
54
54
 
55
55
  Returns:
56
- list[int]: The list of GPU ids available declared by the user.
56
+ list[str]: The list of GPU ids available.
57
57
  Potentially empty.
58
58
  """
59
- if not isinstance(gpu_ids_str, str):
60
- return gpu_ids_str
61
-
62
- gpu_ids_str = "".join(c for c in gpu_ids_str if (c.isalnum() or c == ","))
63
- str_ids = [gpu_id for gpu_id in gpu_ids_str.split(",") if gpu_id]
64
- return list(map(int, str_ids))
59
+ if isinstance(gpu_ids, str):
60
+ # Allow '-' in id strings since UUIDs may include them.
61
+ gpu_ids = "".join(c for c in gpu_ids if (c.isalnum() or c in ("-", ",")))
62
+ str_ids = [gpu_id for gpu_id in gpu_ids.split(",") if gpu_id]
63
+ return str_ids
64
+
65
+ elif isinstance(gpu_ids, list) and all(
66
+ isinstance(gpu_id, int) for gpu_id in gpu_ids
67
+ ):
68
+ return list(map(str, gpu_ids))
69
+
70
+ else:
71
+ logger.warning(
72
+ "Invalid gpu_ids format. Expected a string or a list of ints/strings."
73
+ )
65
74
 
66
75
 
67
76
  def get_hierarchical_config():
@@ -183,18 +183,11 @@ class ResourceTracker:
183
183
  def set_GPU_tracking(self):
184
184
  logger.info("[setup] GPU Tracking...")
185
185
  if self.tracker._gpu_ids:
186
- # If _gpu_ids is a string or a list of int, parse it to a list of ints
187
- if isinstance(self.tracker._gpu_ids, str) or (
188
- isinstance(self.tracker._gpu_ids, list)
189
- and all(isinstance(gpu_id, int) for gpu_id in self.tracker._gpu_ids)
190
- ):
191
- self.tracker._gpu_ids: List[int] = parse_gpu_ids(self.tracker._gpu_ids)
186
+ self.tracker._gpu_ids = parse_gpu_ids(self.tracker._gpu_ids)
187
+ if self.tracker._gpu_ids:
192
188
  self.tracker._conf["gpu_ids"] = self.tracker._gpu_ids
193
189
  self.tracker._conf["gpu_count"] = len(self.tracker._gpu_ids)
194
- else:
195
- logger.warning(
196
- "Invalid gpu_ids format. Expected a string or a list of ints."
197
- )
190
+
198
191
  if gpu.is_gpu_details_available():
199
192
  logger.info("Tracking Nvidia GPU via pynvml")
200
193
  gpu_devices = GPU.from_utils(self.tracker._gpu_ids)
@@ -1538,6 +1538,7 @@ AMD Turion X2 Ultra ZM-88,35
1538
1538
  AMD X940,45.0
1539
1539
  AMD Z-01,6
1540
1540
  AMD Z-60,5
1541
+ ARMv8 Processor rev 0 (v8l),30
1541
1542
  Apple M1,10
1542
1543
  Athlon 5150 APU,25
1543
1544
  Athlon 5350 APU,25
@@ -3111,6 +3112,7 @@ Intel Core i9-11900K,125.0
3111
3112
  Intel Core i9-11900KF,125.0
3112
3113
  Intel Core i9-11900T,35.0
3113
3114
  Intel Core i9-13900E,65.0
3115
+ Intel Core i9-13900K,125.0
3114
3116
  Intel Core i9-13900TE,35.0
3115
3117
  Intel Core i9-14900HX,55.0
3116
3118
  Intel Core i9-7900X,140
@@ -0,0 +1,171 @@
1
+ {
2
+ "british columbia": {
3
+ "naturalGas": 2.8,
4
+ "nuclear": 0.0,
5
+ "petroleum": 0.1,
6
+ "biomass": 6.7,
7
+ "coal": 0.0,
8
+ "other": 0.0,
9
+ "solar": 0.6,
10
+ "tidal": 0.0,
11
+ "total": 99.9,
12
+ "wind": 2.9,
13
+ "hydro": 86.8
14
+ },
15
+ "newfoundland and labrador": {
16
+ "naturalGas": 0.2,
17
+ "nuclear": 0.0,
18
+ "petroleum": 0.4,
19
+ "biomass": 0.0,
20
+ "coal": 0.0,
21
+ "other": 0.0,
22
+ "solar": 0.0,
23
+ "tidal": 0.0,
24
+ "total": 100.0,
25
+ "wind": 0.4,
26
+ "hydro": 99.0
27
+ },
28
+ "saskatchewan": {
29
+ "naturalGas": 50.7,
30
+ "nuclear": 0.0,
31
+ "petroleum": 0.0,
32
+ "biomass": 1.2,
33
+ "coal": 27.9,
34
+ "other": 0.0,
35
+ "solar": 0.6,
36
+ "tidal": 0.0,
37
+ "total": 100.0,
38
+ "wind": 3.0,
39
+ "hydro": 16.6
40
+ },
41
+ "prince edward island": {
42
+ "naturalGas": 0.0,
43
+ "nuclear": 0.0,
44
+ "petroleum": 2.2,
45
+ "biomass": 0.6,
46
+ "coal": 0.0,
47
+ "other": 0.0,
48
+ "solar": 0.1,
49
+ "tidal": 0.0,
50
+ "total": 100.2,
51
+ "wind": 97.3,
52
+ "hydro": 0.0
53
+ },
54
+ "ontario": {
55
+ "naturalGas": 8.6,
56
+ "nuclear": 50.1,
57
+ "petroleum": 0.1,
58
+ "biomass": 1.0,
59
+ "coal": 0.0,
60
+ "other": 0.0,
61
+ "solar": 5.2,
62
+ "tidal": 0.0,
63
+ "total": 100.1,
64
+ "wind": 11.7,
65
+ "hydro": 23.4
66
+ },
67
+ "nova scotia": {
68
+ "naturalGas": 13.9,
69
+ "nuclear": 0.0,
70
+ "petroleum": 0.0,
71
+ "biomass": 3.5,
72
+ "coal": 47.5,
73
+ "other": 0.0,
74
+ "solar": 0.02,
75
+ "tidal": 0,
76
+ "total": 100.02,
77
+ "wind": 24.2,
78
+ "hydro": 10.9
79
+ },
80
+ "quebec": {
81
+ "naturalGas": 0.2,
82
+ "nuclear": 0.0,
83
+ "petroleum": 0.3,
84
+ "biomass": 0.7,
85
+ "coal": 0.0,
86
+ "other": 0.0,
87
+ "solar": 0.01,
88
+ "tidal": 0.0,
89
+ "total": 100.01,
90
+ "wind": 6.7,
91
+ "hydro": 92.1
92
+ },
93
+ "alberta": {
94
+ "naturalGas": 73.5,
95
+ "nuclear": 0.0,
96
+ "petroleum": 0.02,
97
+ "biomass": 2.0,
98
+ "coal": 8.0,
99
+ "other": 0.0,
100
+ "solar": 3.2,
101
+ "tidal": 0.0,
102
+ "total": 99.82,
103
+ "wind": 11.3,
104
+ "hydro": 1.8
105
+ },
106
+ "manitoba": {
107
+ "naturalGas": 0.6,
108
+ "nuclear": 0.0,
109
+ "petroleum": 0.1,
110
+ "biomass": 0.2,
111
+ "coal": 0.0,
112
+ "other": 0.0,
113
+ "solar": 0.1,
114
+ "tidal": 0.0,
115
+ "total": 100.1,
116
+ "wind": 1.7,
117
+ "hydro": 97.4
118
+ },
119
+ "northwest territories": {
120
+ "naturalGas": 7.3,
121
+ "nuclear": 0.0,
122
+ "petroleum": 10.5,
123
+ "biomass": 0.0,
124
+ "coal": 0.0,
125
+ "other": 0.0,
126
+ "solar": 0.9,
127
+ "tidal": 0.0,
128
+ "total": 100.0,
129
+ "wind": 7.1,
130
+ "hydro": 74.2
131
+ },
132
+ "new brunswick": {
133
+ "naturalGas": 16.6,
134
+ "nuclear": 39.9,
135
+ "petroleum": 0.3,
136
+ "biomass": 1.3,
137
+ "coal": 12.1,
138
+ "other": 0.0,
139
+ "solar": 0.0,
140
+ "tidal": 0.0,
141
+ "total": 99.9,
142
+ "wind": 8.7,
143
+ "hydro": 21.0
144
+ },
145
+ "nunavut": {
146
+ "naturalGas": 0.0,
147
+ "nuclear": 0.0,
148
+ "petroleum": 99.8,
149
+ "biomass": 0.0,
150
+ "coal": 0.0,
151
+ "other": 0.0,
152
+ "solar": 0.2,
153
+ "tidal": 0.0,
154
+ "total": 100.0,
155
+ "wind": 0.0,
156
+ "hydro": 0.0
157
+ },
158
+ "yukon": {
159
+ "naturalGas": 20.2,
160
+ "nuclear": 0.0,
161
+ "petroleum": 15.0,
162
+ "biomass": 0.5,
163
+ "coal": 0.0,
164
+ "other": 0.0,
165
+ "solar": 0.1,
166
+ "tidal": 0.0,
167
+ "total": 100.1,
168
+ "wind": 0.2,
169
+ "hydro": 64.1
170
+ }
171
+ }
@@ -843,7 +843,7 @@ class BaseEmissionsTracker(ABC):
843
843
  raise e
844
844
 
845
845
  warning_duration = self._measure_power_secs * 3
846
- if last_duration > warning_duration:
846
+ if last_duration > warning_duration and not self._scheduler._stopped:
847
847
  warn_msg = (
848
848
  "Background scheduler didn't run for a long period"
849
849
  + " (%ds), results might be inaccurate"
@@ -102,25 +102,43 @@ class GPU(BaseHardware):
102
102
  Get the Ids of the GPUs that we will monitor
103
103
  :return: list of ids
104
104
  """
105
- gpu_ids = []
106
105
  if self.gpu_ids is not None:
107
- # Check that the provided GPU ids are valid
108
- if not set(self.gpu_ids).issubset(set(range(self.num_gpus))):
109
- logger.warning(
110
- f"Unknown GPU ids {gpu_ids}, only {self.num_gpus} GPUs available."
111
- )
112
- # Keep only the GPUs that are in the provided list
113
- for gpu_id in range(self.num_gpus):
114
- if gpu_id in self.gpu_ids:
115
- gpu_ids.append(gpu_id)
106
+ uuids_to_ids = {
107
+ gpu.get("uuid"): gpu.get("gpu_index")
108
+ for gpu in self.devices.get_gpu_static_info()
109
+ }
110
+ monitored_gpu_ids = []
111
+
112
+ for gpu_id in self.gpu_ids:
113
+ found_gpu_id = False
114
+ # Does it look like an index into the number of GPUs on the system?
115
+ if isinstance(gpu_id, int) or gpu_id.isdigit():
116
+ gpu_id = int(gpu_id)
117
+ if 0 <= gpu_id < self.num_gpus:
118
+ monitored_gpu_ids.append(gpu_id)
119
+ found_gpu_id = True
120
+ # Does it match a prefix of any UUID on the system after stripping any 'MIG-'
121
+ # id prefix per https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#cuda-environment-variables ?
116
122
  else:
117
- logger.info(
118
- f"GPU number {gpu_id} will not be monitored, at your request."
123
+ stripped_gpu_id_str = gpu_id.lstrip("MIG-")
124
+ for uuid, id in uuids_to_ids.items():
125
+ if uuid.startswith(stripped_gpu_id_str):
126
+ logger.debug(
127
+ f"Matching GPU ID {stripped_gpu_id_str} (originally {gpu_id}) against {uuid} for GPU index {id}"
128
+ )
129
+ monitored_gpu_ids.append(id)
130
+ found_gpu_id = True
131
+ break
132
+ if not found_gpu_id:
133
+ logger.warning(
134
+ f"GPU with ID '{gpu_id}' not found or invalid. It will be ignored."
119
135
  )
120
- self.gpu_ids = gpu_ids
136
+
137
+ monitored_gpu_ids = sorted(list(set(monitored_gpu_ids)))
138
+ self.gpu_ids = monitored_gpu_ids
139
+ return monitored_gpu_ids
121
140
  else:
122
- gpu_ids = set(range(self.num_gpus))
123
- return gpu_ids
141
+ return list(range(self.num_gpus))
124
142
 
125
143
  def total_power(self) -> Power:
126
144
  return self._total_power
@@ -135,7 +153,7 @@ class GPU(BaseHardware):
135
153
  new_gpu_ids = gpus._get_gpu_ids()
136
154
  if len(new_gpu_ids) < gpus.num_gpus:
137
155
  logger.warning(
138
- f"You have {gpus.num_gpus} GPUs but we will monitor only {len(gpu_ids)} of them. Check your configuration."
156
+ f"You have {gpus.num_gpus} GPUs but we will monitor only {len(new_gpu_ids)} ({new_gpu_ids}) of them. Check your configuration."
139
157
  )
140
158
  return cls(gpu_ids=new_gpu_ids)
141
159
 
@@ -24,7 +24,7 @@ class DataSource:
24
24
  "geo_js_url": "https://get.geojs.io/v1/ip/geo.json",
25
25
  "cloud_emissions_path": "data/cloud/impact.csv",
26
26
  "usa_emissions_data_path": "data/private_infra/2016/usa_emissions.json",
27
- "can_energy_mix_data_path": "data/private_infra/2016/canada_energy_mix.json", # noqa: E501
27
+ "can_energy_mix_data_path": "data/private_infra/2023/canada_energy_mix.json", # noqa: E501
28
28
  "global_energy_mix_data_path": "data/private_infra/global_energy_mix.json", # noqa: E501
29
29
  "carbon_intensity_per_source_path": "data/private_infra/carbon_intensity_per_source.json",
30
30
  "cpu_power_path": "data/hardware/cpu_power.csv",
@@ -36,7 +36,11 @@ class FileOutput(BaseOutput):
36
36
  def has_valid_headers(self, data: EmissionsData):
37
37
  with open(self.save_file_path) as csv_file:
38
38
  csv_reader = csv.DictReader(csv_file)
39
- dict_from_csv = dict(list(csv_reader)[0])
39
+ csv_entries_list = list(csv_reader)
40
+ if len(csv_entries_list) == 0:
41
+ # No entries
42
+ return True
43
+ dict_from_csv = dict(csv_entries_list[0])
40
44
  list_of_column_names = list(dict_from_csv.keys())
41
45
  return list(data.values.keys()) == list_of_column_names
42
46
 
@@ -48,7 +52,7 @@ class FileOutput(BaseOutput):
48
52
  """
49
53
  file_exists: bool = os.path.isfile(self.save_file_path)
50
54
  if file_exists and not self.has_valid_headers(total):
51
- logger.warning("The CSV format have changed, backing up old emission file.")
55
+ logger.warning("The CSV format has changed, backing up old emission file.")
52
56
  backup(self.save_file_path)
53
57
  file_exists = False
54
58
  new_df = pd.DataFrame.from_records([dict(total.values)])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecarbon
3
- Version: 3.0.4
3
+ Version: 3.0.5
4
4
  Author: Mila, DataForGood, BCG GAMMA, Comet.ml, Haverford College
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://codecarbon.io/
@@ -39,8 +39,8 @@ codecarbon/data/hardware/cpu_dataset_builder/intel_cpu_scrapper.py
39
39
  codecarbon/data/hardware/cpu_dataset_builder/merge_scrapped_cpu_power.py
40
40
  codecarbon/data/private_infra/carbon_intensity_per_source.json
41
41
  codecarbon/data/private_infra/global_energy_mix.json
42
- codecarbon/data/private_infra/2016/canada_energy_mix.json
43
42
  codecarbon/data/private_infra/2016/usa_emissions.json
43
+ codecarbon/data/private_infra/2023/canada_energy_mix.json
44
44
  codecarbon/external/__init__.py
45
45
  codecarbon/external/geography.py
46
46
  codecarbon/external/hardware.py
@@ -61,7 +61,7 @@ codecarbon = [
61
61
  "data/private_infra/global_energy_mix.json",
62
62
  "data/private_infra/carbon_intensity_per_source.json",
63
63
  "data/private_infra/2016/usa_emissions.json",
64
- "data/private_infra/2016/canada_energy_mix.json",
64
+ "data/private_infra/2023/canada_energy_mix.json",
65
65
  ]
66
66
 
67
67
  [project.urls]
@@ -160,7 +160,7 @@ docs = "cd docs/edit && make docs"
160
160
  carbonboard = "python codecarbon/viz/carbonboard.py"
161
161
 
162
162
  [tool.bumpver]
163
- current_version = "3.0.4"
163
+ current_version = "3.0.5"
164
164
  version_pattern = "MAJOR.MINOR.PATCH[_TAGNUM]"
165
165
 
166
166
  [tool.bumpver.file_patterns]
@@ -32,16 +32,16 @@ class TestConfig(unittest.TestCase):
32
32
 
33
33
  def test_parse_gpu_ids(self):
34
34
  for ids, target in [
35
- ("0,1,2", [0, 1, 2]),
36
- ("[0, 1, 2", [0, 1, 2]),
37
- ("(0, 1, 2)", [0, 1, 2]),
38
- ("[1]", [1]),
39
- ("1", [1]),
40
- ("0", [0]),
35
+ ("0,1,2", ["0", "1", "2"]),
36
+ ("[0, 1, 2", ["0", "1", "2"]),
37
+ ("(0, 1, 2)", ["0", "1", "2"]),
38
+ ("[1]", ["1"]),
39
+ ("1", ["1"]),
40
+ ("0", ["0"]),
41
+ ("MIG-f1e", ["MIG-f1e"]),
41
42
  ("", []),
42
43
  ([], []),
43
- ([1, 2, 3], [1, 2, 3]),
44
- (1, 1),
44
+ ([1, 2, 3], ["1", "2", "3"]),
45
45
  ]:
46
46
  self.assertEqual(parse_gpu_ids(ids), target)
47
47
 
@@ -101,6 +101,7 @@ class TestConfig(unittest.TestCase):
101
101
  "USER": "useless key",
102
102
  "CODECARBON_ENV_OVERWRITE": "SUCCESS:overwritten",
103
103
  "CODECARBON_ENV_NEW_KEY": "cool value",
104
+ "CODECARBON_ALLOW_MULTIPLE_RUNS": "True",
104
105
  },
105
106
  )
106
107
  def test_read_confs_and_parse_envs(self):
@@ -145,9 +146,8 @@ class TestConfig(unittest.TestCase):
145
146
  "builtins.open", new_callable=get_custom_mock_open(global_conf, local_conf)
146
147
  ):
147
148
  conf = dict(get_hierarchical_config())
148
- target = {
149
- "allow_multiple_runs": "True"
150
- } # allow_multiple_runs is a default value
149
+ # allow_multiple_runs is set in pytest.ini and not mocked, so it's visible here.
150
+ target = {"allow_multiple_runs": "True"}
151
151
  self.assertDictEqual(conf, target)
152
152
 
153
153
  @mock.patch.dict(
@@ -190,7 +190,7 @@ class TestConfig(unittest.TestCase):
190
190
  self.assertEqual(tracker._force_ram_power, 50.5)
191
191
  self.assertEqual(tracker._output_dir, "/success/overwritten")
192
192
  self.assertEqual(tracker._emissions_endpoint, "http://testhost:2000")
193
- self.assertEqual(tracker._gpu_ids, [0, 1])
193
+ self.assertEqual(tracker._gpu_ids, ["0", "1"])
194
194
  self.assertEqual(tracker._co2_signal_api_token, "signal-token")
195
195
  self.assertEqual(tracker._project_name, "test-project")
196
196
  self.assertTrue(tracker._save_to_file)
@@ -206,7 +206,7 @@ class TestConfig(unittest.TestCase):
206
206
  tracker = EmissionsTracker(
207
207
  project_name="test-project", allow_multiple_runs=True
208
208
  )
209
- self.assertEqual(tracker._gpu_ids, [2, 3])
209
+ self.assertEqual(tracker._gpu_ids, ["2", "3"])
210
210
 
211
211
  @mock.patch.dict(
212
212
  os.environ,
@@ -220,7 +220,7 @@ class TestConfig(unittest.TestCase):
220
220
  tracker = EmissionsTracker(
221
221
  project_name="test-project", allow_multiple_runs=True
222
222
  )
223
- self.assertEqual(tracker._gpu_ids, [99])
223
+ self.assertEqual(tracker._gpu_ids, ["99"])
224
224
  gpu_count = 0
225
225
  for hardware in tracker._hardware:
226
226
  if isinstance(hardware, GPU):
@@ -4,7 +4,7 @@ from unittest import mock
4
4
 
5
5
  from codecarbon.core.units import Power
6
6
  from codecarbon.emissions_tracker import OfflineEmissionsTracker
7
- from codecarbon.external.hardware import CPU, MODE_CPU_LOAD
7
+ from codecarbon.external.hardware import CPU, MODE_CPU_LOAD, AppleSiliconChip
8
8
 
9
9
 
10
10
  @mock.patch("codecarbon.core.cpu.is_psutil_available", return_value=True)
@@ -57,7 +57,9 @@ class TestCPULoad(unittest.TestCase):
57
57
  ):
58
58
  tracker = OfflineEmissionsTracker(country_iso_code="FRA")
59
59
  for hardware in tracker._hardware:
60
- if isinstance(hardware, CPU) and hardware._mode == MODE_CPU_LOAD:
60
+ if (
61
+ isinstance(hardware, CPU) and hardware._mode == MODE_CPU_LOAD
62
+ ) or isinstance(hardware, AppleSiliconChip):
61
63
  break
62
64
  else:
63
65
  raise Exception("No CPU load !!!")
@@ -154,10 +154,13 @@ class TestEmissions(unittest.TestCase):
154
154
  country_iso_code="CAN", country_name="Canada", region="ontario"
155
155
  ),
156
156
  )
157
+ manually_computed_emissions = (
158
+ 3 * (0.995725971 * 0.0 + 0.8166885263 * 0.1 + 0.7438415916 * 8.6) / 100.1
159
+ )
157
160
 
158
161
  # THEN
159
162
  assert isinstance(emissions, float)
160
- self.assertAlmostEqual(emissions, 0.12, places=2)
163
+ self.assertAlmostEqual(emissions, manually_computed_emissions, places=2)
161
164
 
162
165
  def test_get_emissions_PRIVATE_INFRA_unknown_country(self):
163
166
  """
@@ -520,3 +520,91 @@ class TestCarbonTracker(unittest.TestCase):
520
520
  self.assertEqual("United States", emissions_df["country_name"].values[0])
521
521
  self.assertEqual("USA", emissions_df["country_iso_code"].values[0])
522
522
  self.assertIsInstance(tracker.final_emissions, float)
523
+
524
+ @mock.patch("codecarbon.emissions_tracker.logger")
525
+ def test_scheduler_warning_suppressed_when_stopped(
526
+ self,
527
+ mock_logger,
528
+ mock_setup_intel_cli,
529
+ mock_log_values,
530
+ mocked_get_gpu_details,
531
+ mocked_env_cloud_details,
532
+ mocked_is_gpu_details_available,
533
+ ):
534
+ """Test that scheduler warning is suppressed when scheduler is stopped."""
535
+ with EmissionsTracker(
536
+ output_dir=self.temp_path,
537
+ measure_power_secs=1, # Short interval for testing
538
+ ) as tracker:
539
+ # Stop the scheduler to simulate task mode or manual stopping
540
+ tracker._scheduler.stop()
541
+
542
+ # Artificially set last measured time to simulate long delay
543
+ import time
544
+
545
+ tracker._last_measured_time = time.perf_counter() - 10 # 10 seconds ago
546
+
547
+ # Reset mock to clear any previous warning calls
548
+ mock_logger.warning.reset_mock()
549
+
550
+ # Call _measure_power_and_energy directly - this would normally trigger warning
551
+ tracker._measure_power_and_energy()
552
+
553
+ # Verify that if warning was called, it wasn't the scheduler warning
554
+ if mock_logger.warning.called:
555
+ for call in mock_logger.warning.call_args_list:
556
+ args, kwargs = call
557
+ if (
558
+ args
559
+ and "Background scheduler didn't run for a long period"
560
+ in str(args[0])
561
+ ):
562
+ self.fail(
563
+ "Scheduler warning was called when it should have been suppressed"
564
+ )
565
+
566
+ @mock.patch("codecarbon.emissions_tracker.logger")
567
+ def test_scheduler_warning_shown_when_running(
568
+ self,
569
+ mock_logger,
570
+ mock_setup_intel_cli,
571
+ mock_log_values,
572
+ mocked_get_gpu_details,
573
+ mocked_env_cloud_details,
574
+ mocked_is_gpu_details_available,
575
+ ):
576
+ """Test that scheduler warning is shown when scheduler is running but delayed."""
577
+ with EmissionsTracker(
578
+ output_dir=self.temp_path,
579
+ measure_power_secs=1, # Short interval for testing
580
+ ) as tracker:
581
+ # Ensure scheduler is running (default state)
582
+ self.assertFalse(tracker._scheduler._stopped)
583
+
584
+ # Artificially set last measured time to simulate long delay
585
+ import time
586
+
587
+ tracker._last_measured_time = time.perf_counter() - 10 # 10 seconds ago
588
+
589
+ # Reset mock to clear any previous warning calls
590
+ mock_logger.warning.reset_mock()
591
+
592
+ # Call _measure_power_and_energy directly - this should trigger warning
593
+ tracker._measure_power_and_energy()
594
+
595
+ # Verify warning was logged since scheduler should be running
596
+ scheduler_warning_found = False
597
+ if mock_logger.warning.called:
598
+ for call in mock_logger.warning.call_args_list:
599
+ args, kwargs = call
600
+ if (
601
+ args
602
+ and "Background scheduler didn't run for a long period"
603
+ in str(args[0])
604
+ ):
605
+ scheduler_warning_found = True
606
+ break
607
+
608
+ self.assertTrue(
609
+ scheduler_warning_found, "Expected scheduler warning was not found"
610
+ )
@@ -50,7 +50,7 @@ class FakeGPUEnv:
50
50
  self.DETAILS = {
51
51
  "handle_0": {
52
52
  "name": b"GeForce GTX 1080",
53
- "uuid": b"uuid#1",
53
+ "uuid": b"uuid-1",
54
54
  "memory": real_pynvml.c_nvmlMemory_t(1024, 100, 924),
55
55
  "temperature": 75,
56
56
  "power_usage": 26,
@@ -66,7 +66,7 @@ class FakeGPUEnv:
66
66
  },
67
67
  "handle_1": {
68
68
  "name": b"GeForce GTX 1080",
69
- "uuid": b"uuid#2",
69
+ "uuid": b"uuid-2",
70
70
  "memory": real_pynvml.c_nvmlMemory_t(1024, 200, 824),
71
71
  "temperature": 79,
72
72
  "power_usage": 29,
@@ -84,7 +84,7 @@ class FakeGPUEnv:
84
84
  self.expected = [
85
85
  {
86
86
  "name": "GeForce GTX 1080",
87
- "uuid": "uuid#1",
87
+ "uuid": "uuid-1",
88
88
  "total_memory": 1024,
89
89
  "free_memory": 100,
90
90
  "used_memory": 924,
@@ -102,7 +102,7 @@ class FakeGPUEnv:
102
102
  },
103
103
  {
104
104
  "name": "GeForce GTX 1080",
105
- "uuid": "uuid#2",
105
+ "uuid": "uuid-2",
106
106
  "total_memory": 1024,
107
107
  "free_memory": 200,
108
108
  "used_memory": 824,
@@ -146,14 +146,14 @@ class TestGpu(FakeGPUEnv):
146
146
  expected = [
147
147
  {
148
148
  "name": "GeForce GTX 1080",
149
- "uuid": "uuid#1",
149
+ "uuid": "uuid-1",
150
150
  "total_memory": 1024,
151
151
  "power_limit": 149,
152
152
  "gpu_index": 0,
153
153
  },
154
154
  {
155
155
  "name": "GeForce GTX 1080",
156
- "uuid": "uuid#2",
156
+ "uuid": "uuid-2",
157
157
  "total_memory": 1024,
158
158
  "power_limit": 149,
159
159
  "gpu_index": 1,
@@ -311,6 +311,33 @@ class TestGpu(FakeGPUEnv):
311
311
  expected_power = gpu2_power
312
312
  tc.assertAlmostEqual(expected_power.kW, gpu.total_power().kW)
313
313
 
314
+ def test_get_gpu_ids(self):
315
+ """
316
+ Check parsing of gpu_ids in various forms.
317
+ """
318
+ # Prepare
319
+ from codecarbon.external.hardware import GPU
320
+
321
+ for test_ids, expected_ids in [
322
+ ([0, 1], [0, 1]),
323
+ ([0, 1, 2], [0, 1]),
324
+ ([2], []),
325
+ (["0", "1"], [0, 1]),
326
+ # Only two GPUS in the system, so ignore the third (index 2)
327
+ (["0", "1", "2"], [0, 1]),
328
+ (["2"], []),
329
+ # Check UUID-to-index mapping
330
+ (["uuid-1"], [0]),
331
+ (["uuid-1", "uuid-2"], [0, 1]),
332
+ (["uuid-3"], []),
333
+ # Check UUID-to-index mapping when we need to strip the prefix
334
+ (["MIG-uuid-1"], [0]),
335
+ (["MIG-uuid-3"], []),
336
+ ]:
337
+ gpu = GPU(test_ids)
338
+ result = gpu._get_gpu_ids()
339
+ assert result == expected_ids
340
+
314
341
 
315
342
  class TestGpuNotAvailable:
316
343
  def setup_method(self):
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import sys
2
3
  import unittest
3
4
  from unittest.mock import MagicMock, patch
4
5
 
@@ -18,6 +19,10 @@ class TestUnsupportedGPU(unittest.TestCase):
18
19
  if os.path.exists(self.output_csv):
19
20
  os.remove(self.output_csv)
20
21
 
22
+ # If we run this on macOS, NVMLError_NotSupported has no effect
23
+ # and we end up with non-zero values for GPU energy and power because
24
+ # we use the non-NVML code-path in hardware.AppleSiliconChip().
25
+ @unittest.skipIf(sys.platform == "darwin", "NVML not available on macOS")
21
26
  @patch("codecarbon.core.gpu.pynvml")
22
27
  def test_emissions_tracker_unsupported_gpu(self, mock_pynvml):
23
28
  mock_pynvml.NVMLError_NotSupported = self.NVMLError_NotSupported
@@ -1 +0,0 @@
1
- __version__ = "3.0.4"
@@ -1,171 +0,0 @@
1
- {
2
- "british columbia": {
3
- "naturalGas": 1.1,
4
- "nuclear": 0.0,
5
- "petroleum": 0.7,
6
- "biomass": 6.4,
7
- "coal": 0.0,
8
- "other": 0.0,
9
- "solar": 0.0,
10
- "tidal": 0.0,
11
- "total": 100,
12
- "wind": 1.3,
13
- "hydro": 90.5
14
- },
15
- "newfoundland and labrador": {
16
- "naturalGas": 0.7,
17
- "nuclear": 0.0,
18
- "petroleum": 4.8,
19
- "biomass": 0.3,
20
- "coal": 0.0,
21
- "other": 0.0,
22
- "solar": 0.0,
23
- "tidal": 0.0,
24
- "total": 100,
25
- "wind": 0.5,
26
- "hydro": 93.7
27
- },
28
- "saskatchewan": {
29
- "naturalGas": 35.7,
30
- "nuclear": 0.0,
31
- "petroleum": 0.0,
32
- "biomass": 0.0,
33
- "coal": 46.6,
34
- "other": 0.0,
35
- "solar": 0.1,
36
- "tidal": 0.0,
37
- "total": 99.9,
38
- "wind": 3.8,
39
- "hydro": 13.7
40
- },
41
- "prince edward island": {
42
- "naturalGas": 0.0,
43
- "nuclear": 0.0,
44
- "petroleum": 1.1,
45
- "biomass": 0.7,
46
- "coal": 0.0,
47
- "other": 0.0,
48
- "solar": 0.3,
49
- "tidal": 0.0,
50
- "total": 100,
51
- "wind": 97.9,
52
- "hydro": 0.0
53
- },
54
- "ontario": {
55
- "naturalGas": 5.2,
56
- "nuclear": 58.6,
57
- "petroleum": 0.1,
58
- "biomass": 1.3,
59
- "coal": 0.0,
60
- "other": 0.0,
61
- "solar": 2.2,
62
- "tidal": 0.0,
63
- "total": 100,
64
- "wind": 6.7,
65
- "hydro": 25.9
66
- },
67
- "nova scotia": {
68
- "naturalGas": 14.3,
69
- "nuclear": 0.0,
70
- "petroleum": 12.2,
71
- "biomass": 4.9,
72
- "coal": 47.9,
73
- "other": 0.0,
74
- "solar": 0.03,
75
- "tidal": 0.2,
76
- "total": 100.13,
77
- "wind": 11.8,
78
- "hydro": 8.8
79
- },
80
- "quebec": {
81
- "naturalGas": 0.1,
82
- "nuclear": 0.0,
83
- "petroleum": 0.2,
84
- "biomass": 0.8,
85
- "coal": 0.0,
86
- "other": 0.0,
87
- "solar": 0.0,
88
- "tidal": 0.0,
89
- "total": 100,
90
- "wind": 3.9,
91
- "hydro": 95.0
92
- },
93
- "alberta": {
94
- "naturalGas": 42.2,
95
- "nuclear": 0.0,
96
- "petroleum": 2.6,
97
- "biomass": 2.2,
98
- "coal": 44.9,
99
- "other": 0.2,
100
- "solar": 0.1,
101
- "tidal": 0.0,
102
- "total": 100.1,
103
- "wind": 5.4,
104
- "hydro": 2.5
105
- },
106
- "manitoba": {
107
- "naturalGas": 0.0,
108
- "nuclear": 0.0,
109
- "petroleum": 0.2,
110
- "biomass": 0.1,
111
- "coal": 0.1,
112
- "other": 0.0,
113
- "solar": 0.0,
114
- "tidal": 0.0,
115
- "total": 99.9,
116
- "wind": 2.7,
117
- "hydro": 96.8
118
- },
119
- "northwest territories": {
120
- "naturalGas": 4.0,
121
- "nuclear": 0.0,
122
- "petroleum": 55.3,
123
- "biomass": 0.0,
124
- "coal": 0.0,
125
- "other": 0.0,
126
- "solar": 0.2,
127
- "tidal": 0.0,
128
- "total": 100,
129
- "wind": 2.0,
130
- "hydro": 38.5
131
- },
132
- "new brunswick": {
133
- "naturalGas": 9.9,
134
- "nuclear": 36.1,
135
- "petroleum": 7.6,
136
- "biomass": 4.2,
137
- "coal": 15.8,
138
- "other": 0.0,
139
- "solar": 0.0,
140
- "tidal": 0.0,
141
- "total": 99.8,
142
- "wind": 6.6,
143
- "hydro": 19.6
144
- },
145
- "nunavut": {
146
- "naturalGas": 0.0,
147
- "nuclear": 0.0,
148
- "petroleum": 100.0,
149
- "biomass": 0.0,
150
- "coal": 0.0,
151
- "other": 0.0,
152
- "solar": 0.0,
153
- "tidal": 0.0,
154
- "total": 100,
155
- "wind": 0.0,
156
- "hydro": 0.0
157
- },
158
- "yukon": {
159
- "naturalGas": 2.0,
160
- "nuclear": 0.0,
161
- "petroleum": 5.5,
162
- "biomass": 0.0,
163
- "coal": 0.0,
164
- "other": 0.0,
165
- "solar": 0.3,
166
- "tidal": 0.0,
167
- "total": 100,
168
- "wind": 0.0,
169
- "hydro": 92.2
170
- }
171
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes