roverdevkit / scripts /run_power_prediction.py
jjreif's picture
Deploy roverdevkit @ 2676a67
b3d14e3
Raw
History Blame Contribute Delete
7.96 kB
"""Run the de-tuned peak-solar prediction and write the paper artifacts (§5.3).
Replaces the *circular* flown-rover peak-solar band check (which used per-rover
``panel_efficiency`` / ``panel_dust_factor`` chosen to match each rover's
published number) with an *honest* forward prediction
(:mod:`roverdevkit.validation.power_prediction`): a single, fixed,
literature-justified panel parameter set is applied uniformly to every rover,
and the only rover-specific inputs are published solar-array area and scenario
latitude. The prediction is allowed to be wrong, and the residual is reported.
Headline result the artifacts capture:
- Pragyan (fresh array, single lunar day): the de-tuned beginning-of-life
clean-array prediction lands inside the published band with single-digit
percent error and stays in-band across the full literature cell-efficiency
range -- a genuine, zero-tuning predictive hit.
- Yutu-2 (dozens of lunar days): the BOL prediction over-predicts the published
operational peak by ~2x; the implied net derate we back out (published / BOL)
is consistent with multi-year dust + end-of-life degradation. Reported as a
recovered output, not a tuned input.
Outputs (under ``--out-dir``, default ``reports/power_prediction``):
- ``summary.csv`` -- one row per flown rover with geometry, the de-tuned
predictions, the sensitivity band, the published band, in-band flag, percent
error, and the implied total derate.
- ``power_prediction_report.md`` -- human-readable rollup.
Usage
-----
::
python scripts/run_power_prediction.py
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
import pandas as pd
from roverdevkit.validation.power_prediction import (
CELL_EFFICIENCY_BOL,
CELL_EFFICIENCY_RANGE,
CLEAN_DUST_FACTOR,
ELECTRICAL_DERATE,
HIGH_TEMP_DERATE,
PACKING_FACTOR,
SYSTEM_EFFICIENCY,
DetunedPowerPrediction,
format_report,
predict_all_flown,
)
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
p = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
p.add_argument("--out-dir", type=Path, default=Path("reports/power_prediction"))
return p.parse_args(argv)
def _to_frame(predictions: tuple[DetunedPowerPrediction, ...]) -> pd.DataFrame:
rows: list[dict[str, object]] = []
for p in predictions:
rows.append(
{
"rover_name": p.rover_name,
"latitude_deg": p.latitude_deg,
"panel_area_m2": p.panel_area_m2,
"peak_elevation_deg": round(p.peak_elevation_deg, 2),
"mission_duration_days": p.mission_duration_days,
"predicted_bol_w": round(p.predicted_bol_w, 1),
"predicted_clean_w": round(p.predicted_clean_w, 1),
"sensitivity_low_w": round(p.sensitivity_low_w, 1),
"sensitivity_high_w": round(p.sensitivity_high_w, 1),
"published_w": p.published_w,
"band_low_w": p.band_low_w,
"band_high_w": p.band_high_w,
"in_band": p.in_band,
"pct_error_vs_published": round(p.pct_error_vs_published, 1),
"implied_total_derate": round(p.implied_total_derate, 3),
}
)
return pd.DataFrame(rows)
def _markdown(df: pd.DataFrame, predictions: tuple[DetunedPowerPrediction, ...]) -> str:
lines = [
"# De-tuned peak-solar prediction (flown rovers)",
"",
"**What this is.** A genuine forward prediction of peak noon solar power",
"for the flown rovers using a *single, fixed, literature-justified* panel",
"parameter set applied uniformly -- no per-rover calibration. The only",
"rover-specific inputs are published solar-array area and scenario",
"latitude. This replaces the earlier circular band check, which tuned",
"`panel_efficiency` / `panel_dust_factor` per rover to match each rover's",
"own published number.",
"",
"**Fixed literature parameter set (every rover).**",
"",
"| factor | value | source |",
"|---|---|---|",
f"| cell efficiency (BOL AM0) | {CELL_EFFICIENCY_BOL:.2f} | "
"triple-junction GaAs/Ge (Spectrolab XTJ / AzurSpace 3G30) |",
f"| packing factor | {PACKING_FACTOR:.2f} | Patel Ch. 4 |",
f"| electrical derate (MPPT+harness+diode) | {ELECTRICAL_DERATE:.2f} | SMAD Ch. 11 |",
f"| high-temperature derate (lunar noon) | {HIGH_TEMP_DERATE:.2f} | "
"GaAs power coefficient |",
f"| **net system efficiency** | **{SYSTEM_EFFICIENCY:.3f}** | product of the above |",
f"| clean-array dust factor | {CLEAN_DUST_FACTOR:.2f} | fresh / lunar-day-1 array |",
"",
"The sensitivity band sweeps cell efficiency over "
f"{CELL_EFFICIENCY_RANGE[0]:.2f}-{CELL_EFFICIENCY_RANGE[1]:.2f} so no single",
"efficiency choice is load-bearing.",
"",
"## Per-rover predictions",
"",
"| rover | area (m^2) | noon elev (deg) | pred BOL (W) | pred clean (W) | "
"sensitivity (W) | published (W) | band (W) | in-band | err % | implied derate |",
"|---|---|---|---|---|---|---|---|---|---|---|",
]
for p in predictions:
lines.append(
f"| {p.rover_name} | {p.panel_area_m2:.2f} | {p.peak_elevation_deg:.1f} | "
f"{p.predicted_bol_w:.1f} | {p.predicted_clean_w:.1f} | "
f"{p.sensitivity_low_w:.0f}-{p.sensitivity_high_w:.0f} | "
f"{p.published_w:.0f} | {p.band_low_w:.0f}-{p.band_high_w:.0f} | "
f"{'yes' if p.in_band else '**no**'} | {p.pct_error_vs_published:+.1f} | "
f"{p.implied_total_derate:.2f} |"
)
lines += [
"",
"## Interpretation",
"",
"- **Fresh arrays predict cleanly.** A rover that flew a single lunar day",
" reports a near-beginning-of-life peak; the de-tuned BOL + clean-array",
" prediction lands inside its published band with single-digit percent",
" error and stays in-band across the full literature cell-efficiency",
" range. That is a real predictive hit with zero per-rover tuning.",
"- **Aged arrays expose their degradation rather than hide it.** A rover",
" that operated for dozens of lunar days reports a heavily dust- and",
" end-of-life-degraded operational peak. The BOL prediction over-predicts",
" it, and the implied net derate (published / BOL) that we back out is",
" independently consistent with multi-year lunar dust accumulation plus",
" EOL cell degradation. We report that derate as a recovered output, not",
" a tuned input.",
"",
"Net: with literature BOL clean-array parameters and no per-rover",
"calibration, the power sub-model predicts the fresh-array rover within",
"its published band; the only residual is a physically attributable aging",
"derate on the multi-year rover. Regenerate via",
"`scripts/run_power_prediction.py`.",
"",
"```",
format_report(predictions),
"```",
"",
]
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
args = _parse_args(argv)
predictions = predict_all_flown()
df = _to_frame(predictions)
args.out_dir.mkdir(parents=True, exist_ok=True)
csv_path = args.out_dir / "summary.csv"
df.to_csv(csv_path, index=False)
md_path = args.out_dir / "power_prediction_report.md"
md_path.write_text(_markdown(df, predictions))
print(f"Wrote 2 artifact(s) to {args.out_dir}:")
print(f" csv: {csv_path}")
print(f" report: {md_path}")
print()
print(format_report(predictions))
return 0
if __name__ == "__main__":
sys.exit(main())