|
8 | 8 |
|
9 | 9 | from __future__ import annotations |
10 | 10 |
|
| 11 | +import hashlib |
11 | 12 | import io |
12 | 13 | import json |
13 | 14 | import logging |
|
18 | 19 | import sys |
19 | 20 | from contextlib import redirect_stdout |
20 | 21 | from dataclasses import dataclass, field |
21 | | -from datetime import UTC, datetime |
| 22 | +from datetime import datetime, timezone |
22 | 23 | from pathlib import Path |
23 | | -from typing import Dict, List, Optional, Tuple |
| 24 | +from typing import Any, Dict, List, Optional, Tuple |
24 | 25 |
|
25 | 26 | import matplotlib.pyplot as plt |
26 | 27 | import numpy as np |
@@ -795,7 +796,7 @@ def _availability_curve( |
795 | 796 | write_csv_atomic(scen_dir / "network_stats_summary.csv", ns_df) |
796 | 797 |
|
797 | 798 | provenance = { |
798 | | - "generated_at": datetime.now(UTC).isoformat(), |
| 799 | + "generated_at": datetime.now(timezone.utc).isoformat(), |
799 | 800 | "python": sys.version, |
800 | 801 | "platform": platform.platform(), |
801 | 802 | } |
@@ -911,6 +912,18 @@ def _availability_curve( |
911 | 912 | print(text, end="") |
912 | 913 | print(f"Wrote project CSV: {project_csv}") |
913 | 914 |
|
| 915 | + # Create and save comprehensive provenance information for metrics run |
| 916 | + metrics_provenance = _create_metrics_provenance(root, out_root, files, only) |
| 917 | + |
| 918 | + # Add scenarios and seeds analyzed |
| 919 | + for scenario_stem, seed_map in grouped.items(): |
| 920 | + metrics_provenance["scenarios_analyzed"].append(scenario_stem) |
| 921 | + metrics_provenance["seeds_analyzed"][scenario_stem] = sorted(seed_map.keys()) |
| 922 | + |
| 923 | + provenance_path = out_root / "provenance.json" |
| 924 | + write_json_atomic(provenance_path, metrics_provenance) |
| 925 | + print(f"📋 Metrics provenance saved to: {provenance_path}") |
| 926 | + |
914 | 927 |
|
915 | 928 | def print_summary_from_csv( |
916 | 929 | root: Path, plots: bool = False, quiet: bool = False |
@@ -1042,6 +1055,8 @@ def _plot_dist_abs(column: str, title: str, ylabel: str, fname: str) -> None: |
1042 | 1055 | "lat_fail_p99": "lat_fail_p99", |
1043 | 1056 | "USD_per_Gbit_offered": "USD_per_Gbit_offered", |
1044 | 1057 | "USD_per_Gbit_p999": "USD_per_Gbit_p999", |
| 1058 | + "Watt_per_Gbit_offered": "Watt_per_Gbit_offered", |
| 1059 | + "Watt_per_Gbit_p999": "Watt_per_Gbit_p999", |
1045 | 1060 | "capex_total": "capex_total", |
1046 | 1061 | "node_count": "node_count", |
1047 | 1062 | "link_count": "link_count", |
@@ -1177,6 +1192,18 @@ def _plot_dist_norm(column: str, title: str, ylabel: str, fname: str) -> None: |
1177 | 1192 | ylabel="USD/Gbps", |
1178 | 1193 | fname="abs_USD_per_Gbit_p999.png", |
1179 | 1194 | ) |
| 1195 | + _plot_dist_abs( |
| 1196 | + "Watt_per_Gbit_offered", |
| 1197 | + title="Power per Gbps (offered)", |
| 1198 | + ylabel="W/Gbps", |
| 1199 | + fname="abs_Watt_per_Gbit_offered.png", |
| 1200 | + ) |
| 1201 | + _plot_dist_abs( |
| 1202 | + "Watt_per_Gbit_p999", |
| 1203 | + title="Power per Gbps at p99.9", |
| 1204 | + ylabel="W/Gbps", |
| 1205 | + fname="abs_Watt_per_Gbit_p999.png", |
| 1206 | + ) |
1180 | 1207 | _plot_dist_abs( |
1181 | 1208 | "lat_fail_p99", |
1182 | 1209 | title="Latency p99 under failures (median across seeds)", |
@@ -1217,9 +1244,75 @@ def _plot_dist_norm(column: str, title: str, ylabel: str, fname: str) -> None: |
1217 | 1244 | "ratio", |
1218 | 1245 | "norm_USD_per_Gbit_p999.png", |
1219 | 1246 | ) |
| 1247 | + _plot_dist_norm( |
| 1248 | + "Watt_per_Gbit_offered_r", |
| 1249 | + "Power per Gbps (offered, relative)", |
| 1250 | + "ratio", |
| 1251 | + "norm_Watt_per_Gbit_offered.png", |
| 1252 | + ) |
| 1253 | + _plot_dist_norm( |
| 1254 | + "Watt_per_Gbit_p999_r", |
| 1255 | + "Power per Gbps p99.9 (relative)", |
| 1256 | + "ratio", |
| 1257 | + "norm_Watt_per_Gbit_p999.png", |
| 1258 | + ) |
1220 | 1259 | _plot_dist_norm( |
1221 | 1260 | "lat_fail_p99_r", |
1222 | 1261 | "Latency p99 under failures (relative)", |
1223 | 1262 | "ratio", |
1224 | 1263 | "norm_Latency_fail_p99.png", |
1225 | 1264 | ) |
| 1265 | + |
| 1266 | + |
| 1267 | +def _create_metrics_provenance( |
| 1268 | + root: Path, out_root: Path, files: List[Path], only: Optional[str] = None |
| 1269 | +) -> Dict[str, Any]: |
| 1270 | + """Create comprehensive provenance information for a metrics run.""" |
| 1271 | + provenance: Dict[str, Any] = { |
| 1272 | + "generated_at": datetime.now(timezone.utc).isoformat(), |
| 1273 | + "python": sys.version, |
| 1274 | + "platform": platform.platform(), |
| 1275 | + "command": "metrics", |
| 1276 | + "source_root": str(root), |
| 1277 | + "output_root": str(out_root), |
| 1278 | + "source_files": {}, |
| 1279 | + "scenarios_analyzed": [], |
| 1280 | + "seeds_analyzed": {}, |
| 1281 | + } |
| 1282 | + |
| 1283 | + # Get git commit if available |
| 1284 | + try: |
| 1285 | + commit = ( |
| 1286 | + subprocess.check_output( |
| 1287 | + ["git", "rev-parse", "HEAD"], stderr=subprocess.DEVNULL |
| 1288 | + ) |
| 1289 | + .decode("utf-8") |
| 1290 | + .strip() |
| 1291 | + ) |
| 1292 | + provenance["git_commit"] = commit |
| 1293 | + except Exception as e: |
| 1294 | + logging.warning("Failed to retrieve git commit: %s", e) |
| 1295 | + provenance["git_commit_error"] = str(e) |
| 1296 | + |
| 1297 | + # Add source file information with hashes |
| 1298 | + for file_path in files: |
| 1299 | + try: |
| 1300 | + file_content = file_path.read_bytes() |
| 1301 | + file_hash = hashlib.sha256(file_content).hexdigest() |
| 1302 | + provenance["source_files"][str(file_path.relative_to(root))] = { |
| 1303 | + "path": str(file_path.relative_to(root)), |
| 1304 | + "sha256": file_hash, |
| 1305 | + "size_bytes": len(file_content), |
| 1306 | + } |
| 1307 | + except Exception as e: |
| 1308 | + logging.warning("Failed to hash source file %s: %s", file_path, e) |
| 1309 | + provenance["source_files"][str(file_path.relative_to(root))] = { |
| 1310 | + "path": str(file_path.relative_to(root)), |
| 1311 | + "hash_error": str(e), |
| 1312 | + } |
| 1313 | + |
| 1314 | + # Add analysis scope information |
| 1315 | + if only: |
| 1316 | + provenance["only_scenarios"] = [s.strip() for s in only.split(",") if s.strip()] |
| 1317 | + |
| 1318 | + return provenance |
0 commit comments