"""01b — vocmax named-tool corroboration of the cold-Voc result.
CLAUDE.md names `vocmax` as the NEC 690.7 grounding tool. vocmax 1.0.4 is incompatible
with the main env's pvlib 0.15 (it imports the removed get_psm3), so it runs in a pinned
venv: ~/.venvs/prsm-vocmax (python3.9 + pvlib 0.9.5 + numpy<2).
This computes module Voc at the design low temperature with vocmax's own Sandia Voc
model (vocmax.calculate_voc) and derives the max string length vs the inverter ceilings —
a fourth, independent check on the 7S result from 01_string_sizing.py.
NOTE: vocmax's full value is its NSRDB statistical worst-case (P99.5 etc.), which needs
an NREL key. At full-sun (Ee=1) the Sandia Voc reduces to the linear 690.7 form, so this
offline run confirms the per-module Voc; the NSRDB workflow is the open item.
Run: ~/.venvs/prsm-vocmax/bin/python scripts/01b_vocmax.py
"""
import vocmax
from common import load_site, require, banner, result, dump_result
def main():
cfg = load_site()
banner("01b VOCMAX named-tool corroboration (offline, design-low temp)")
voc = require(cfg, "panel.voc_v")
b_voc_pct = require(cfg, "panel.voc_tempco_pct_per_C")
cells = int(require(cfg, "panel.cells_electrical_series"))
t_min = require(cfg, "site.design_low_temp_C")
inv_design = require(cfg, "inverter.pv_design_voc_ceiling_v")
inv_abs = require(cfg, "inverter.pv_max_voc_v")
module = {
"Voco": voc,
"Bvoco": b_voc_pct / 100.0 * voc, # V/C
"cells_in_series": cells,
"n_diode": 1.1,
"FD": 1.0,
}
result("vocmax version", vocmax.__version__)
result("module Voco / Bvoco", f"{module['Voco']} / {module['Bvoco']:.4f}", "V , V/C")
voc_stc = float(vocmax.calculate_voc(1000.0, 25.0, module))
voc_cold = float(vocmax.calculate_voc(1000.0, t_min, module)) # full-sun, design low
result("vocmax Voc @ 25C", round(voc_stc, 2), "V", "should reproduce Voco")
result("vocmax Voc @ Tmin", round(voc_cold, 2), "V", f"{t_min} C, Ee=1")
max_series_design = int(inv_design // voc_cold)
max_series_abs = int(inv_abs // voc_cold)
# String to validate: the even-fill per-MPPT length (module_count // mppt_count).
n_series = require(cfg, "array.module_count") // require(cfg, "inverter.mppt_count")
string_cold = n_series * voc_cold
result(f"{n_series}S string cold Voc", round(string_cold, 1), "V",
f"<= {inv_design} ({'PASS' if string_cold <= inv_design else 'FAIL'})")
result(f"max modules/series <={inv_design:.0f}", max_series_design)
result(f"max modules/series <={inv_abs:.0f}", max_series_abs)
result("VOCMAX VERDICT", f"{n_series}S PASS" if (n_series <= max_series_design) else "REVIEW")
dump_result("01b_vocmax", {
"phase": "phase1",
"vocmax_version": vocmax.__version__,
"voc_module_stc_V": round(voc_stc, 2),
"voc_module_cold_V": round(voc_cold, 2),
"tmin_C": t_min,
"design_ceiling_V": inv_design, "abs_ceiling_V": inv_abs,
"string_series_checked": n_series,
"string_cold_voc_V": round(string_cold, 1),
"max_series_le_design": max_series_design,
"max_series_le_abs": max_series_abs,
"verdict": f"{n_series}S PASS" if (n_series <= max_series_design) else "REVIEW",
"note": "offline full-sun Sandia Voc (=linear 690.7); NSRDB statistical run needs NREL key",
})
if __name__ == "__main__":
main()