"""09 — Phase-2 sizing + cost: wire the REMAINING pallet panels to meet the load.
Phase-1 wired 14 of the 31-panel Waaree pallet (8.05 kW) and that already MAXES the
single EG4 6000XP PV input (8 kW usable/unit). To use the remaining 17 panels (9.78 kW
-> 17.8 kW full pallet) you must add INVERTER capacity; the panels themselves are already
paid for (sunk in the pallet) = $0 additional.
Computes (grounded): full-pallet production (PVWatts), coverage vs the real Hardwick load
(08), and battery autonomy. Cost figures are ESTIMATES (sourced: Swarm §10 / Key Decisions),
not computed — clearly labeled.
Run: ~/.venvs/prsm-electrical/bin/python scripts/09_phase2.py (uses NREL_API_KEY or DEMO_KEY)
"""
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"]
DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
# --- COST ESTIMATES (USD) — NOT computed; sourced as noted. Ranges (lo, hi). ---
INV_6000XP = 1549 # Key Decisions (retail)
PARALLEL_KIT = 150 # parallel comms/cabling kit (est)
DOCAN = (1758, 2000) # Key Decisions base SKU (heating is propane/separate)
BOS_PH2 = (800, 1500) # PV wire for 17 panels, DC disc, battery interconnects, AC upsize, lugs/MC4 (est)
def pvwatts(kw, dc_ac, cfg):
key = os.environ.get("NREL_API_KEY", "DEMO_KEY")
params = {"api_key": key, "system_capacity": kw, "module_type": 0, "array_type": 0,
"tilt": require(cfg, "array.tilt_deg"), "azimuth": require(cfg, "array.azimuth_deg"),
"losses": 14, "lat": require(cfg, "site.latitude"),
"lon": require(cfg, "site.longitude"), "dc_ac_ratio": round(dc_ac, 3),
"bifaciality": require(cfg, "panel.bifaciality"), # BOM bifacial 0.80
"gcr": require(cfg, "phase2.array.gcr")} # MULTI-ROW Phase-2 (flagged)
for host in HOSTS:
try:
r = requests.get(host + "/api/pvwatts/v8.json", params=params, timeout=30)
if r.status_code == 200 and "outputs" in r.json():
return r.json()["outputs"], host
except Exception: # noqa
pass
return None, None
def rng(lo, hi):
return f"${lo:,.0f}-{hi:,.0f}"
def main():
cfg = load_site()
banner("09 PHASE-2 — use remaining pallet to meet the load (sizing + cost)")
load = load_result("08_load_actual")
pmp = require(cfg, "panel.pmp_w")
n_total, n_ph1 = 31, int(require(cfg, "array.module_count"))
n_remaining = n_total - n_ph1
kw_ph1 = round(n_ph1 * pmp / 1000, 2)
kw_full = round(n_total * pmp / 1000, 2)
result("pallet / Phase-1 / remaining", f"{n_total} / {n_ph1} / {n_remaining}", "panels")
result("array kW: Phase-1 -> full", f"{kw_ph1} -> {kw_full}", "kW DC")
result("CONSTRAINT", "1× 6000XP PV input = 8 kW usable", "",
"Phase-1 8.05 kW already maxes it -> Phase-2 needs more inverter")
# --- production at full pallet (12 kW AC from expanded inverter) ---
out, host = pvwatts(kw_full, kw_full / 12.0, cfg)
if out is None:
raise SystemExit("PVWatts unavailable; set NREL_API_KEY and retry.")
m = out["ac_monthly"]
ann = out["ac_annual"]
dec = m[11] / DAYS[11]
winter = (m[0] + m[11]) / (DAYS[0] + DAYS[11])
summer = sum(m[i] / DAYS[i] for i in (5, 6, 7)) / 3
print(f"\n FULL-PALLET PRODUCTION ({kw_full} kW, via {host.split('//')[1]}):")
result(" annual", round(ann), "kWh/yr", f"vs load {load['last12_kwh_yr']} kWh/yr")
result(" array = % of annual load", round(100 * ann / load["last12_kwh_yr"]), "%",
"now EXCEEDS load (was 79% at Phase-1)")
result(" Dec / winter avg", f"{dec:.1f} / {winter:.1f}", "kWh/day",
f"vs winter load {load['seasonal_daily_kwh']['winter']}")
result(" summer avg", round(summer, 1), "kWh/day",
f"vs summer load {load['seasonal_daily_kwh']['summer']} / Aug {load['august_peak_kwh_day']}")
# coverage vs load by season (solar-only, usage band 0.85)
print("\n COVERAGE vs load (full pallet, x0.85 usable):")
seas = load["seasonal_daily_kwh"]
for name, prod_d, ld in [("winter", winter, seas["winter"]),
("summer", summer, seas["summer"]),
("Aug peak", summer, load["august_peak_kwh_day"])]:
cov = 100 * (0.85 * prod_d) / ld
result(f" {name}", f"{0.85*prod_d:.0f} usable vs {ld} load", "kWh/day",
f"{cov:.0f}% ({'MET' if cov >= 100 else 'gen/short'})")
# --- battery autonomy at the load ---
usable_pack = round(require(cfg, "battery.usable_kwh") * require(cfg, "battery.dod_design"), 2)
avg = load["daily_avg_kwh"]; peak = load["august_peak_kwh_day"]
print(f"\n BATTERY AUTONOMY ({usable_pack} kWh usable/pack @ 80% DoD):")
print(f" {'packs':>6}{'usable':>9}{'days@avg':>11}{'days@peak':>11}")
for n in (1, 2, 3):
u = usable_pack * n
print(f" {n:>6}{u:>7.1f}kWh{u/avg:>10.2f}{u/peak:>11.2f}")
result("Swarm §6 battery target", "24-30 kWh usable", "", "= 2 packs (25.7 usable); 3 for ~1 day @ avg")
# --- COST (estimates) ---
print("\n PHASE-2 ADDED COST (estimates; energy/autonomy above are computed):")
result(" remaining 17 panels", "$0", "", "already paid (sunk in pallet)")
par_inv = INV_6000XP + PARALLEL_KIT
result(" inverter: parallel 2nd 6000XP", f"${par_inv:,}", "", "12 kW AC / 16 kW PV (Key Dec §76)")
result(" battery: +1 pack (→2 total)", rng(*DOCAN), "", "25.7 kWh usable (Swarm §6 min)")
result(" battery: +2 packs (→3 total)", rng(2*DOCAN[0], 2*DOCAN[1]), "", "38.6 kWh usable, ~1 day @ avg")
result(" BoS (PV wire/disc/cables/AC upsize)", rng(*BOS_PH2))
# bundle (Path A: 2nd 6000XP + 2 packs)
lean_lo = par_inv + DOCAN[0] + BOS_PH2[0]
lean_hi = par_inv + DOCAN[1] + BOS_PH2[1]
print()
result(" BUNDLE — Path A (parallel 6000XP + 1 pack)", rng(lean_lo, lean_hi), "",
"2 packs, 12 kW AC; generator covers deep autonomy")
dump_result("09_phase2", {
"remaining_panels": n_remaining, "kw_full_pallet": kw_full,
"bifaciality": require(cfg, "panel.bifaciality"), "gcr": require(cfg, "phase2.array.gcr"),
"full_pallet_annual_kwh": round(ann), "array_pct_of_load": round(100 * ann / load["last12_kwh_yr"]),
"dec_kwh_day": round(dec, 1), "winter_kwh_day": round(winter, 1), "summer_kwh_day": round(summer, 1),
"panels_added_cost": 0,
"inverter_parallel_6000xp": par_inv,
"battery_plus1_pack": list(DOCAN), "battery_plus2_packs": [2*DOCAN[0], 2*DOCAN[1]],
"bos": list(BOS_PH2),
"bundle_pathA_usd": [lean_lo, lean_hi],
"note": "panels $0 (owned); cost figures estimates (Swarm §10/Key Decisions); energy computed",
})
if __name__ == "__main__":
main()