Spaces:
Sleeping
Sleeping
first baseline for project OptiQ. Contains research resources, first baseline using GNNs + QC, and benchmarks against current industry standards, while addressing the challenges that prevents better practices to be used in industry.
55e3496 | """ | |
| Hamiltonian Construction — Encode network reconfiguration as a QUBO / Ising problem. | |
| The decision variable x_i ∈ {0, 1} represents whether line i is OUT of service (open). | |
| x_i = 1 → line i is OPEN | |
| x_i = 0 → line i is CLOSED (in service) | |
| Objective: Minimise total active power loss subject to radiality. | |
| We use the loss approximation from DC power flow (linear) to build a quadratic | |
| objective, then add penalty terms for the constraint that exactly K lines must | |
| be open (where K = number of tie lines in the default config). | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| import pandapower as pp | |
| from src.grid.loader import get_line_info | |
| def _compute_line_loss_coefficients(net: pp.pandapowerNet) -> np.ndarray: | |
| """Compute an approximate loss contribution coefficient for each line. | |
| Uses the heuristic: loss_i ∝ r_i * |P_i|^2 / V^2, where P_i is the | |
| baseline power flow on line i. We run a baseline power flow to get P_i. | |
| Returns | |
| ------- | |
| np.ndarray of shape (n_lines,) | |
| Approximate loss coefficient per line (higher = more loss when closed). | |
| """ | |
| import copy | |
| net_copy = copy.deepcopy(net) | |
| # Run baseline power flow | |
| pp.runpp(net_copy) | |
| n_lines = len(net_copy.line) | |
| coeffs = np.zeros(n_lines) | |
| for idx in net_copy.line.index: | |
| if net_copy.line.at[idx, "in_service"]: | |
| r = net_copy.line.at[idx, "r_ohm_per_km"] * net_copy.line.at[idx, "length_km"] | |
| p_flow = abs(net_copy.res_line.at[idx, "p_from_mw"]) | |
| # Loss ∝ r * I^2 ≈ r * P^2 / V^2 (V ≈ 1 p.u.) | |
| coeffs[idx] = r * p_flow ** 2 | |
| else: | |
| # Tie lines have no baseline flow; assign a moderate loss estimate | |
| # based on resistance (encourages closing low-R tie lines) | |
| r = net_copy.line.at[idx, "r_ohm_per_km"] * net_copy.line.at[idx, "length_km"] | |
| coeffs[idx] = r * 0.1 # small estimate | |
| return coeffs | |
| def build_qubo_matrix( | |
| net: pp.pandapowerNet, | |
| radiality_penalty: float = 100.0, | |
| ) -> tuple[np.ndarray, int]: | |
| """Build the QUBO matrix Q for network reconfiguration. | |
| The QUBO objective is: | |
| min x^T Q x | |
| where: | |
| - Diagonal Q[i,i] encodes the loss benefit of opening line i | |
| (negative = benefit of opening). Since x_i=1 means OPEN, lines with | |
| high loss contribution get negative diagonal (incentive to open them | |
| ... but wait, opening a feeder line disconnects load, so we actually | |
| want to keep high-flow feeder lines closed and open lines that form | |
| redundant paths). | |
| We reformulate: we want exactly K lines open. The loss when line i is | |
| closed is coeffs[i]. The total loss = Σ coeffs[i] * (1 - x_i). | |
| Minimising loss = maximising Σ coeffs[i] * x_i (open the lossy lines). | |
| Equivalently, minimise -Σ coeffs[i] * x_i. | |
| Constraint: Σ x_i = K (exactly K lines open for radiality). | |
| Penalty: P * (Σ x_i - K)^2 | |
| Parameters | |
| ---------- | |
| net : pp.pandapowerNet | |
| radiality_penalty : float | |
| Weight for the constraint penalty term. | |
| Returns | |
| ------- | |
| (Q, K) where Q is the QUBO matrix and K is the required number of open lines. | |
| """ | |
| line_info = get_line_info(net) | |
| n_lines = len(line_info["all"]) | |
| K = line_info["n_required_open"] | |
| coeffs = _compute_line_loss_coefficients(net) | |
| Q = np.zeros((n_lines, n_lines)) | |
| # Objective: minimise -Σ coeffs[i] * x_i (open the lossiest lines) | |
| for i in range(n_lines): | |
| Q[i, i] -= coeffs[i] | |
| # Constraint: P * (Σ x_i - K)^2 = P * (Σ x_i^2 + 2*Σ_{i<j} x_i*x_j - 2K*Σ x_i + K^2) | |
| # Since x_i ∈ {0,1}, x_i^2 = x_i: | |
| # P * (Σ x_i - K)^2 = P * [ Σ (1 - 2K) x_i + 2 Σ_{i<j} x_i*x_j + K^2 ] | |
| # The constant K^2 doesn't affect optimization. | |
| for i in range(n_lines): | |
| Q[i, i] += radiality_penalty * (1 - 2 * K) | |
| for j in range(i + 1, n_lines): | |
| Q[i, j] += radiality_penalty | |
| Q[j, i] += radiality_penalty # symmetric | |
| return Q, K | |