Spaces:
Sleeping
Sleeping
| """ | |
| Simulate endpoint — Manual switch control and power flow simulation. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from typing import Optional | |
| from fastapi import APIRouter, HTTPException, Depends | |
| from pydantic import BaseModel, Field | |
| from src.grid.loader import load_network, clone_network, get_line_info | |
| from src.grid.power_flow import apply_topology, run_power_flow, extract_results, check_radial_connected, check_topology_valid | |
| from src.evaluation.metrics import compute_impact | |
| from api.auth import optional_auth, require_auth, FirebaseUser | |
| from api.database import log_usage, log_audit | |
| router = APIRouter() | |
| class SimulateRequest(BaseModel): | |
| system: str = Field(default="case33bw", description="IEEE test system name") | |
| open_lines: list[int] = Field(description="List of line indices to set as OPEN") | |
| load_multiplier: float = Field(default=1.0, ge=0.5, le=2.0, description="Load scaling factor") | |
| def simulate(req: SimulateRequest, user: FirebaseUser = Depends(optional_auth)): | |
| """Simulate power flow with custom switch configuration. | |
| Allows manual open/close of switches and recomputes losses. | |
| """ | |
| try: | |
| net = load_network(req.system) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| # Scale loads if multiplier != 1.0 | |
| if req.load_multiplier != 1.0: | |
| net.load["p_mw"] *= req.load_multiplier | |
| net.load["q_mvar"] *= req.load_multiplier | |
| # Get baseline first | |
| net_baseline = clone_network(net) | |
| if not run_power_flow(net_baseline): | |
| raise HTTPException(status_code=500, detail="Baseline power flow did not converge") | |
| baseline = extract_results(net_baseline) | |
| # Check connectivity (required) — radiality is a soft warning, not a hard block | |
| if not check_topology_valid(net, req.open_lines, require_radial=False): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Invalid topology: all buses must remain connected" | |
| ) | |
| # Apply topology and run power flow | |
| net_sim = apply_topology(net, req.open_lines) | |
| if not run_power_flow(net_sim): | |
| raise HTTPException(status_code=500, detail="Power flow did not converge for this configuration") | |
| simulated = extract_results(net_sim) | |
| simulated["open_lines"] = req.open_lines | |
| # Compute impact | |
| impact = compute_impact(baseline, simulated) | |
| # Log if authenticated | |
| if user: | |
| try: | |
| # Calculate annualized values | |
| hours_per_year = 8760 | |
| saved_kw = baseline["total_loss_kw"] - simulated["total_loss_kw"] | |
| saved_kwh = saved_kw * hours_per_year | |
| co2_saved = saved_kwh * 0.50 # Egypt factor | |
| money_saved = saved_kwh * 0.08 | |
| log_usage( | |
| firebase_uid=user.uid, | |
| system=req.system, | |
| method="simulate", | |
| baseline_loss_kw=baseline["total_loss_kw"], | |
| optimized_loss_kw=simulated["total_loss_kw"], | |
| energy_saved_kwh=saved_kwh, | |
| co2_saved_kg=co2_saved, | |
| money_saved_usd=money_saved, | |
| computation_time_sec=0.0, | |
| load_multiplier=req.load_multiplier, | |
| switches_changed=json.dumps(req.open_lines), | |
| ) | |
| except Exception: | |
| pass # Don't fail request if logging fails | |
| return { | |
| "system": req.system, | |
| "load_multiplier": req.load_multiplier, | |
| "baseline": baseline, | |
| "simulated": simulated, | |
| "impact": impact, | |
| "open_lines": req.open_lines, | |
| } | |
| class ToggleSwitchRequest(BaseModel): | |
| system: str = Field(default="case33bw") | |
| line_id: int = Field(description="Line index to toggle") | |
| current_open_lines: list[int] = Field(description="Current list of open line indices") | |
| def toggle_switch(req: ToggleSwitchRequest, user: FirebaseUser = Depends(optional_auth)): | |
| """Toggle a single switch and return the new configuration validity.""" | |
| try: | |
| net = load_network(req.system) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| # Compute new open lines | |
| if req.line_id in req.current_open_lines: | |
| new_open_lines = [l for l in req.current_open_lines if l != req.line_id] | |
| action = "closed" | |
| else: | |
| new_open_lines = req.current_open_lines + [req.line_id] | |
| action = "opened" | |
| # Check connectivity (hard requirement) — loops are allowed | |
| is_connected = check_topology_valid(net, new_open_lines, require_radial=False) | |
| is_distribution = "33" in req.system | |
| is_radial = check_topology_valid(net, new_open_lines, require_radial=True) if is_connected else False | |
| result = { | |
| "line_id": req.line_id, | |
| "action": action, | |
| "new_open_lines": new_open_lines, | |
| "is_valid": is_connected, | |
| } | |
| if is_connected: | |
| # Run power flow — pandapower handles meshed grids fine | |
| net_sim = apply_topology(net, new_open_lines) | |
| if run_power_flow(net_sim): | |
| simulated = extract_results(net_sim) | |
| result["power_flow"] = simulated | |
| if is_distribution and not is_radial: | |
| result["warnings"] = ["Configuration has loops — distribution grids are normally operated radially."] | |
| else: | |
| result["is_valid"] = False | |
| result["error"] = "Power flow did not converge" | |
| else: | |
| result["error"] = "Configuration disconnects the network — all buses must remain reachable" | |
| return result | |