OverviewFilesCommitsIssuesDocsReleasesPeers

electrical-review / scripts / 01b_vocmax.py

75 lines · python ref:
"""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()