Spaces:
Running
Running
| """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()) | |