OptiQ / src /quantum /hamiltonian.py
AhmedSamir1598's picture
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