OptiQ / api /routes /simulate.py
mohammademad2003's picture
first commit
7477316
"""
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")
@router.post("/simulate")
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")
@router.post("/simulate/toggle")
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