GreenTrace 0.1.0__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.
File without changes
@@ -0,0 +1,61 @@
1
+ # GreenTrace/emissions.py
2
+
3
+
4
+ class CarbonIntensityProvider:
5
+ """
6
+ Provides carbon intensity data (gCO₂eq/kWh).
7
+ """
8
+
9
+ # Source: IEA – International Energy Agency. Data for 2025.
10
+ # Data represents grams of CO2 equivalent per kilowatt-hour (gCO₂eq/kWh).
11
+ _INTENSITY_DATA = {
12
+ # Europe
13
+ "DE": 380, # Germany (declining, but coal still retains a share)
14
+ "GB": 145, # Great Britain (active decommissioning of coal)
15
+ "FR": 55, # France (traditionally low due to nuclear power)
16
+ "IT": 290, # Italy
17
+ "ES": 140, # Spain (high share of solar generation)
18
+ "PL": 650, # Poland (gradual move away from coal, significant reduction)
19
+ "NL": 310, # Netherlands
20
+ "SE": 12, # Sweden (almost complete decarbonization)
21
+ "NO": 10, # Norway
22
+ "FI": 50, # Finland
23
+ "DK": 85, # Denmark
24
+ "RU": 335, # Russia (stable, slight increase in the share of gas)
25
+ "CH": 25, # Switzerland
26
+ "AT": 80, # Austria
27
+ "BE": 135, # Belgium
28
+ "IE": 310, # Ireland
29
+ "PT": 150, # Portugal
30
+ "GR": 265, # Greece (reached a historic low in 2025)
31
+ # America
32
+ "US": 355, # USA (moderate decline, gas growth limits the fall)
33
+ "CA": 105, # Canada
34
+ "BR": 85, # Brazil (high share of hydropower)
35
+ "MX": 410, # Mexico
36
+ "AR": 260, # Argentina
37
+ "CL": 145, # Chile
38
+ "CO": 90, # Colombia
39
+ # Asia and Oceania
40
+ "CN": 525, # China (noticeable reduction due to record commissioning of renewables)
41
+ "IN": 680, # India (intensity is falling, despite the growth in absolute emissions)
42
+ "JP": 440, # Japan
43
+ "AU": 480, # Australia (rapid adoption of solar panels)
44
+ "KR": 395, # South Korea
45
+ "ID": 740, # Indonesia
46
+ "VN": 450, # Vietnam
47
+ "TH": 420, # Thailand
48
+ "MY": 560, # Malaysia
49
+ "PK": 480, # Pakistan
50
+ "NZ": 85, # New Zealand
51
+ # Other regions
52
+ "ZA": 810, # South Africa (remains one of the highest in the world)
53
+ "TR": 430, # Turkey
54
+ "DEFAULT": 425, # Global average (IEA forecast for 2025: ~415-430)
55
+ }
56
+
57
+ def get_intensity(self, region_code: str) -> int:
58
+ """Returns the intensity for a region or the default value."""
59
+ return self._INTENSITY_DATA.get(
60
+ region_code.upper(), self._INTENSITY_DATA["DEFAULT"]
61
+ )
File without changes
@@ -0,0 +1,56 @@
1
+ # GreenTrace/hardware/base.py
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import List, Dict, Tuple, Optional
5
+ from datetime import datetime
6
+
7
+
8
+ class HardwareMonitor(ABC):
9
+ """Abstract base class for monitoring hardware power consumption."""
10
+
11
+ def __init__(self, max_measurements: Optional[int] = None):
12
+ self._measurements: List[Tuple[datetime, float]] = []
13
+ self.max_measurements = max_measurements
14
+
15
+ @abstractmethod
16
+ def start(self, silent: bool = False) -> None:
17
+ """Start monitoring."""
18
+ pass
19
+
20
+ @abstractmethod
21
+ def stop(self, silent: bool = False) -> None:
22
+ """Stop monitoring."""
23
+ pass
24
+
25
+ @abstractmethod
26
+ def get_power_usage(self) -> float:
27
+ """Get the current power consumption in Watts."""
28
+ pass
29
+
30
+ def get_total_energy_kwh(self, interval_seconds: int) -> float:
31
+ """
32
+ Calculates the total energy consumed in kWh.
33
+
34
+ Formula: (average_power_watts * total_seconds) / (1000 * 3600)
35
+ """
36
+ if not self._measurements:
37
+ return 0.0
38
+
39
+ avg_power_watts = sum(power for _, power in self._measurements) / len(
40
+ self._measurements
41
+ )
42
+ total_seconds = len(self._measurements) * interval_seconds
43
+
44
+ kwh = (avg_power_watts * total_seconds) / (1000 * 3600)
45
+ return kwh
46
+
47
+ def add_measurement(self, power_watts: float):
48
+ """Adds a power measurement with a timestamp."""
49
+ self._measurements.append((datetime.now(), power_watts))
50
+
51
+ if self.max_measurements and len(self._measurements) > self.max_measurements:
52
+ self._measurements.pop(0)
53
+
54
+ def get_metadata(self) -> Dict[str, str]:
55
+ """Returns metadata about the hardware."""
56
+ return {}
@@ -0,0 +1,150 @@
1
+ # GreenTrace/hardware/cpu.py
2
+
3
+ import os
4
+ import platform
5
+ import time
6
+ import psutil
7
+ from typing import Optional, Dict
8
+
9
+ import cpuinfo
10
+
11
+ from .base import HardwareMonitor
12
+
13
+ # Data source for TDP/Max Power:
14
+ # Intel/AMD: Data from manufacturers and benchmarks.
15
+ CPU_POWER_DATA: Dict[str, float] = {
16
+ # Apple Silicon (Max Power, W)
17
+ "Apple M1": 20.0,
18
+ "Apple M1 Pro": 40.0,
19
+ "Apple M1 Max": 90.0,
20
+ "Apple M1 Ultra": 150.0,
21
+ "Apple M2": 25.0,
22
+ "Apple M2 Pro": 50.0,
23
+ "Apple M2 Max": 100.0,
24
+ "Apple M2 Ultra": 160.0,
25
+ "Apple M3": 28.0,
26
+ "Apple M3 Pro": 60.0,
27
+ "Apple M3 Max": 120.0,
28
+ "Apple M4": 24.0,
29
+ "Apple M4 Pro": 38.0,
30
+ "Apple M4 Max": 70.0,
31
+ # Intel Core (TDP, W)
32
+ "Intel Core i9-13900K": 125.0,
33
+ "Intel Core i7-13700K": 125.0,
34
+ "Intel Core i5-13600K": 125.0,
35
+ "Intel Core i9-12900K": 125.0,
36
+ "Intel Core i7-12700K": 125.0,
37
+ "Intel Core i5-12600K": 125.0,
38
+ "Intel Core i9-11900K": 125.0,
39
+ "Intel Core i7-11700K": 125.0,
40
+ "Intel Core i5-11600K": 125.0,
41
+ # AMD Ryzen (TDP, W)
42
+ "AMD Ryzen 9 7950X": 170.0,
43
+ "AMD Ryzen 7 7700X": 105.0,
44
+ "AMD Ryzen 5 7600X": 105.0,
45
+ "AMD Ryzen 9 5950X": 105.0,
46
+ "AMD Ryzen 9 5900X": 105.0,
47
+ "AMD Ryzen 7 5800X": 105.0,
48
+ "AMD Ryzen 5 5600X": 65.0,
49
+ }
50
+
51
+ RAPL_DIR = "/sys/class/powercap/intel-rapl/"
52
+
53
+
54
+ class CPUMonitor(HardwareMonitor):
55
+ """Monitors CPU power consumption."""
56
+
57
+ def __init__(self, max_measurements: Optional[int] = None):
58
+ super().__init__(max_measurements=max_measurements)
59
+ self.tdp = 125.0 # Default value
60
+ self.is_rapl_available = False
61
+ self.rapl_energy_files = []
62
+ self._last_rapl_energy = 0.0
63
+ self._last_rapl_time = 0.0
64
+
65
+ def start(self, silent: bool = False) -> None:
66
+ self._measurements = []
67
+ self._detect_cpu_power_source(silent)
68
+
69
+ if self.is_rapl_available:
70
+ self._last_rapl_energy = self._get_rapl_energy()
71
+ self._last_rapl_time = time.time()
72
+ if not silent:
73
+ print("CPU Monitor: Started using Intel RAPL.")
74
+ else:
75
+ if not silent:
76
+ print(f"CPU Monitor: Started using TDP model ({self.tdp}W).")
77
+
78
+ def _detect_cpu_power_source(self, silent: bool):
79
+ """Determines the data source: RAPL or TDP."""
80
+ # 1. Check for Intel RAPL (Linux only)
81
+ if platform.system() == "Linux" and os.path.exists(RAPL_DIR):
82
+ for root, _, files in os.walk(RAPL_DIR):
83
+ if "energy_uj" in files and "intel-rapl" in root:
84
+ self.rapl_energy_files.append(os.path.join(root, "energy_uj"))
85
+ if self.rapl_energy_files:
86
+ self.is_rapl_available = True
87
+ return
88
+
89
+ cpu_info = cpuinfo.get_cpu_info()
90
+ brand = cpu_info.get("brand_raw", "")
91
+
92
+ for name, power in CPU_POWER_DATA.items():
93
+ if name.lower() in brand.lower():
94
+ self.tdp = power
95
+ if not silent:
96
+ print(f"CPU Monitor: Detected '{brand}', using {power}W.")
97
+ return
98
+
99
+ if not silent:
100
+ print(
101
+ f"CPU Monitor Warning: CPU '{brand}' not in database. "
102
+ f"Falling back to default TDP ({self.tdp}W)."
103
+ )
104
+
105
+ def _get_rapl_energy(self) -> float:
106
+ """Reads the total energy from all found RAPL files."""
107
+ total_energy_uj = 0
108
+ for file_path in self.rapl_energy_files:
109
+ try:
110
+ with open(file_path, "r") as f:
111
+ total_energy_uj += int(f.read())
112
+ except (IOError, ValueError):
113
+ continue
114
+ return total_energy_uj
115
+
116
+ def _get_power_from_rapl(self) -> float:
117
+ """Calculates the average power over a time interval using RAPL."""
118
+ current_energy = self._get_rapl_energy()
119
+ current_time = time.time()
120
+
121
+ time_delta = current_time - self._last_rapl_time
122
+ energy_delta = current_energy - self._last_rapl_energy
123
+
124
+ # Update values for the next call
125
+ self._last_rapl_energy = current_energy
126
+ self._last_rapl_time = current_time
127
+
128
+ if time_delta == 0:
129
+ return 0.0
130
+
131
+ # Convert from microjoules to joules and divide by seconds to get Watts
132
+ power_watts = (energy_delta / 1_000_000) / time_delta
133
+ return power_watts if power_watts >= 0 else 0.0
134
+
135
+ def _get_power_from_tdp(self) -> float:
136
+ """Estimates power consumption based on TDP and CPU utilization."""
137
+ cpu_utilization = psutil.cpu_percent() / 100.0
138
+ power_watts = self.tdp * cpu_utilization
139
+ return power_watts
140
+
141
+ def stop(self, silent: bool = False) -> None:
142
+ if not silent:
143
+ print("CPU Monitor: Stopped")
144
+
145
+ def get_power_usage(self) -> float:
146
+ """Returns the current CPU power consumption in Watts."""
147
+ if self.is_rapl_available:
148
+ return self._get_power_from_rapl()
149
+ else:
150
+ return self._get_power_from_tdp()
File without changes
@@ -0,0 +1,206 @@
1
+ # GreenTrace/reporting.py
2
+ import json
3
+ import csv
4
+ import os
5
+ from datetime import datetime
6
+ from typing import Dict, Any
7
+
8
+
9
+ class Reporter:
10
+ """
11
+ A class for generating reports in various formats (console, JSON, CSV).
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ data: Dict[str, Any],
17
+ output_file: str = None,
18
+ output_format: str = "console",
19
+ csv_summary_only: bool = False,
20
+ silent: bool = False,
21
+ ):
22
+ self.data = data
23
+ self.output_file = output_file
24
+ self.output_format = output_format
25
+ self.csv_summary_only = csv_summary_only
26
+ self.silent = silent
27
+
28
+ def report(self):
29
+ """The main method that calls the required output format."""
30
+ if self.output_file:
31
+ self._report_to_file()
32
+ else:
33
+ self.to_console()
34
+
35
+ def to_console(self):
36
+ """Prints the report to the console."""
37
+ if self.silent:
38
+ return
39
+ summary = self.data["summary"]
40
+ print("\n" + "=" * 30 + " GreenTrace Report " + "=" * 30)
41
+ print(f"Start Time: {summary.get('start_time', 'N/A')}")
42
+ print(f"End Time: {summary.get('end_time', 'N/A')}")
43
+ print(f"Duration: {summary.get('duration_seconds', 0):.2f} seconds")
44
+ print(f"Total Energy Consumed: {summary['total_energy_kwh']:.6f} kWh")
45
+ print(
46
+ f"Carbon Intensity ({summary['region']}): {summary['carbon_intensity_gco2_per_kwh']} gCO₂eq/kWh"
47
+ )
48
+ print(f"Total CO₂ Emissions: {summary['emissions_gco2eq']:.4f} gCO₂eq")
49
+ print(f"Number of measurements: {len(self.data['measurements'])}")
50
+ print("=" * 80 + "\n")
51
+
52
+ def _report_to_file(self):
53
+ """Calls the method to save the report to a file depending on the format."""
54
+ if self.output_format == "json":
55
+ self._to_json()
56
+ elif self.output_format == "csv":
57
+ self._to_csv()
58
+ elif self.output_format == "html":
59
+ self._to_html()
60
+ elif not self.silent:
61
+ print(
62
+ f"Warning: Unsupported file format '{self.output_format}'. Defaulting to console output."
63
+ )
64
+ self.to_console()
65
+
66
+ def _to_json(self):
67
+ with open(self.output_file, "w", encoding="utf-8") as f:
68
+ json.dump(self.data, f, ensure_ascii=False, indent=4)
69
+ if not self.silent:
70
+ print(f"Report saved to {self.output_file}")
71
+
72
+ def _to_csv(self):
73
+ if self.csv_summary_only:
74
+ rows = [self.data.get("summary", {})]
75
+ if not rows:
76
+ if not self.silent:
77
+ print("No summary data to save to CSV.")
78
+ return
79
+ else:
80
+ measurements = self.data.get("measurements", [])
81
+ if not measurements:
82
+ if not self.silent:
83
+ print("No measurements to save to CSV.")
84
+ return
85
+ rows = measurements
86
+
87
+ with open(self.output_file, "w", newline="", encoding="utf-8") as f:
88
+ writer = csv.DictWriter(f, fieldnames=rows[0].keys())
89
+ writer.writeheader()
90
+ writer.writerows(rows)
91
+ if not self.silent:
92
+ print(f"Report saved to {self.output_file}")
93
+
94
+ def _to_html(self):
95
+ summary = self.data["summary"]
96
+ # Average car emissions: ~120 gCO2/km.
97
+ # Source: European Environment Agency
98
+ car_emissions_per_km = 120
99
+ equivalent_km = summary["emissions_gco2eq"] / car_emissions_per_km
100
+
101
+ # Average smartphone battery capacity ~15 Wh = 0.015 kWh
102
+ smartphone_charge_kwh = 0.015
103
+ equivalent_charges = (
104
+ summary["total_energy_kwh"] / smartphone_charge_kwh
105
+ if smartphone_charge_kwh > 0
106
+ else 0
107
+ )
108
+
109
+ start_time = (
110
+ datetime.fromisoformat(summary["start_time"])
111
+ if summary.get("start_time")
112
+ else None
113
+ )
114
+ report_id = (
115
+ f"CP-PY-{(start_time.strftime('%Y%m%d-%H%M%S'))}" if start_time else "N/A"
116
+ )
117
+ issue_date = datetime.now().strftime("%d.%m.%Y")
118
+ duration_str = f"{summary.get('duration_seconds', 0):.2f} sec"
119
+ start_time_str = (
120
+ start_time.strftime("%d.%m.%Y %H:%M:%S") if start_time else "N/A"
121
+ )
122
+
123
+ html_content = f"""
124
+ <!DOCTYPE html>
125
+ <html lang="en">
126
+ <head>
127
+ <meta charset="UTF-8">
128
+ <title>Carbon Footprint Certificate</title>
129
+ <style>
130
+ body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 40px; background-color: #e9ecef; color: #343a40; }}
131
+ .certificate {{ max-width: 800px; margin: auto; background: white; padding: 40px; border: 10px solid #343a40; box-shadow: 0 0 20px rgba(0,0,0,0.15); position: relative; }}
132
+ .header {{ text-align: center; border-bottom: 2px solid #dee2e6; padding-bottom: 20px; margin-bottom: 20px; }}
133
+ .header h1 {{ font-size: 28px; color: #343a40; margin: 0; }}
134
+ .header p {{ font-size: 14px; color: #6c757d; }}
135
+ .main-results {{ text-align: center; margin: 40px 0; }}
136
+ .main-results h2 {{ font-size: 16px; color: #6c757d; text-transform: uppercase; margin-bottom: 10px; }}
137
+ .main-results .value {{ font-size: 48px; font-weight: bold; color: #2a9d8f; }}
138
+ .details-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-top: 30px; }}
139
+ .card {{ background-color: #f8f9fa; padding: 20px; border-radius: 8px; border: 1px solid #dee2e6; }}
140
+ .card h3 {{ margin-top: 0; font-size: 16px; color: #495057; border-bottom: 1px solid #dee2e6; padding-bottom: 10px; margin-bottom: 15px;}}
141
+ .card p {{ font-size: 22px; font-weight: bold; margin: 0; color: #343a40; }}
142
+ .card small {{ color: #6c757d; font-size: 12px; }}
143
+ .methodology {{ margin-top: 40px; padding-top: 20px; border-top: 1px solid #dee2e6; font-size: 12px; color: #6c757d; text-align: justify; }}
144
+ .footer {{ margin-top: 30px; text-align: center; font-size: 12px; color: #adb5bd; }}
145
+ .seal {{ position: absolute; bottom: 30px; right: 30px; width: 90px; height: 90px; background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="48" fill="%23343a40"/><path d="M50,15 L61.8,38.2 L87.5,42.5 L68.8,60.8 L73.6,86 L50,73.8 L26.4,86 L31.2,60.8 L12.5,42.5 L38.2,38.2 Z" fill="%23e9ecef"/><text x="50" y="55" font-family="Arial" font-size="10" fill="%23343a40" text-anchor="middle" font-weight="bold">GreenTrace</text></svg>'); }}
146
+ </style>
147
+ </head>
148
+ <body>
149
+ <div class="certificate">
150
+ <div class="header">
151
+ <h1>Carbon Footprint Certificate</h1>
152
+ <p>Report ID: {report_id} | Issue Date: {issue_date}</p>
153
+ </div>
154
+
155
+ <div class="main-results">
156
+ <h2>Total CO₂ Emissions</h2>
157
+ <p class="value">{summary['emissions_gco2eq']:.4f} gCO₂eq</p>
158
+ </div>
159
+
160
+ <div class="details-grid">
161
+ <div class="card">
162
+ <h3>Total Energy Consumption</h3>
163
+ <p>{summary['total_energy_kwh']:.6f} kWh</p>
164
+ </div>
165
+ <div class="card">
166
+ <h3>Analysis Duration</h3>
167
+ <p>{duration_str}</p>
168
+ <small>Start: {start_time_str}</small>
169
+ </div>
170
+ </div>
171
+
172
+ <div class="details-grid">
173
+ <div class="card">
174
+ <h3>Carbon Intensity ({summary['region']})</h3>
175
+ <p>{summary['carbon_intensity_gco2_per_kwh']} g/kWh</p>
176
+ <small>Amount of CO₂ emitted per 1 kWh of energy generated in this region.</small>
177
+ </div>
178
+ <div class="card">
179
+ <h3>Equivalents</h3>
180
+ <p>~{equivalent_km:.2f} km</p>
181
+ <small>of a passenger car journey</small>
182
+ <p style="margin-top: 10px;">~{equivalent_charges:.0f}</p>
183
+ <small>full smartphone charges</small>
184
+ </div>
185
+ </div>
186
+
187
+ <div class="methodology">
188
+ <h3>Methodology</h3>
189
+ <p>
190
+ The emissions estimate was produced by the GreenTrace library. The calculation is based on measuring CPU power consumption. For Linux systems with Intel processors, Intel RAPL technology can be used for more accurate measurements. In other cases, a model based on Thermal Design Power (TDP) and current processor load is used. The resulting energy consumption is multiplied by the carbon intensity factor for the '{summary['region']}' region. The data is an estimate and is intended for comparative analysis.
191
+ </p>
192
+ </div>
193
+
194
+ <div class="footer">
195
+ Generated with GreenTrace
196
+ </div>
197
+
198
+ <div class="seal"></div>
199
+ </div>
200
+ </body>
201
+ </html>
202
+ """
203
+ with open(self.output_file, "w", encoding="utf-8") as f:
204
+ f.write(html_content)
205
+ if not self.silent:
206
+ print(f"Report saved to {self.output_file}")