"""06 — Schematic renderings (schemdraw -> PNG + SVG into artifacts/).
Three drawings, every rating pulled from the COMPUTED result JSONs (01/03/04) so
the gauge/fuse on the page equals what the calc produced:
sld.(png|svg) system single-line
dc-strings.(png|svg) module-level string grouping across the 2 MPPTs
battery-wiring.(png|svg) 6000XP <-> Docan S3 with Class-T fuse + BMS comms
Run (after 01/03/04): ~/.venvs/prsm-electrical/bin/python scripts/06_schematics.py
"""
import argparse
import pathlib
import schemdraw
from schemdraw import elements as elm
from common import load_site, banner, load_result, require
schemdraw.use("matplotlib")
ART = pathlib.Path(__file__).resolve().parent.parent / "artifacts"
# Owner selected PATH A (2x 6000XP) on 2026-06-03 -> it is the only path.
ART_PA = ART / "phase2" # Path A = the selected build (primary deliverable)
CAP = "PRSM OFF-GRID PV · NEK Vermont · NEC 2023 · ratings trace to scripts 01/03/04"
CAP_PA = "PRSM OFF-GRID PV — SELECTED BUILD · PATH A (2× EG4 6000XP) · NEK Vermont · NEC 2023 · ratings trace to scripts 01/11"
def save(d, name, art=ART):
art.mkdir(parents=True, exist_ok=True)
d.save(str(art / f"{name}.svg"))
d.save(str(art / f"{name}.png"), dpi=200)
print(f" rendered {art.name}/{name}.svg + .png")
def title(d, text, xy, size=13):
d += elm.Label().label(text, fontsize=size).at(xy)
def sld(cfg, s01, s03, s04):
pv, bat, ac = s03["pv_string"], s03["battery"], s03["ac_output"]
ukwh = require(cfg, "battery.usable_kwh")
d = schemdraw.Drawing()
d.config(fontsize=10, unit=2.6)
# --- main spine ---
d += elm.Solar().right().label(
f"PV ARRAY\n14×575W · 2×{s01['n_series']}S\n{s01['array_dc_W']}W DC\n"
f"cold Voc {s01['string_cold_voc_V']}V", loc="bottom")
d += elm.Line().right().length(2.4).label(
f"{pv['final_gauge_AWG']}AWG·{pv['length_ft']}ft\n{pv['vdrop_pct']}%", loc="top")
d += elm.SwitchDpst().right().label("DC\ndisc", loc="bottom")
d += elm.Line().right().length(1.0)
inv = d.add(elm.Ic(pins=[
elm.IcPin(name="PV", side="left"),
elm.IcPin(name="AC", side="right"),
elm.IcPin(name="BATP", side="bottom", slot="1/4"),
elm.IcPin(name="GND", side="bottom", slot="4/4"),
], w=5.2, h=4.4).label(
"EG4 6000XP\n6kW 120/240V\ndual MPPT\nRSD·AFCI·GFDI", loc="center", fontsize=10
).anchor("PV"))
# --- AC branch ---
d += elm.Line().right().length(1.6).at(inv.AC)
d += elm.SwitchDpst().right().label(f"{ac['ocpd_A']}A\n2-pole", loc="top")
d += elm.Line().right().length(0.6)
d += elm.RBox(w=2.8, h=2.2).label(
f"MAIN PANEL\nsubfed\n{ac['final_gauge_AWG']}AWG·{ac['length_ft']}ft", fontsize=9)
# --- battery branch (down from BATP, far-left bottom pin) ---
d += elm.Line().down().length(1.4).at(inv.BATP)
d += elm.Fuse().down().label(f"{s04['classT_fuse_A']}A Class-T", loc="left")
d += elm.Line().down().length(0.5)
d += elm.SwitchDpst().down().label("batt disc", loc="left")
d += elm.Line().down().length(0.5)
d += elm.Battery().down().label(
f"Docan S3\n51.2V {ukwh}kWh\n{bat['final_gauge_AWG']}Cu·{bat['length_ft']}ft·{bat['vdrop_pct']}%",
loc="left")
# --- ground branch (down from GND, far-right bottom pin) ---
d += elm.Line().down().length(1.2).at(inv.GND)
d += elm.Ground().label("GEC\n6AWG\n690.47", loc="right", fontsize=8)
cx, cy = inv.center
title(d, "SINGLE-LINE DIAGRAM — PRSM OFF-GRID PV", (cx, cy + 4.2))
d += elm.Label().label(CAP, fontsize=8).at((cx, cy + 3.3))
save(d, "sld")
def dc_strings(cfg, s01, s03):
pv = s03["pv_string"]
voc_cold = s01["string_cold_voc_V"]
vmp = round(s01["n_series"] * require(cfg, "panel.vmp_v"))
n = s01["n_series"]
d = schemdraw.Drawing()
d.config(fontsize=10, unit=1.15)
for row, (yy, mppt) in enumerate([(0.0, "MPPT1"), (-3.0, "MPPT2")]):
d += elm.Label().label(f"String {row+1}\n(+)", fontsize=9).at((-1.5, yy))
d += elm.Solar().right().at((0, yy))
for _ in range(n - 1):
d += elm.Solar().right()
d += elm.Line().right().length(0.9).label(f"{pv['final_gauge_AWG']}AWG", loc="top", fontsize=8)
d += elm.RBox(w=1.6, h=1.5).label(f"{mppt}\nEG4", fontsize=9)
title(d, f"DC STRING WIRING — 2 strings × {n}S across MPPT1 / MPPT2", (4.5, 1.9))
d += elm.Label().label(
f"Each string: {n}×Waaree 575W series · Vmp≈{vmp}V · cold Voc {voc_cold}V "
f"(≤480V design / 500V abs)\nNO source-circuit fuse — one string per MPPT (690.9(A) Exc.) "
f"· MAX 7 modules/string (8S = 491V, unsafe)", fontsize=8).at((4.5, -5.2))
save(d, "dc-strings")
def battery_wiring(cfg, s03, s04):
bat = s03["battery"]
ukwh = require(cfg, "battery.usable_kwh")
d = schemdraw.Drawing()
d.config(fontsize=10, unit=2.4)
inv = d.add(elm.Ic(pins=[
elm.IcPin(name="BATP", side="right", slot="3/3"),
elm.IcPin(name="BATN", side="right", slot="1/3"),
elm.IcPin(name="COM", side="bottom"),
], w=3.0, h=3.2).label("EG4 6000XP\nBAT 46.4–60V\nmax 140A dis", loc="center", fontsize=9).at((0, 0)))
# positive path: BATP -> Class-T fuse -> disconnect -> battery top
d += elm.Line().right().length(1.3).at(inv.BATP).label("+", loc="top")
d += elm.Fuse().right().label(f"{s04['classT_fuse_A']}A\nClass-T", loc="top")
d += elm.SwitchDpst().right().label("disc", loc="bottom")
top = d.add(elm.Line().right().length(1.7))
batt = d.add(elm.Battery().down().at(top.end).label(
f"Docan S3\n51.2V·{ukwh}kWh\n16S1P · JK 200A BMS", loc="right", fontsize=9))
# negative return
d += elm.Line().left().at(batt.end).tox(inv.BATN[0]).label(
f"− {bat['final_gauge_AWG']}Cu×{bat['length_ft']}ft·{bat['vdrop_pct']}%", loc="bottom", fontsize=9)
d += elm.Line().up().toy(inv.BATN[1])
# BMS comms (dashed)
c = d.add(elm.Line(ls="--").down().length(1.6).at(inv.COM))
d += elm.Line(ls="--").right().length(5.2).at(c.end).label(
"JK BMS → Pylontech CAN → 6000XP 'Pylontech 2' (V19 fw)", loc="bottom", fontsize=8)
cx, cy = inv.center
title(d, "BATTERY WIRING — EG4 6000XP ↔ Docan S3", (cx + 2.5, cy + 2.6))
d += elm.Label().label(
f"Discharge @6kW = {s04['discharge_A_6kW']}A (binding {s04['binding_limit']['name']} "
f"{s04['binding_limit']['A']}A, {s04['binding_limit']['headroom_A']}A headroom) · "
f"{s04['discharge_C']}C · recommend {bat.get('recommended_upsize_AWG','4/0')}Cu upsize",
fontsize=8).at((cx + 2.0, cy - 4.2))
save(d, "battery-wiring")
def sld_pathA(cfg, pa):
"""Path-A single-line (clean single-spine layout; the explicit two-box detail is in the
Path-A dc-strings + battery-wiring diagrams). 28-panel array → 2× 6000XP (parallel) → bank."""
pv = pa["pv_string"]; acu = pa["ac_per_unit"]; acc = pa["ac_combined"]
pk = pa["battery_per_pack"]; iv = pa["battery_per_inverter"]; m = pa["meta"]
npack, n_classT = 2, pa["limits"]["classT_count"]
bank_dod = pa["limits"]["bank_usable_kwh_80dod"]
d = schemdraw.Drawing()
d.config(fontsize=10, unit=2.7)
d += elm.Solar().right().label(
f"PV ARRAY\n28×575W · {m['strings']}×7S\n{m['array_dc_W']}W DC\ncoldVoc {m['string_cold_voc_V']}V (≤480)",
loc="bottom")
d += elm.Line().right().length(2.6).label(
f"{pv['final_gauge_AWG']}AWG×{pv['length_ft']}ft\n{pv['vdrop_pct']}% · 4 homeruns", loc="top")
d += elm.SwitchDpst().right().label("DC\ndisc", loc="bottom")
d += elm.Line().right().length(1.2)
inv = d.add(elm.Ic(pins=[
elm.IcPin(name="PV", side="left"), elm.IcPin(name="ACIN", side="top"),
elm.IcPin(name="AC", side="right"),
elm.IcPin(name="BATP", side="bottom", slot="1/4"), elm.IcPin(name="GND", side="bottom", slot="4/4"),
], w=6.8, h=6.4).anchor("PV"))
icx, icy = inv.center
d += elm.Label().label("2× EG4 6000XP (parallel) · 12kW\n4 MPPT · 16kW PV · RSD·AFCI·GFDI ×2",
fontsize=9).at((icx, icy + 1.55))
# generator AC-IN
d += elm.Line().up().length(1.4).at(inv.ACIN)
d += elm.SwitchDpst().up().label("gen AC-IN", loc="right", fontsize=9)
d += elm.Line().up().length(0.6)
d += elm.RBox(w=2.8, h=1.3).label("GENERATOR\n200A bypass · 90A gen", fontsize=8)
# AC: 2× 35A unit outputs combine to 70A → main panel
d += elm.Line().right().length(1.8).at(inv.AC)
d += elm.SwitchDpst().right().label(f"{acc['ocpd_A']}A\n2-pole", loc="top")
d += elm.Line().right().length(1.3)
d += elm.RBox(w=3.0, h=2.3).label(
f"MAIN PANEL\nsubfed (main off)\n{acc['final_gauge_AWG']}AWG (2×{acu['ocpd_A']}A→{acc['ocpd_A']}A)", fontsize=9)
# battery branch → busbar → bank
d += elm.Line().down().length(0.9).at(inv.BATP)
d += elm.SwitchDpst().down().label(f"bank disc {pa['bank']['busbar_A']}A", loc="right", fontsize=8)
d += elm.Line().down().length(1.5).label(
f"{iv['final_gauge_AWG']}Cu/inverter · {pa['bank']['i_inv_A']:.0f}A ea\n(2 inverters tap the bus)",
loc="left", fontsize=8)
bus = d.add(elm.RBox(w=3.6, h=1.0).label(f"DC BUSBAR ({pa['bank']['busbar_A']}A)", fontsize=9))
d += elm.Line().down().length(0.8)
d += elm.Fuse().down().label(f"{n_classT}× {pa['limits']['classT_fuse_A']}A Class-T\n(2 packs + 2 inverters)", loc="right", fontsize=8)
d += elm.Line().down().length(0.8)
d += elm.Battery().down().label(
f"{npack}× Docan S3 · 51.2V\n{bank_dod}kWh usable (80% DoD)\n{pk['final_gauge_AWG']}Cu/pack · "
f"{pk['vdrop_pct']}% · 1 master BMS→CAN + RS485 daisy", loc="left", fontsize=8)
# TRANSITION option (DC-coupled, islanded): shore/grid -> Chargeverter -> fused busbar tap.
try:
cv = load_result("12_chargeverter")
except SystemExit:
cv = None
if cv:
bx, by = bus.center
d += elm.Line().left().length(0.5).at((bx - 1.8, by))
d += elm.Fuse().left().label(f"{cv['dc_output']['ocpd_a']}A\nfuse", loc="top", fontsize=7)
d += elm.RBox(w=3.0, h=1.2).label("EG4 Chargeverter GC\n48V · 0–100A DC\n(≤50A w/ full PV)", fontsize=7)
d += elm.Line().left().length(0.5)
d += elm.SwitchDpst().left().label(f"{cv['ac_input']['ocpd_a']}A\n2-pole", loc="top", fontsize=7)
d += elm.Line().left().length(0.4)
d += elm.RBox(w=2.4, h=1.1).label("SHORE / GRID\n(transition only —\nDC-coupled, islanded)", fontsize=7)
d += elm.Line().down().length(1.2).at(inv.GND)
d += elm.Ground().label("GEC 6AWG\n690.47", loc="right", fontsize=8)
cx, cy = inv.center
title(d, "PATH-A SINGLE-LINE — 2× EG4 6000XP (parallel)", (cx, cy + 7.4))
d += elm.Label().label(CAP_PA, fontsize=8).at((cx, cy + 6.7))
save(d, "sld", ART_PA)
def dc_strings_pathA(cfg, pa):
"""Path-A DC strings: 4× 7S, two strings per 6000XP (2 MPPTs each)."""
pv = pa["pv_string"]; m = pa["meta"]
vmp = round(7 * require(cfg, "panel.vmp_v"))
cold = m["string_cold_voc_V"]
d = schemdraw.Drawing()
d.config(fontsize=10, unit=1.3)
y = 0.0
for i in range(4):
unit = 1 if i < 2 else 2
mppt = (i % 2) + 1
d += elm.Label().label(f"String {i+1}\n7S (+)", fontsize=9).at((-1.7, y))
d += elm.Solar().right().at((0, y)).label("7× Waaree 575W", loc="top", fontsize=8)
d += elm.Line().right().length(1.0).label(f"{pv['final_gauge_AWG']}AWG", loc="top", fontsize=8)
d += elm.Line().right().length(0.3).label(f"Vmp≈{vmp}V\ncoldVoc {cold}V", loc="bottom", fontsize=8)
d += elm.RBox(w=2.2, h=1.0).label(f"6000XP #{unit}\nMPPT{mppt}", fontsize=8)
y -= 1.8
title(d, "PATH-A DC STRINGS — 4× 7S, two strings per 6000XP", (2.8, 1.6))
d += elm.Label().label(
f"#1: MPPT1+MPPT2 (7S+7S) · #2: MPPT1+MPPT2 (7S+7S) · 28 modules · {m['array_dc_W']}W DC\n"
f"7S cold Voc {cold}V ≤ 480V design / 500V abs · 1 string/MPPT → NO source fuse · 3 panels held as spares",
fontsize=8).at((2.8, y - 0.4))
save(d, "dc-strings", ART_PA)
def battery_wiring_pathA(cfg, pa):
"""Path-A battery: busbar fed by 2 packs, feeding 2× 6000XP inputs; per-pack + per-inverter Class-T."""
pk = pa["battery_per_pack"]; iv = pa["battery_per_inverter"]; L = pa["limits"]; bk = pa["bank"]
npack = 2; fuse = L["classT_fuse_A"]; ukwh = require(cfg, "battery.usable_kwh")
d = schemdraw.Drawing()
d.config(fontsize=9, unit=1.1)
# central vertical busbar
d += elm.Line().up().length(2.6).at((0, 0))
d += elm.Line().down().length(2.6).at((0, 0))
d += elm.Label().label(f"DC BUSBAR\n{bk['busbar_A']}A", fontsize=9).at((0, 0.0))
# left: 2 packs in
for k in range(npack):
yy = 1.6 - k * 3.2
d += elm.Line().left().length(1.0).at((0, yy))
d += elm.Fuse().left().label(f"{fuse}A\nClass-T", loc="top", fontsize=7)
d += elm.SwitchDpst().left().label("disc", loc="bottom", fontsize=7)
d += elm.Battery().left().label(f"Docan S3 #{k+1}\n51.2V·{ukwh}kWh\n{pk['final_gauge_AWG']}Cu", loc="top", fontsize=7)
# right: 2 inverters out
for k in range(npack):
yy = 1.6 - k * 3.2
d += elm.Line().right().length(1.0).at((0, yy))
d += elm.Fuse().right().label(f"{fuse}A\nClass-T", loc="top", fontsize=7)
d += elm.SwitchDpst().right().label("disc", loc="bottom", fontsize=7)
d += elm.RBox(w=2.0, h=1.2).label(f"6000XP #{k+1}\nBAT max140A\n{iv['final_gauge_AWG']}Cu", fontsize=7)
title(d, "PATH-A BATTERY WIRING — busbar: 2 packs → 2× 6000XP", (0, 5.0))
d += elm.Label().label(
f"Bank {bk['i_bank_A']:.0f}A @12kW → {bk['i_pack_A']:.0f}A/pack (17% under 150A BMS) + "
f"{bk['i_inv_A']:.0f}A/inverter (14A under 140A) · {L['c_dis']}C · {npack+npack}× {fuse}A Class-T · "
f"busbar/disc {bk['busbar_A']}A (125%×{bk['i_bank_A']:.0f}A) · 1 master BMS→CAN + RS485 daisy + EG4 parallel kit",
fontsize=7).at((0, -5.2))
save(d, "battery-wiring", ART_PA)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--pathA", action="store_true",
help="render the SELECTED Path-A (2× 6000XP) diagrams to artifacts/phase2/")
args = ap.parse_args()
if args.pathA:
cfg = load_site()
banner("06 SCHEMATICS [SELECTED PATH A — 2× 6000XP -> artifacts/phase2/]")
pa = load_result("11_pathA")
ok = []
for name, fn in [("sld", lambda: sld_pathA(cfg, pa)),
("dc-strings", lambda: dc_strings_pathA(cfg, pa)),
("battery-wiring", lambda: battery_wiring_pathA(cfg, pa))]:
try:
fn(); ok.append(name)
except Exception as e: # noqa
import traceback
print(f" [FAIL] {name}: {type(e).__name__}: {e}")
traceback.print_exc()
print(f"\n rendered OK: {ok}")
return
cfg = load_site()
banner("06 SCHEMATICS (schemdraw -> PNG + SVG)")
s01 = load_result("01_string")
s03 = load_result("03_conductors")
s04 = load_result("04_battery")
ok = []
for name, fn in [("sld", lambda: sld(cfg, s01, s03, s04)),
("dc-strings", lambda: dc_strings(cfg, s01, s03)),
("battery-wiring", lambda: battery_wiring(cfg, s03, s04))]:
try:
fn()
ok.append(name)
except Exception as e: # noqa
import traceback
print(f" [FAIL] {name}: {type(e).__name__}: {e}")
traceback.print_exc()
print(f"\n rendered OK: {ok}")
if __name__ == "__main__":
main()