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.
- greentrace-0.1.0/GreenTrace/__init__.py +0 -0
- greentrace-0.1.0/GreenTrace/emissions.py +61 -0
- greentrace-0.1.0/GreenTrace/hardware/__init__.py +0 -0
- greentrace-0.1.0/GreenTrace/hardware/base.py +56 -0
- greentrace-0.1.0/GreenTrace/hardware/cpu.py +150 -0
- greentrace-0.1.0/GreenTrace/hardware/gpu.py +0 -0
- greentrace-0.1.0/GreenTrace/reporting.py +206 -0
- greentrace-0.1.0/GreenTrace/tracker.py +293 -0
- greentrace-0.1.0/GreenTrace.egg-info/PKG-INFO +198 -0
- greentrace-0.1.0/GreenTrace.egg-info/SOURCES.txt +24 -0
- greentrace-0.1.0/GreenTrace.egg-info/dependency_links.txt +1 -0
- greentrace-0.1.0/GreenTrace.egg-info/requires.txt +4 -0
- greentrace-0.1.0/GreenTrace.egg-info/top_level.txt +2 -0
- greentrace-0.1.0/LICENSE +9 -0
- greentrace-0.1.0/PKG-INFO +198 -0
- greentrace-0.1.0/README.md +180 -0
- greentrace-0.1.0/example/__init__.py +0 -0
- greentrace-0.1.0/example/console_async.py +22 -0
- greentrace-0.1.0/example/console_sync.py +22 -0
- greentrace-0.1.0/example/monitoring.py +46 -0
- greentrace-0.1.0/example/report_csv.py +28 -0
- greentrace-0.1.0/example/report_html.py +20 -0
- greentrace-0.1.0/pyproject.toml +12 -0
- greentrace-0.1.0/setup.cfg +4 -0
- greentrace-0.1.0/setup.py +22 -0
- greentrace-0.1.0/tests/test_tracker.py +186 -0
|
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}")
|