OverviewFilesCommitsIssuesDocsReleasesPeers

electrical-review / scripts / 02_production.py

114 lines · python ref:
"""02 — Production / per-MPPT energy sanity (PVWatts v8, fixed open-rack).

Annual + monthly AC kWh for the ~8 kW array, with the winter (Dec/Jan) worst-case
that actually governs an off-grid system. Per-MPPT DC power check vs the inverter.

Ground truth: NREL PVWatts V8 REST API. The host is VERIFIED empirically here
(the project docs claim a 2026-05-29 migration to developer.nlr.gov; we test the
canonical developer.nrel.gov first and only fall back if it fails).

Run:  ~/.venvs/prsm-electrical/bin/python scripts/02_production.py
Uses NREL_API_KEY if set, else DEMO_KEY (rate-limited but works for PVWatts).
"""
import os
import requests
from common import load_site, require, banner, result, dump_result, load_result

HOSTS = ["https://developer.nrel.gov", "https://developer.nlr.gov"]
PATH = "/api/pvwatts/v8.json"
MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
          "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]


def call_pvwatts(params):
    """Try each host; return (json, host, err)."""
    last = None
    for host in HOSTS:
        try:
            r = requests.get(host + PATH, params=params, timeout=30)
            if r.status_code == 200 and "outputs" in r.json():
                return r.json(), host, None
            last = f"{host}: HTTP {r.status_code} {r.text[:120]}"
        except Exception as e:  # noqa
            last = f"{host}: {type(e).__name__}: {e}"
    return None, None, last


def main():
    cfg = load_site()
    banner("02  PRODUCTION  (PVWatts v8, fixed open-rack array_type=0)")
    s01 = load_result("01_string")

    modules = int(require(cfg, "array.module_count"))
    pmp = require(cfg, "panel.pmp_w")
    kw = round(modules * pmp / 1000.0, 2)
    tilt = require(cfg, "array.tilt_deg")
    az = require(cfg, "array.azimuth_deg")
    lat = require(cfg, "site.latitude")
    lon = require(cfg, "site.longitude")
    bif = require(cfg, "panel.bifaciality")   # BOM panel = 0.80 (bifacial)
    gcr = require(cfg, "array.gcr")            # Phase-1 single-row = 0.30 (flagged in site.yaml)
    key = os.environ.get("NREL_API_KEY", "DEMO_KEY")

    result("system_capacity", kw, "kW DC", f"{modules} x {pmp}W (BOM Waaree BiN-08-575 bifacial)")
    result("tilt / azimuth", f"{tilt} / {az}", "deg")
    result("bifaciality / gcr", f"{bif} / {gcr}", "", "BOM bifacial rear-gain (PVWatts v8); single-row Phase 1")
    result("api key", "NREL_API_KEY" if key != "DEMO_KEY" else "DEMO_KEY (rate-limited)")

    params = {"api_key": key, "system_capacity": kw, "module_type": 0,
              "array_type": 0, "tilt": tilt, "azimuth": az, "losses": 14,
              "lat": lat, "lon": lon, "dc_ac_ratio": round(kw / 6.0, 2),
              "bifaciality": bif, "gcr": gcr}
    data, host, err = call_pvwatts(params)
    if data is None:
        result("PVWatts", "UNAVAILABLE", "", err)
        result("note", "production is a sanity check; safety calcs (01/03/04) do not depend on it")
        dump_result("02_production", {"status": "unavailable", "error": err,
                                      "system_kw": kw})
        raise SystemExit("PVWatts call failed; rerun with a valid NREL_API_KEY.")

    out = data["outputs"]
    result("VERIFIED host", host, "", "developer.nrel.gov canonical"
           if "nrel" in host else "fell back to alt host")
    ac_annual = out["ac_annual"]
    ac_monthly = out["ac_monthly"]
    cf = out.get("capacity_factor")
    result("AC annual", round(ac_annual), "kWh/yr")
    result("capacity factor", round(cf, 1) if cf else "n/a", "%")
    print("\n  Monthly AC (kWh) + kWh/day:")
    for i, m in enumerate(MONTHS):
        print(f"    {m}  {ac_monthly[i]:8.0f}   {ac_monthly[i]/DAYS[i]:5.1f}/day")
    dec_day = ac_monthly[11] / DAYS[11]
    jan_day = ac_monthly[0] / DAYS[0]
    jun_day = ac_monthly[5] / DAYS[5]
    print()
    result("WINTER Dec / Jan kWh/day", f"{dec_day:.1f} / {jan_day:.1f}", "",
           "off-grid binding case")
    result("Summer Jun kWh/day", f"{jun_day:.1f}", "", "for contrast")
    result("winter:summer ratio", f"{dec_day/jun_day:.2f}", "",
           "NEK winter PV starvation -> generator + Phase-2 PV (Swarm §2,§6)")

    # per-MPPT DC power check (from 01)
    print("\n  Per-MPPT loading (DC nameplate, from 01):")
    result("string DC power", s01["string_power_W"], "W",
           f"vs 4000 usable / 5000 oversize cap per MPPT")
    result("array DC total", s01["array_dc_W"], "W", "vs 8000 usable / 10000 oversize")
    result("DC:AC ratio", round(kw / 6.0, 2), "", "winter-favorable oversizing")

    dump_result("02_production", {
        "status": "ok", "verified_host": host, "system_kw": kw,
        "panel": "Waaree BiN-08-575 bifacial (Grade B) — BOM",
        "bifaciality": bif, "gcr": gcr,
        "ac_annual_kwh": round(ac_annual), "capacity_factor_pct": cf,
        "ac_monthly_kwh": [round(x) for x in ac_monthly],
        "dec_kwh_day": round(dec_day, 1), "jan_kwh_day": round(jan_day, 1),
        "jun_kwh_day": round(jun_day, 1),
        "string_dc_W": s01["string_power_W"], "array_dc_W": s01["array_dc_W"],
        "dc_ac_ratio": round(kw / 6.0, 2),
    })


if __name__ == "__main__":
    main()