"""03 — Conductor ampacity + voltage drop (NEC 690.8 / 310.15 / 240.4 / Ch.9 T8).
For each run: corrected continuous current -> required ampacity after 310.15 derates
-> smallest compliant copper gauge (honoring 240.4(D)) -> upsize if voltage drop
exceeds target -> OCPD (240.6) -> EGC (250.122). All NEC tables in nec_tables.py.
Run: ~/.venvs/prsm-electrical/bin/python scripts/03_conductors.py
"""
from common import (load_site, require, get, banner, result, dump_result, load_result)
from nec_tables import (T310_16, T8_DCRES, fill_factor, temp_factor, AWG_ORDER,
std_ocpd_at_least, egc_awg, max_ocpd_240_4d)
def vdrop_pct(awg, length_ft, amps, vref, n_conductors=2):
"""Voltage drop %, NEC Ch.9 Table 8 DC resistance (75C). n_conductors=2 for a
2-wire DC or 1-phase circuit (out and back)."""
r = T8_DCRES[awg] / 1000.0 # ohm/ft
drop = n_conductors * length_ft * amps * r
return 100.0 * drop / vref, drop
def pick_gauge(required_term_a, required_derated_a, ambient_c, ccc, term_col="75",
start="14"):
"""Smallest copper AWG whose 75C(term) ampacity >= required_term_a AND whose
90C ampacity x temp x fill >= required_derated_a."""
tf = temp_factor(ambient_c, "90")
ff = fill_factor(ccc)
for awg in AWG_ORDER[AWG_ORDER.index(start):]:
amp_term = T310_16[awg][{"60": 0, "75": 1, "90": 2}[term_col]]
amp_derated = T310_16[awg][2] * tf * ff
if amp_term >= required_term_a - 1e-9 and amp_derated >= required_derated_a - 1e-9:
return awg, amp_term, amp_derated, tf, ff
raise SystemExit("no gauge in table satisfies ampacity; extend table")
def size_run(name, *, mode, current, vref, length_ft, target_pct, ambient_c, ccc,
isc=None, term_col="75", set_ocpd=True):
"""mode 'pv' -> 690.8(A)(1)+(B); mode 'continuous' -> 125% continuous (210/215/480)."""
if mode == "pv":
max_current = isc * 1.25 # 690.8(A)(1)
req_term = max_current * 1.25 # 690.8(B)(1): 125% at terminal rating
req_derated = max_current # 690.8(B)(2): after conditions of use
load_for_vd = current # Imp for voltage drop
ocpd_basis = max_current
else: # continuous
req_term = current * 1.25 # 215.2/210.19: 125% continuous
req_derated = current * 1.25
load_for_vd = current
ocpd_basis = current * 1.25
awg, amp_term, amp_derated, tf, ff = pick_gauge(req_term, req_derated, ambient_c, ccc, term_col)
# upsize for voltage drop
vd_awg = awg
vd_pct, vd_v = vdrop_pct(vd_awg, length_ft, load_for_vd, vref)
while vd_pct > target_pct and AWG_ORDER.index(vd_awg) < len(AWG_ORDER) - 1:
vd_awg = AWG_ORDER[AWG_ORDER.index(vd_awg) + 1]
vd_pct, vd_v = vdrop_pct(vd_awg, length_ft, load_for_vd, vref)
final = vd_awg
ocpd = std_ocpd_at_least(ocpd_basis) if set_ocpd else None
# 240.4(D): OCPD must not exceed the conductor's small-conductor ceiling.
# Upsize the conductor until it can be protected by the required OCPD.
if ocpd is not None:
while (max_ocpd_240_4d(final, T310_16[final][1]) < ocpd
and AWG_ORDER.index(final) < len(AWG_ORDER) - 1):
final = AWG_ORDER[AWG_ORDER.index(final) + 1]
ceil = max_ocpd_240_4d(final, T310_16[final][1])
ocpd_note = "OK" if ocpd <= ceil else f"OCPD {ocpd}>ceil {ceil}"
else:
ocpd_note = "n/a"
# recompute voltage drop for the final (possibly upsized) gauge
vd_pct, vd_v = vdrop_pct(final, length_ft, load_for_vd, vref)
driver = ("ampacity" if final == awg
else "voltage-drop" if final == vd_awg else "240.4(D) OCPD")
egc = egc_awg(ocpd) if ocpd else None
res = {
"run": name, "mode": mode, "length_ft": length_ft, "vref_V": vref,
"current_A": round(current, 2),
"req_ampacity_term_A": round(req_term, 1),
"req_ampacity_derated_A": round(req_derated, 1),
"ambient_C": ambient_c, "ccc": ccc, "temp_factor90": tf, "fill_factor": ff,
"ampacity_gauge": awg, "ampacity_75C_A": amp_term,
"ampacity_derated90_A": round(amp_derated, 1),
"final_gauge_AWG": final, "gauge_driver": driver,
"final_ampacity_75C_A": T310_16[final][1],
"final_ampacity_derated90_A": round(T310_16[final][2] * tf * ff, 1),
"vdrop_pct": round(vd_pct, 3), "vdrop_V": round(vd_v, 3),
"target_pct": target_pct,
"ocpd_A": ocpd, "ocpd_note": ocpd_note, "egc_AWG": egc,
}
return res
def report(r):
result(r["run"], f"{r['current_A']} A cont", "",
f"{r['mode']} basis")
result(" req ampacity (term/derated)",
f"{r['req_ampacity_term_A']} / {r['req_ampacity_derated_A']}", "A",
f"310.15(B) Tf={r['temp_factor90']} x 310.15(C) Ff={r['fill_factor']} @ {r['ambient_C']}C, {r['ccc']}CCC")
result(" ampacity-min gauge", f"{r['ampacity_gauge']} ({r['ampacity_75C_A']}A@75C)")
result(" FINAL gauge", f"{r['final_gauge_AWG']} Cu", "", f"driver: {r['gauge_driver']}")
result(" voltage drop", f"{r['vdrop_pct']}% ({r['vdrop_V']} V)", "",
f"target <= {r['target_pct']}% ({'PASS' if r['vdrop_pct']<=r['target_pct'] else 'FAIL'}) [Ch.9 T8]")
if r["ocpd_A"]:
result(" OCPD / EGC", f"{r['ocpd_A']} A / {r['egc_AWG']} AWG Cu",
"", f"240.6 / 250.122 ({r['ocpd_note']})")
def main():
cfg = load_site()
banner("03 CONDUCTORS (NEC 690.8 ampacity + 310.15 derates + voltage drop)")
s01 = load_result("01_string")
isc = require(cfg, "panel.isc_a")
imp = require(cfg, "panel.imp_a")
n_series = s01["n_series"]
vmp_string = round(n_series * require(cfg, "panel.vmp_v"), 1)
ac_w = require(cfg, "inverter.ac_continuous_w")
ac_v = 240.0
vnom_batt = require(cfg, "battery.nominal_v")
eff = require(cfg, "inverter.discharge_eff")
runs = cfg["runs_ft"]
tgt = cfg["targets"]
i_batt = ac_w / eff / vnom_batt
i_ac = ac_w / ac_v
result("PV Imp / Isc", f"{imp} / {isc}", "A")
result("string Vmp (DC)", vmp_string, "V", f"{n_series}S")
result("battery current @ 6kW", round(i_batt, 1), "A", "ac_w/eff/Vnom")
result("AC output current", round(i_ac, 1), "A", "6kW/240V split-phase")
print()
results = {}
# 1) PV string source/output, array -> inverter (one per MPPT). 2 strings in one
# trench conduit -> 4 CCC. Conservative ambient 40C for above-grade segments.
dc_len = runs["combiner_to_inverter"]
r_pv = size_run("PV string (array->inverter, per MPPT)", mode="pv",
current=imp, vref=vmp_string, length_ft=dc_len,
target_pct=tgt["vdrop_dc_pct_max"], ambient_c=40, ccc=4,
isc=isc, set_ocpd=False)
report(r_pv)
# DC voltage-drop sweep across owner's stated 150-250 ft range, chosen gauge
sweep = {}
for L in cfg.get("dc_run_sweep_ft", [dc_len]):
p, v = vdrop_pct(r_pv["final_gauge_AWG"], L, imp, vmp_string)
sweep[L] = round(p, 3)
result(" DC Vdrop sweep 150/200/250 ft",
" / ".join(f"{L}ft:{p}%" for L, p in sweep.items()), "",
f"gauge {r_pv['final_gauge_AWG']} holds <= {tgt['vdrop_dc_pct_max']}%")
r_pv["vdrop_sweep_pct"] = sweep
results["pv_string"] = r_pv
print()
# 2) Battery -> inverter (high current, short). Indoor 30C, 2 CCC.
r_batt = size_run("Battery <-> inverter", mode="continuous",
current=i_batt, vref=vnom_batt, length_ft=runs["inverter_to_battery"],
target_pct=tgt["vdrop_battery_pct_max"], ambient_c=30, ccc=2)
report(r_batt)
# recommended upsize for low loss / margin
up = AWG_ORDER[AWG_ORDER.index(r_batt["final_gauge_AWG"]) + 2]
up_pct, _ = vdrop_pct(up, runs["inverter_to_battery"], i_batt, vnom_batt)
result(" recommended upsize", f"{up} Cu", "", f"Vdrop {round(up_pct,3)}% (margin + low loss)")
r_batt["recommended_upsize_AWG"] = up
results["battery"] = r_batt
print()
# 3) AC output -> subfed main panel. Indoor 30C, 2 CCC (L1/L2).
r_ac = size_run("AC output (inverter->main panel)", mode="continuous",
current=i_ac, vref=ac_v, length_ft=runs["inverter_to_loads"],
target_pct=tgt["vdrop_ac_pct_max"], ambient_c=30, ccc=2)
report(r_ac)
results["ac_output"] = r_ac
print()
# 4) Battery Class-T fuse coordination (size in 04; here confirm cable >= fuse)
batt_ocpd = r_batt["ocpd_A"]
cable_amp = T310_16[r_batt["final_gauge_AWG"]][1]
result("Battery Class-T fuse (prelim)", f"{batt_ocpd} A",
"", f"<= cable {r_batt['final_gauge_AWG']} ampacity {cable_amp}A "
f"({'OK' if batt_ocpd<=cable_amp else 'UPSIZE CABLE'}); final in 04_battery")
dump_result("03_conductors", results)
if __name__ == "__main__":
main()