"""11 — Path A design (the SELECTED build): 2x EG4 6000XP (parallel), 28 panels (4x7S), 2-pack bank.
Owner's choices: 2 packs, battery adjacent (~10 ft), panels co-located on the 200 ft trench. 28 of the
31-panel pallet are wired (4x7S, 16.1 kW); the 3 remaining panels are held as field SPARES.
String safety is the already-validated Phase-1 7S result (430.1 V <= 480 V), replicated x4 across
the four MPPTs (2 per 6000XP). This script computes the Path-A CONDUCTORS and BATTERY bank for the
2-inverter topology (the bank busbar feeds two inverter inputs, so no single 250 A cable), the
production (PVWatts), and the NEC checklist. Conductor sizing reuses the validated size_run() from 03.
Run: ~/.venvs/prsm-electrical/bin/python scripts/11_pathA.py
"""
import importlib
import os
import requests
from common import load_site, require, get, banner, result, dump_result, load_result
from nec_tables import T310_16, std_ocpd_at_least, egc_awg
_c = importlib.import_module("03_conductors") # reuse validated NEC sizing
size_run, report, vdrop_pct = _c.size_run, _c.report, _c.vdrop_pct
HOSTS = ["https://developer.nrel.gov", "https://developer.nlr.gov"]
DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
def pvwatts(kw, dc_ac, cfg):
"""Path-A production (PVWatts v8, BOM bifacial; same model as 02/15)."""
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 season(m):
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
return dec, winter, summer
def main():
cfg = load_site() # base inverter: = EG4 6000XP (per-unit specs)
banner("11 PATH A — 2x EG4 6000XP (parallel), 28 panels (4x7S), 2-pack bank")
s01 = load_result("01_string") # validated Phase-1 7S string
load = load_result("08_load_actual")
pmp = require(cfg, "panel.pmp_w"); isc = require(cfg, "panel.isc_a")
imp = require(cfg, "panel.imp_a"); vmp = require(cfg, "panel.vmp_v")
vnom = require(cfg, "battery.nominal_v"); cap_ah = require(cfg, "battery.capacity_ah")
usable_kwh = require(cfg, "battery.usable_kwh"); dod = require(cfg, "battery.dod_design")
# --- topology constants ---
n_inv, npack = 2, 2
n_str, n_series, panels = 4, 7, 28
kw = round(panels * pmp / 1000, 2)
spares = 31 - panels
# per-unit 6000XP specs
ac_unit = require(cfg, "inverter.ac_continuous_w") # 6000
ac_a_unit = require(cfg, "inverter.ac_cont_a_per_leg") # 25
surge_unit = require(cfg, "inverter.ac_surge_w") # 12000 (3.5 s)
eff = require(cfg, "inverter.discharge_eff") # 0.93
inv_dis_unit = require(cfg, "inverter.batt_max_discharge_a") # 140
inv_chg_unit = require(cfg, "inverter.batt_max_charge_a") # 125
pv_unit = require(cfg, "inverter.pv_max_power_w") # 8000
i_in_max = require(cfg, "inverter.pv_max_input_a_per_mppt") # 17
isc_in_max = require(cfg, "inverter.pv_max_isc_a_per_mppt") # 25
voc_ceiling = require(cfg, "inverter.pv_design_voc_ceiling_v") # 480
bms_trip = require(cfg, "battery.bms_discharge_ocp_a") # 150
bms_delay = require(cfg, "battery.bms_ocp_delay_s") # 300
bms_sc = require(cfg, "battery.bms_short_circuit_a") # 300
cell_cont = require(cfg, "battery.cell_continuous_a") # 157
# aggregates
ac_total = n_inv * ac_unit; ac_a_total = n_inv * ac_a_unit
inv_dis_total = n_inv * inv_dis_unit; inv_chg_total = n_inv * inv_chg_unit
pv_usable = n_inv * pv_unit; surge_total = n_inv * surge_unit
runs = get(cfg, "phase2.runs_ft") or cfg["runs_ft"]
tgt = cfg["targets"]
pv_vref = round(n_series * vmp, 1) # 7S Vmp = 307 V
result("config", f"{n_inv}x EG4 6000XP // {panels} panels ({n_str}x{n_series}S) // {npack} packs")
result("array DC / spares held", f"{kw} kW / {spares} panels", "", "3 panels reserved as field spares")
result("string cold Voc (validated 7S)", f"{s01['string_cold_voc_V']} V", "",
f"<= {voc_ceiling} V design (PASS, {voc_ceiling - s01['string_cold_voc_V']:.0f} V margin); same module/ceiling as Phase 1")
result("AC / PV-usable / batt limit", f"{ac_total:.0f}W / {pv_usable:.0f}W / {inv_dis_total:.0f}A",
"", f"2x(6000W / 8000W / 140A); array {kw*1000:.0f}W DC vs {pv_usable:.0f}W usable")
results = {"meta": {"path": "A", "inverters": n_inv, "panels": panels, "spares": spares,
"kw": kw, "strings": n_str, "n_series": n_series,
"string_cold_voc_V": s01["string_cold_voc_V"], "voc_ceiling_V": voc_ceiling,
"array_dc_W": panels * pmp, "pv_usable_W": pv_usable}}
# ---------- conductors ----------
print("\n CONDUCTORS (NEC 690.8 / 310.15 / 240.4 / Ch.9 T8):")
# 1) PV string homeruns (4 strings -> 8 CCC in the trench), worst-case 7S vref
ccc_pv = 2 * n_str
r_pv = size_run("PV string (array->6000XP MPPT, per string)", mode="pv",
current=imp, vref=pv_vref, length_ft=runs["combiner_to_inverter"],
target_pct=tgt["vdrop_dc_pct_max"], ambient_c=40, ccc=ccc_pv, isc=isc, set_ocpd=False)
report(r_pv)
sweep = {L: round(vdrop_pct(r_pv["final_gauge_AWG"], L, imp, pv_vref)[0], 3)
for L in cfg.get("dc_run_sweep_ft", [200])}
result(" DC Vdrop sweep", " / ".join(f"{L}ft:{p}%" for L, p in sweep.items()), "",
f"{r_pv['final_gauge_AWG']}Cu, {ccc_pv} CCC; 1 string/MPPT -> NO source fuse (690.9(A))")
r_pv["vdrop_sweep_pct"] = sweep; r_pv["ccc"] = ccc_pv; r_pv["source_fuse_required"] = False
results["pv_string"] = r_pv
print()
# 2) AC: each 6000XP output (25 A) + the combined feed (50 A) at the parallel busbar
r_ac_unit = size_run("AC per-unit (6000XP->AC combiner)", mode="continuous",
current=ac_a_unit, vref=240.0, length_ft=runs["inverter_to_loads"],
target_pct=tgt["vdrop_ac_pct_max"], ambient_c=30, ccc=2)
report(r_ac_unit)
r_ac_comb = size_run("AC combined (busbar->main panel)", mode="continuous",
current=ac_a_total, vref=240.0, length_ft=runs["inverter_to_loads"],
target_pct=tgt["vdrop_ac_pct_max"], ambient_c=30, ccc=2)
report(r_ac_comb)
results["ac_per_unit"] = r_ac_unit; results["ac_combined"] = r_ac_comb
print()
# 3) Battery bank: busbar fed by 2 packs, feeding 2 inverter inputs.
i_bank = ac_total / eff / vnom # 12 kW from the bank
i_pack = i_bank / npack # per pack
i_inv = i_bank / n_inv # per inverter input
r_pack = size_run("Battery per-pack (pack->busbar)", mode="continuous",
current=i_pack, vref=vnom, length_ft=runs["pack_to_busbar"],
target_pct=tgt["vdrop_battery_pct_max"], ambient_c=30, ccc=2)
report(r_pack)
r_inv = size_run("Battery per-inverter (busbar->6000XP)", mode="continuous",
current=i_inv, vref=vnom, length_ft=runs["inverter_to_battery"],
target_pct=tgt["vdrop_battery_pct_max"], ambient_c=30, ccc=2)
report(r_inv)
# NEC 690.8(B)/705/706.31: busbar + bank disconnect >= 125% of continuous bank current.
busbar_min_A = i_bank * 1.25 # 252 A * 1.25 = 315 A
busbar_std_A = std_ocpd_at_least(busbar_min_A) # next standard rating (350 A)
result("bank busbar / disconnect", f">= {busbar_min_A:.0f} A -> {busbar_std_A} A rated", "",
f"125% x {i_bank:.0f}A continuous (690.8(B)); 2 packs in ({i_pack:.0f}A ea) + 2 inverters out "
f"({i_inv:.0f}A ea) — no single cable, but busbar/disconnect carry the full {i_bank:.0f}A bank")
results["battery_per_pack"] = r_pack; results["battery_per_inverter"] = r_inv
results["bank"] = {"i_bank_A": round(i_bank, 1), "i_pack_A": round(i_pack, 1),
"i_inv_A": round(i_inv, 1),
"busbar_min_A": round(busbar_min_A, 1), "busbar_A": busbar_std_A}
print()
# ---------- battery limits / C-rate / surge / autonomy / fusing ----------
print(" BATTERY (limits, C-rate, surge, autonomy, Class-T):")
result("bank vs total inverter limit", f"{i_bank:.1f} / {inv_dis_total}", "A",
f"{'PASS' if i_bank <= inv_dis_total else 'FAIL'} (headroom {inv_dis_total - i_bank:.0f}A, {100*(inv_dis_total-i_bank)/inv_dis_total:.0f}%)")
result("per-inverter vs 6000XP limit", f"{i_inv:.1f} / {inv_dis_unit}", "A",
f"{'PASS' if i_inv <= inv_dis_unit else 'FAIL'} (headroom {inv_dis_unit - i_inv:.0f}A) — same as Phase-1 single pack")
pack_limits = [("JK-BMS OCP (300s)", bms_trip), ("EVE cell 0.5P", cell_cont)]
pb = min(pack_limits, key=lambda x: x[1])
for nm, lim in pack_limits:
result(f"per-pack vs {nm}", f"{i_pack:.1f} / {lim}", "A",
f"{'PASS' if i_pack <= lim else 'FAIL'} ({100*i_pack/lim:.0f}%)")
c_dis = i_pack / cap_ah; c_chg = (inv_chg_total / npack) / cap_ah
result("per-pack C-rate dis / chg", f"{c_dis:.2f}C / {c_chg:.2f}C", "", "<= 0.5C OK")
i_surge_bank = surge_total / eff / vnom; i_surge_pack = i_surge_bank / npack
result("surge (2x12kW, 3.5s)", f"bank {i_surge_bank:.0f}A -> {i_surge_pack:.0f}A/pack", "",
f"> {bms_trip}A OCP but 3.5s << {bms_delay}s delay & < {bms_sc}A SC -> transient, no trip")
bank_kwh = usable_kwh * npack; bank_dod = bank_kwh * dod
avg = load["daily_avg_kwh"]; peak = load["august_peak_kwh_day"]
result("autonomy", f"{bank_dod:.1f} kWh usable -> {bank_dod/avg:.2f} d @avg / {bank_dod/peak:.2f} d @peak",
"", f"{npack} packs @ 80% DoD; generator backfills deep autonomy")
req_pack = i_pack * 1.25
pca = r_pack["final_gauge_AWG"]; pamp = T310_16[pca][1]; pack_fuse = std_ocpd_at_least(req_pack)
n_classT = npack + n_inv
result("Class-T fusing", f"{n_classT}x {pack_fuse}A (2 packs + 2 inverter inputs)", "",
f">= 125%x{i_pack:.0f}A; <= {pca} cable {pamp}A; each 6000XP battery input fused per its manual")
results["limits"] = {"bank_limit_A": inv_dis_total, "bank_headroom_A": round(inv_dis_total - i_bank, 1),
"per_inv_limit_A": inv_dis_unit, "per_inv_headroom_A": round(inv_dis_unit - i_inv, 1),
"per_pack_binding": pb[0], "per_pack_binding_A": pb[1],
"per_pack_headroom_A": round(pb[1] - i_pack, 1),
"c_dis": round(c_dis, 3), "c_chg": round(c_chg, 3),
"surge_bank_A": round(i_surge_bank, 1), "surge_pack_A": round(i_surge_pack, 1),
"surge_duration_s": 3.5,
"bank_usable_kwh_80dod": round(bank_dod, 2),
"days_avg": round(bank_dod / avg, 2), "days_peak": round(bank_dod / peak, 2),
"classT_fuse_A": pack_fuse, "classT_count": n_classT}
# ---------- production (PVWatts v8, BOM bifacial) ----------
out, host = pvwatts(kw, kw / 12.0, cfg)
if out is None:
raise SystemExit("PVWatts unavailable; set NREL_API_KEY and retry.")
yr_load = load["last12_kwh_yr"]; seas_load = load["seasonal_daily_kwh"]
aug = load["august_peak_kwh_day"]
m = out["ac_monthly"]; annual = round(out["ac_annual"])
dec_p, winter_p, summer_p = season(m)
pct_load = round(100 * annual / yr_load)
cov_w = round(100 * 0.85 * winter_p / seas_load["winter"])
cov_s = round(100 * 0.85 * summer_p / seas_load["summer"])
cov_a = round(100 * 0.85 * summer_p / aug)
meets = all(c >= 100 for c in (cov_w, cov_s, cov_a))
result("production (PVWatts v8, BOM bifacial)", f"{annual} kWh/yr ({pct_load}% of load)", "",
f"coverage W/S/Aug {cov_w}/{cov_s}/{cov_a}% (Aug short — battery+gen backfill)")
results["production"] = {"annual_kwh": annual, "pct_load": pct_load,
"cov_winter": cov_w, "cov_summer": cov_s, "cov_aug": cov_a,
"meets_all_seasons": meets, "host": host.split("//")[1]}
# ---------- NEC checklist (Path A final state) ----------
checks = [
("690.7", "PASS", f"Max system voltage {s01['string_cold_voc_V']}V (7S) <= 480V design / 500V abs (validated x4 strings)."),
("690.8(A)", "PASS", f"Per string Isc x1.25 = {round(isc*1.25,1)}A; 1 string/MPPT -> Isc {isc}A <= 25A MPPT rating, Imp {imp}A <= 17A input."),
("690.9(A) Ex", "PASS", "NO PV source fusing: exactly 1 string per MPPT (no parallel strings) on either 6000XP."),
("690.9 / 710.15", "PASS", f"AC OCPD: per-unit {r_ac_unit['ocpd_A']}A 2-pole x2 (8AWG) at the parallel busbar; combined {r_ac_comb['ocpd_A']}A (4AWG) to main panel."),
("690.11/690.41(B)", "PASS", "Each 6000XP has integrated PV AFCI + GFDI/isolation (x2 units)."),
("690.12", "VERIFY-AHJ", "Each 6000XP supplies an RSD initiator if required; same ground-mount/AHJ question as Phase 1."),
("690.13/690.15", "PASS", "Each 6000XP integrated DC disconnect; bank + per-pack + per-inverter disconnects."),
("690.9/480.9/706.31", "PASS", f"{n_classT}x {pack_fuse}A Class-T (2 packs + 2 inverter inputs), 20kAIC, <= 2/0 cable 175A."),
("705", "N/A", "Off-grid. Two inverters PARALLELED (EG4 parallel kit), not utility-interactive; main panel subfed, main breaker locked off."),
("706.7", "PASS", f"2-pack bank on a busbar feeding 2 inverters; bank {i_bank:.0f}A split {i_pack:.0f}A/pack (17% under 150A BMS) and {i_inv:.0f}A/inverter (14A under 140A)."),
("706.x comms", "VERIFY", "ONE master BMS-CAN link (master pack -> master 6000XP); master shares battery data to slave 6000XP over the EG4 inverter parallel/comms kit; packs daisy-chain pack-to-pack via RS485. NOT two CAN-to-both-inverters links (bus-collision risk). Open-loop fallback viable."),
("Redundancy", "NOTE", "N+1: one 6000XP can keep ~half the system (8kW PV / 6kW AC) running if the other fails."),
("690.53/54/56", "ACTION", f"Labels: max system voltage {s01['string_cold_voc_V']}V, 2x inverter output ratings, disconnects, battery hazard."),
]
print("\n NEC CHECKLIST (Path A):")
for sec, st, ev in checks:
print(f" {sec:<20} {st:<11} {ev}")
results["nec_checks"] = [{"section": s, "status": st, "evidence": e} for s, st, e in checks]
dump_result("11_pathA", results)
if __name__ == "__main__":
main()