OverviewFilesCommitsIssuesDocsReleasesPeers

electrical-review / scripts / 07_load_capacity.py

77 lines · python ref:
"""07 — Supportable household-load envelope (energy + power + autonomy).

NOT a load study — the household's actual load profile is still an open item (HANDOFF).
This BOUNDS what daily kWh and instantaneous kW THIS configuration can support, from the
computed winter-binding production (02) and the battery/inverter limits (04). Off-grid the
binding constraint is winter production; the battery sets short-term autonomy; the Phase-1
generator backfills multi-day deficits.

Run (after 02, 04):  ~/.venvs/prsm-electrical/bin/python scripts/07_load_capacity.py
"""
from common import load_site, require, banner, result, dump_result, load_result

# Fraction of delivered AC production usable as household load. PVWatts already applied a
# 14% system loss to AC, so this band is mainly LiFePO4 round-trip (~0.95) on the
# time-shifted energy plus a practical reserve. A band, not a point estimate.
USE_LO, USE_HI = 0.85, 0.95
DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]


def main():
    cfg = load_site()
    banner("07  SUPPORTABLE HOUSEHOLD LOAD  (energy + power + autonomy)")
    p = load_result("02_production")

    m = p["ac_monthly_kwh"]
    dec = p["dec_kwh_day"]
    jun = p["jun_kwh_day"]
    winter = round((m[0] + m[11]) / (DAYS[0] + DAYS[11]), 1)        # Dec+Jan per day
    shoulder = round(sum(m[i] / DAYS[i] for i in (2, 3, 8, 9)) / 4, 1)  # Mar/Apr/Sep/Oct
    annual = round(p["ac_annual_kwh"] / 365.0, 1)

    inv_w = require(cfg, "inverter.ac_continuous_w")
    surge_w = require(cfg, "inverter.ac_surge_w")
    leg_a = require(cfg, "inverter.ac_cont_a_per_leg")
    usable = require(cfg, "battery.usable_kwh")
    dod = require(cfg, "battery.dod_design")
    usable_dod = round(usable * dod, 1)

    seasons = [("Deep winter (Dec)", dec), ("Winter avg (Dec-Jan)", winter),
               ("Shoulder (Mar/Apr/Sep/Oct)", shoulder), ("Summer (Jun)", jun),
               ("Annual avg", annual)]

    print("\n  ENERGY — supportable daily load, solar-only (production x 0.85-0.95):")
    print(f"  {'season':<28}{'AC produced':>13}{'usable load':>18}")
    rows = []
    for name, prod in seasons:
        lo, hi = USE_LO * prod, USE_HI * prod
        print(f"  {name:<28}{prod:>9.1f} kWh{f'{lo:.0f}-{hi:.0f} kWh/day':>18}")
        rows.append({"season": name, "ac_kwh_day": prod,
                     "supportable_kwh_day": [round(lo), round(hi)]})

    print("\n  POWER — instantaneous envelope (inverter limits, from 02/04):")
    result("  continuous", inv_w, "W", "max sum of simultaneous loads")
    result("  surge (3.5 s)", surge_w, "W", "motor / compressor starts")
    result("  per 120 V leg", leg_a, "A", "~3 kW per leg of split-phase")

    print(f"\n  AUTONOMY — one battery, usable {usable_dod} kWh @ {dod*100:.0f}% DoD:")
    print(f"  {'daily load':>12}{'days w/o sun':>16}")
    auton = {}
    for daily in (5, 8, 10, 13, 20):
        d = round(usable_dod / daily, 2)
        print(f"  {daily:>8} kWh{d:>13} days")
        auton[str(daily)] = d

    dump_result("07_load_capacity", {
        "production_kwh_day": {n: v for n, v in seasons},
        "usage_band": [USE_LO, USE_HI],
        "supportable_daily_load_kwh": rows,
        "inverter_continuous_W": inv_w, "inverter_surge_W": surge_w, "per_leg_A": leg_a,
        "battery_usable_kwh_80dod": usable_dod, "days_autonomy": auton,
        "note": "capacity envelope, not a load study; generator backfills winter deficit",
    })


if __name__ == "__main__":
    main()