#!/usr/bin/env python3 """Reproduce the Uptime reference arithmetic and verify CSV/JSON consistency. Usage: python rolling-vs-sectional-calculations.py --verify No network or third-party modules are required. This verifies published data and illustrative arithmetic, not equipment, engineering suitability or the live site. """ from __future__ import annotations import argparse import csv import json import math import sys from pathlib import Path from typing import Any def finite_number(value: float, name: str) -> float: result = float(value) if not math.isfinite(result): raise ValueError(f"{name} must be finite") return result def years_to_accumulate(cycles: float, cycles_per_day: float, days_per_year: float) -> float: """Hypothetical accumulation time; never a failure date or warranty.""" cycles = finite_number(cycles, "cycles") per_day = finite_number(cycles_per_day, "cycles_per_day") days = finite_number(days_per_year, "days_per_year") if cycles < 0 or per_day <= 0 or not 0 < days <= 366: raise ValueError("Use nonnegative cycles, positive daily cycles and 1–366 operating days/year") return cycles / (per_day * days) def spaced_event_count(cycles: int, interval_cycles: int = 1500) -> int: """Count events spaced from the first full interval; excludes initial prep. This is an illustrative spacing calculation. Manufacturer instructions may permit less maintenance, and this is not a field maintenance schedule. """ if isinstance(cycles, bool) or isinstance(interval_cycles, bool): raise ValueError("Cycle counts must be integers, not booleans") if not isinstance(cycles, int) or not isinstance(interval_cycles, int): raise ValueError("Cycle counts and intervals must be integers") if cycles < 0 or interval_cycles <= 0: raise ValueError("Use nonnegative cycles and a positive interval") return cycles // interval_cycles def disclosed_ratio(numerator: float, denominator: float) -> float: """Ratio of named values with matched units; not an energy estimate.""" numerator = finite_number(numerator, "numerator") denominator = finite_number(denominator, "denominator") if numerator < 0 or denominator <= 0: raise ValueError("Use a nonnegative numerator and a positive denominator") return numerator / denominator def require(condition: bool, message: str) -> None: if not condition: raise ValueError(message) def expected_error(function: Any, *args: Any) -> None: try: function(*args) except (ValueError, TypeError): return raise ValueError(f"Expected invalid input to fail: {function.__name__}{args!r}") def self_test() -> int: require(math.isclose(years_to_accumulate(50000, 20, 365), 50000 / 7300), "Normal workload case") require(years_to_accumulate(0, 20, 260) == 0, "Zero target cycles") require(years_to_accumulate(1, 1, 1) == 1, "One-cycle boundary") for args in [(100, 0, 260), (100, 20, 0), (-1, 20, 260), (100, 20, 367), (100, float('nan'), 260)]: expected_error(years_to_accumulate, *args) for count, expected in [(0, 0), (1499, 0), (1500, 1), (1501, 1), (100000, 66)]: require(spaced_event_count(count) == expected, f"Event boundary {count}") expected_error(spaced_event_count, 100, 0) expected_error(spaced_event_count, -1, 1500) expected_error(spaced_event_count, 1.5, 1500) require(math.isclose(disclosed_ratio(.532, .13), 4.092307692307692), "Normal ratio") require(disclosed_ratio(0, .13) == 0, "Zero numerator") require(disclosed_ratio(.13, .13) == 1, "Equal inputs") expected_error(disclosed_ratio, .532, 0) expected_error(disclosed_ratio, -.532, .13) return 21 def load_csv(path: Path) -> list[dict[str, str]]: with path.open(encoding="utf-8-sig", newline="") as handle: return list(csv.DictReader(handle)) def verify(directory: Path) -> dict[str, Any]: manifest_path = directory / "rolling-vs-sectional-ledger.json" bundle = json.loads(manifest_path.read_text(encoding="utf-8")) loaded: dict[str, list[dict[str, str]]] = {} total = 0 for filename, description in bundle["files"].items(): require(Path(filename).name == filename, "Expected a local flat CSV filename") rows = load_csv(directory / filename) key = description["table_key"] require(len(rows) == description["row_count"], f"Row count mismatch: {filename}") normalized = [{k: (None if v == "" else v) for k, v in row.items()} for row in rows] require(normalized == bundle["tables"][key], f"CSV/JSON field or value mismatch: {filename}") id_field = "source_id" if key == "sources" else "field_no" if key == "quote_checklist" else "row_id" ids = [r[id_field] for r in rows] require(len(ids) == len(set(ids)), f"Duplicate identifiers: {filename}") loaded[key] = rows total += len(rows) require(total == bundle["total_csv_rows"], "Total CSV row mismatch") calculations = loaded["calculations"] require(len(calculations) == 21, "Expected all 21 published calculations") for row in calculations: identifier = row["row_id"] if identifier.startswith("CY"): result = years_to_accumulate(float(row["input_cycles"]), float(row["input_cycles_per_day"]), float(row["input_days_per_year"])) shown = f"{result:.1f}" elif identifier.startswith("MT"): result = spaced_event_count(int(row["input_cycles"]), int(row["input_interval_cycles"])) shown = str(result) elif identifier == "TH01": result = disclosed_ratio(float(row["input_numerator"]), float(row["input_denominator"])) shown = f"{result:.2f}" else: raise ValueError(f"Unknown calculation: {identifier}") require(math.isclose(float(row["result_unrounded"]), result, rel_tol=1e-12, abs_tol=1e-12), f"Arithmetic mismatch: {identifier}") require(row["result_display"] == shown, f"Display rounding mismatch: {identifier}") figures = {r["row_id"]: r for r in loaded["published_figures"]} require(figures["PF16"]["value"] == "", "Unknown ESD40 leakage must stay empty") require(figures["PF35"]["value"] == "20000" and figures["PF36"]["value"] == "10000", "Standard spring fields changed") require(figures["PF38"]["value"] == "0.19" and figures["PF39"]["value"] == "0.39", "Corrected Clopay U fields changed") require(figures["PF34"]["value"] == f"{disclosed_ratio(.532, .13):.2f}", "Thermal ratio summary mismatch") require(figures["PF31"]["value"] == "6.85 at 20/day x 365; 9.62 at 20/day x 260; 4.81 at 40/day x 260; 3.21 at 60/day x 260", "Composite cycle summary changed") require(figures["PF33"]["value"] == "6 at 10,000; 16 at 25,000; 33 at 50,000; 66 at 100,000", "Maintenance summary mismatch") return {"status": "PASS", "version": bundle["version"], "csv_files": len(loaded), "csv_rows": total, "calculated_rows": len(calculations), "edge_cases_passed": self_test(), "scope": "Local files and arithmetic only; not independent source re-verification or live-site validation"} def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--verify", action="store_true", help="Verify all CSV/JSON rows and calculations (also the default)") parser.add_argument("--data-dir", type=Path, default=Path(__file__).resolve().parent, help="Folder containing the data files") args = parser.parse_args() try: print(json.dumps(verify(args.data_dir), indent=2)) return 0 except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as error: print(f"Verification failed: {error}", file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main())