"""Deterministic solar sizing engine — location-aware, over the real catalog. All math in Python (the honest core). Uses the user's location peak-sun-hours and the real product catalog; the ≤32B model only narrates over these results. """ from __future__ import annotations import math import catalog from data import APPLIANCES, AUTONOMY_DAYS, SYSTEM_LOSS PF = 0.8 # inverter power factor DIVERSITY = 0.8 # not everything runs continuously at once HEADROOM = 1.2 # inverter sizing margin SURGE_RATIO = 2.0 # inverters tolerate ~2x their rating for a few seconds (startup) # Startup surge multiple for motor/compressor loads (they pull 2-3x running on start). SURGE = {"ac": 3.0, "fridge": 3.0, "freezer": 3.0, "pump": 3.0, "washing": 2.5, "microwave": 1.5} def _surge_factor(name: str) -> float: n = name.lower() if "air condition" in n: return SURGE["ac"] if "fridge" in n or "refriger" in n: return SURGE["fridge"] if "freezer" in n: return SURGE["freezer"] if "pump" in n: return SURGE["pump"] if "washing" in n: return SURGE["washing"] if "microwave" in n: return SURGE["microwave"] return 1.0 def load_profile(selection: dict): items, daily_wh, peak_w, surge_extra = [], 0.0, 0.0, 0.0 for name, qty in selection.items(): qty = int(qty or 0) if qty <= 0 or name not in APPLIANCES: continue watt, hours, cat = APPLIANCES[name] wh = watt * hours * qty items.append({"name": name, "qty": qty, "watt": watt, "hours": hours, "wh": round(wh), "category": cat}) daily_wh += wh peak_w += watt * qty extra = watt * (_surge_factor(name) - 1.0) # one unit's startup surge above its running watts if extra > surge_extra: surge_extra = extra return {"items": items, "daily_wh": round(daily_wh), "peak_w": round(peak_w), "surge_extra": round(surge_extra)} def size(selection: dict, sun_hours: float = 4.6): prof = load_profile(selection) if not prof["items"]: return {"error": "No appliances selected."} daily_wh, peak_w = prof["daily_wh"], prof["peak_w"] daily_kwh = round(daily_wh / 1000, 2) # continuous diversified load, and the worst-case single motor-startup surge running_va = peak_w * DIVERSITY / PF surge_va = (peak_w + prof["surge_extra"]) / PF req_kva = round(max(running_va, surge_va / SURGE_RATIO) / 1000 * HEADROOM, 2) inv = catalog.pick_inverter(req_kva) array_w = round(daily_wh / (sun_hours * SYSTEM_LOSS)) panel = catalog.pick_panel(array_w) n_panels = max(1, math.ceil(array_w / panel["watt"])) panels_cost = n_panels * panel["naira"] durable = catalog.pick_battery(daily_kwh, AUTONOMY_DAYS, "lithium") budget = catalog.pick_battery(daily_kwh, AUTONOMY_DAYS, "tubular") return { "profile": prof, "daily_kwh": daily_kwh, "peak_w": peak_w, "sun_hours": sun_hours, "array_w": array_w, "panel": {"name": f"{panel['brand']} {panel['model']}", "count": n_panels, "each_naira": panel["naira"]}, "inverter": {"name": f"{inv['brand']} {inv['model']}", "kva": inv["kva"], "naira": inv["naira"], "required_kva": req_kva}, "batteries": { "durable": {"name": f"{durable['battery']['brand']} {durable['battery']['model']}", "count": durable["count"], "backup_kwh": durable["backup_kwh"], "total": panels_cost + inv["naira"] + durable["cost"]}, "budget": {"name": f"{budget['battery']['brand']} {budget['battery']['model']}", "count": budget["count"], "backup_kwh": budget["backup_kwh"], "total": panels_cost + inv["naira"] + budget["cost"]}, }, "recommended": "durable", } def summary_text(r, state=None): if r.get("error"): return r["error"] d, b = r["batteries"]["durable"], r["batteries"]["budget"] loc = f" for **{state}** (~{r['sun_hours']} sun-hrs/day)" if state else "" return ( f"Daily energy{loc}: **{r['daily_kwh']} kWh/day** · peak **{r['peak_w']} W**.\n\n" f"**Recommended system**\n" f"- ☀️ **{r['panel']['count']} × {r['panel']['name']}** (~{r['array_w']} W array)\n" f"- 🔌 **{r['inverter']['name']}** ({r['inverter']['kva']} kVA, needs ≥{r['inverter']['required_kva']})\n" f"- 🔋 **{d['count']} × {d['name']}** ({d['backup_kwh']} kWh backup)\n\n" f"💰 **₦{d['total']:,}** durable (lithium) · **₦{b['total']:,}** budget (tubular)\n\n" f"_Real catalog prices ({catalog.SOURCES}) — confirm with a licensed installer._" )