mambo_power.pf¶
Power-flow solvers. See the manual page for the formulation, the slack convention and the AC solver's contract.
mambo_power.pf
¶
Power-flow solvers (epic Design §2 pf/): AC Newton-Raphson (W1) and DC (W2).
Public entry points take and return pydantic models (a Network in,
a typed result out) and stamp provenance; the array-level solvers
(mambo_power.pf.ac_newton.newton, mambo_power.pf.dc.solve) work on
NetworkArrays only. Both entry points derive the bus roles
through mambo_power.numerics.effective_roles (W3) — a PV bus without an in-service
generator solves as PQ, a slack without one raises
NoSlackGeneratorError, and a
SetpointConflictWarning propagates to the caller.
AcOptions
¶
Bases: BaseModel
Options of the AC Newton-Raphson solve (spec design item 1).
tol is compared against the infinity norm of the per-unit power mismatch (MATPOWER
pf.tol semantics; pandapower's tolerance_mva is the same pu quantity despite its
name). init="auto" warm-starts from the buses' stored vm_pu/va_deg when every
in-service bus carries both, else flat; PV and slack magnitudes are always the setpoint.
AcSolution
dataclass
¶
AcSolution(
v: ComplexArray,
converged: bool,
iterations: int,
max_mismatch_pu: float,
q_limit_rounds: int,
q_limited: IntArray,
bus_type: IntArray,
s_bus_pu: ComplexArray,
gen_p_pu: FloatArray,
gen_q_pu: FloatArray,
message: str | None = None,
)
Positional AC solution in per unit, in NetworkArrays order.
converged
instance-attribute
¶
Final Newton solve met tol and no Q-limit violation remained.
max_mismatch_pu
instance-attribute
¶
Infinity norm of the final mismatch vector, pu.
q_limit_rounds
instance-attribute
¶
Number of re-solves triggered by pinning (0 when nothing was pinned).
q_limited
instance-attribute
¶
Per bus: 0 free, +1 pinned at ΣQmax, -1 pinned at ΣQmin.
bus_type
instance-attribute
¶
Effective bus types after pinning (1 = pq, 2 = pv, 3 = slack).
s_bus_pu
instance-attribute
¶
Realised net complex injection per bus V·conj(Y V).
gen_p_pu
instance-attribute
¶
Per-generator active output; the first slack-bus generator absorbs the balance.
gen_q_pu
instance-attribute
¶
Per-generator reactive output, split by the MATPOWER pfsoln rule.
message
class-attribute
instance-attribute
¶
Diagnostic when converged is False; None otherwise.
DcSolution
dataclass
¶
DcSolution(
theta_rad: FloatArray,
p_from_pu: FloatArray,
p_inj_pu: FloatArray,
gen_p_pu: FloatArray,
)
Positional DC solution in per unit, in NetworkArrays order.
theta_rad
instance-attribute
¶
Bus angles, radians; theta_rad[arr.slack] == 0.
p_from_pu
instance-attribute
¶
From-side branch flow Bf·θ + pf_shift; the to-side flow is its negative.
p_inj_pu
instance-attribute
¶
Realised net injection per bus B'·θ + p_shift (slack closes the balance).
gen_p_pu
instance-attribute
¶
Per-generator output; the first in-service slack-bus generator absorbs the balance.
initial_voltage
¶
initial_voltage(
net: Network,
arr: NetworkArrays,
roles: EffectiveRoles,
options: AcOptions,
) -> ComplexArray
Starting voltages for solve_ac under options.init.
"flat": mambo_power.pf.ac_newton.flat_start. "auto": when every in-service
bus carries both vm_pu and va_deg the stored state is the start (angles in radians,
the slack keeping its stored angle), with PV and slack magnitudes replaced by the effective
setpoint; otherwise flat.
Source code in src/mambo_power/pf/__init__.py
solve_ac
¶
solve_ac(
net: Network, *, options: AcOptions | None = None
) -> AcPowerFlowResult
AC power flow of net by Newton-Raphson (mambo_power.pf.ac_newton).
Builds the in-service NetworkArrays and the effective roles, solves with Q-limit
enforcement per options, computes branch flows S_from = V_f · conj(Yf V) and
S_to = V_t · conj(Yt V), and returns an AcPowerFlowResult
in MW/MVAr keyed by ids with provenance (kind = "pf.ac", solver =
scipy.sparse.linalg.splu, the options as run). A solve that does not converge is
reported through converged = False — never raised. The network is not modified.
Source code in src/mambo_power/pf/__init__.py
solve_dc
¶
solve_dc(net: Network) -> DcPowerFlowResult
DC power flow of net: lossless B'θ = P with phase shifts, flows via Bf.
Builds the in-service NetworkArrays, runs mambo_power.pf.dc.solve, and
returns a DcPowerFlowResult in MW keyed by ids, with
provenance (version = mambo_power.__version__, solver = scipy.sparse.linalg.splu,
UTC start time, wall-clock duration). role_effective reports the effective roles (W3):
the solve itself needs no setpoints, but a slack without an in-service generator is still
an error and a gen-less PV bus is reported as PQ. The network is not modified.
Source code in src/mambo_power/pf/__init__.py
DC solver over arrays¶
mambo_power.pf.dc
¶
DC power flow over NetworkArrays (MATPOWER rundcpf).
Formulation. With the DC susceptance matrix B' (mambo_power.numerics.bbus), the
from-side flow matrix Bf (mambo_power.numerics.bf), the phase-shifter injections
p_shift = Cftᵀ · pf_shift and pf_shift = -b · shift (mambo_power.numerics.bbus),
and the declared net injection per bus in per unit::
P_bus = P_gen − P_load − G_shunt (G_shunt: conductance consumption at 1.0 pu)
the angles solve the linear system with the slack row and column removed and θ_slack = 0::
B'[keep, keep] · θ[keep] = (P_bus − p_shift)[keep] θ[slack] = 0
and the flows and realised injections follow::
p_from = Bf · θ + pf_shift p_to = −p_from p_inj = B' · θ + p_shift
p_inj equals P_bus on every non-slack bus; at the slack it is whatever closes the
balance (lossless, so Σ p_inj = 0). Those are exactly MATPOWER's rundcpf steps —
Pbus = real(makeSbus) − Pbusinj − GS/baseMVA, Va = dcpf(B, Pbus, Va0, ref, pv, pq),
PF = (Bf·Va + Pfinj)·baseMVA, PT = −PF — which pandapower's rundcpp copies
verbatim (record/m2-research.md §2).
Slack generation. The slack-bus balance goes entirely to the first in-service generator at
the slack bus, every other generator keeping its dispatch (MATPOWER rundcpf:
gen(on(refgen(1)), PG) += (B(ref,:)·Va − Pbus(ref))·baseMVA; pandapower reports the same
number on res_ext_grid). Bus-level generation is therefore engine-independent; the
per-generator split is a documented convention. If the slack bus carries no in-service
generator the balance is still visible on the bus injection; naming that situation is W3's
effective_roles.
The reduced system is factorised with scipy.sparse.linalg.splu (the same backend as the
PTDF builder and the AC Newton solve). Bus roles are the declared roles from the arrays —
DC needs no generator setpoints, so effective-role derivation (W3) does not enter here.
SOLVER
module-attribute
¶
Linear-algebra backend name stamped into the result provenance.
DcSolution
dataclass
¶
DcSolution(
theta_rad: FloatArray,
p_from_pu: FloatArray,
p_inj_pu: FloatArray,
gen_p_pu: FloatArray,
)
Positional DC solution in per unit, in NetworkArrays order.
theta_rad
instance-attribute
¶
Bus angles, radians; theta_rad[arr.slack] == 0.
p_from_pu
instance-attribute
¶
From-side branch flow Bf·θ + pf_shift; the to-side flow is its negative.
p_inj_pu
instance-attribute
¶
Realised net injection per bus B'·θ + p_shift (slack closes the balance).
gen_p_pu
instance-attribute
¶
Per-generator output; the first in-service slack-bus generator absorbs the balance.
declared_injection
¶
declared_injection(arr: NetworkArrays) -> FloatArray
P_gen − P_load − G_shunt per bus in pu — the right-hand side before phase shifts.
solve
¶
solve(arr: NetworkArrays) -> DcSolution
Solve B'θ = P − p_shift with the slack at 0 and return angles, flows and injections.
Raises UnsolvableNetworkError when a branch has x == 0
(susceptance undefined; user data DC cannot solve, distinct from a malformed-input
ValueError) or ValueError when the reduced B' is singular / yields non-finite
angles (an islanded bus set).
Source code in src/mambo_power/pf/dc.py
AC solver over arrays¶
mambo_power.pf.ac_newton
¶
AC power flow by polar Newton-Raphson over NetworkArrays (W1).
Formulation (MATPOWER newtonpf). With the bus admittance matrix Y
(mambo_power.numerics.ybus), the complex voltages V = Vm·e^{jVa} and the specified
net injections S_spec = (P_gen − P_load) + j(Q_gen − Q_load) in per unit (shunts live in
Y), the mismatch is::
ΔS = V · conj(Y V) − S_spec
F = [ real(ΔS)[pv ∪ pq] ; imag(ΔS)[pq] ]
The state is x = [Va[pv ∪ pq]; Vm[pq]] and each iteration solves J·Δx = −F with the
Jacobian assembled from the sparse partial derivatives (MATPOWER dSbus_dV, polar)::
∂S/∂Vm = diag(V) · conj(Y · diag(V/|V|)) + conj(diag(Y V)) · diag(V/|V|)
∂S/∂Va = j · diag(V) · conj(diag(Y V) − Y · diag(V))
J = [ real(∂S/∂Va)[pvpq, pvpq] real(∂S/∂Vm)[pvpq, pq] ]
[ imag(∂S/∂Va)[pq, pvpq] imag(∂S/∂Vm)[pq, pq] ]
factorised with scipy.sparse.linalg.splu. The mismatch is tested before each step, so a
start that already satisfies ‖F‖∞ ≤ tol reports zero iterations; the loop stops with
converged = False after max_iter updates, on a singular Jacobian, or when an update
produces a non-finite voltage (the last finite iterate is returned).
Start. Flat: Vm = 1, Va = 0 at PQ buses, Vm = v_set (the effective setpoint from
mambo_power.numerics.effective_roles) and Va = 0 at PV and slack buses. Warm: a
caller-supplied v0 (solve_ac builds one from the buses' stored vm_pu/va_deg under
init="auto"); PV and slack magnitudes are always overridden by the setpoint. The slack angle
is whatever the start carries (0 for flat).
Q-limit enforcement (pandapower semantics, spec design item 3; pandapower 3.3.0
pf/run_newton_raphson_pf.py:182-249 _run_ac_pf_with_qlims_enforced, itself MATPOWER
runpf.m:366-440 with pf.enforce_q_lims = 1). After every converged Newton solve the
reactive generation per bus is Qg = imag(V·conj(Y V)) + Q_load; every bus still PV whose
Qg > ΣQmax or Qg < ΣQmin (aggregate over its in-service generators, strict
comparison — pandapower :199-200; MATPOWER adds a 5e-6 opf.violation slack, pandapower
does not) is converted to PQ with Q_spec = Q_limit − Q_load (:224-242: the generator's
QG is pinned at the limit and folded into the bus load). All violators of a round are
converted together (enforce_q_lims=1, simultaneous), the next solve warm-starts from the
current voltages, and pins accumulate — a pinned bus is never restored to PV
(limited = r_[limited, mx], :235; the spec rejects the restore). The slack bus is never
converted (setdiff1d(changed_gens, ref), :227). The loop ends when a converged solve shows
no new violation; if violations persist after max_q_rounds re-solves the result carries
converged = False and a diagnostic (pandapower would raise LoadflowNotConverged). A
Newton solve that fails to converge ends the loop immediately without pinning.
Generator allocation (MATPOWER pfsoln; pandapower pypower/pfsoln.py:109-141 is a
verbatim copy). Active power: every generator keeps its dispatch except the first in-service
generator at the slack bus, which absorbs the slack-bus balance (the rule pf.dc already
applies). Reactive power: the bus total Qg_bus = imag(S) + Q_load is split among the bus's
in-service generators — equally when every generator's range is zero, otherwise
Qg_i = Qmin_i + (Qg_bus − ΣQmin) / (ΣQmax − ΣQmin) · (Qmax_i − Qmin_i) (proportional to
each generator's reactive range). A pinned bus's generators therefore sit exactly at their
individual limits.
SOLVER
module-attribute
¶
Linear-algebra backend name stamped into the result provenance.
AcOptions
¶
Bases: BaseModel
Options of the AC Newton-Raphson solve (spec design item 1).
tol is compared against the infinity norm of the per-unit power mismatch (MATPOWER
pf.tol semantics; pandapower's tolerance_mva is the same pu quantity despite its
name). init="auto" warm-starts from the buses' stored vm_pu/va_deg when every
in-service bus carries both, else flat; PV and slack magnitudes are always the setpoint.
AcSolution
dataclass
¶
AcSolution(
v: ComplexArray,
converged: bool,
iterations: int,
max_mismatch_pu: float,
q_limit_rounds: int,
q_limited: IntArray,
bus_type: IntArray,
s_bus_pu: ComplexArray,
gen_p_pu: FloatArray,
gen_q_pu: FloatArray,
message: str | None = None,
)
Positional AC solution in per unit, in NetworkArrays order.
converged
instance-attribute
¶
Final Newton solve met tol and no Q-limit violation remained.
max_mismatch_pu
instance-attribute
¶
Infinity norm of the final mismatch vector, pu.
q_limit_rounds
instance-attribute
¶
Number of re-solves triggered by pinning (0 when nothing was pinned).
q_limited
instance-attribute
¶
Per bus: 0 free, +1 pinned at ΣQmax, -1 pinned at ΣQmin.
bus_type
instance-attribute
¶
Effective bus types after pinning (1 = pq, 2 = pv, 3 = slack).
s_bus_pu
instance-attribute
¶
Realised net complex injection per bus V·conj(Y V).
gen_p_pu
instance-attribute
¶
Per-generator active output; the first slack-bus generator absorbs the balance.
gen_q_pu
instance-attribute
¶
Per-generator reactive output, split by the MATPOWER pfsoln rule.
message
class-attribute
instance-attribute
¶
Diagnostic when converged is False; None otherwise.
flat_start
¶
flat_start(
arr: NetworkArrays, roles: EffectiveRoles
) -> ComplexArray
1∠0 at PQ buses, v_set∠0 at PV and slack buses (effective roles).
Source code in src/mambo_power/pf/ac_newton.py
specified_injection
¶
specified_injection(arr: NetworkArrays) -> ComplexArray
(P_gen − P_load) + j(Q_gen − Q_load) per bus, pu; shunts are in Y, not here.
Source code in src/mambo_power/pf/ac_newton.py
newton_raphson
¶
newton_raphson(
y: Any,
s_spec: ComplexArray,
v0: ComplexArray,
pv: IntArray,
pq: IntArray,
*,
tol: float,
max_iter: int,
) -> tuple[ComplexArray, bool, int, float, str | None]
One Newton solve (MATPOWER newtonpf): (V, converged, iterations, ‖F‖∞, message).
Source code in src/mambo_power/pf/ac_newton.py
allocate_generation
¶
allocate_generation(
arr: NetworkArrays,
s_bus: ComplexArray,
q_limited: IntArray,
) -> tuple[FloatArray, FloatArray]
Per-generator (P, Q) from the bus totals: MATPOWER pfsoln rules (module docstring).
A pinned bus (q_limited ±1) reports exactly its aggregate limit rather than the solved
imag(S) + Q_load, which differs from it by the convergence tolerance (pandapower restores
fixedQg the same way, run_newton_raphson_pf.py:246).
Source code in src/mambo_power/pf/ac_newton.py
newton
¶
newton(
arr: NetworkArrays,
roles: EffectiveRoles,
opts: AcOptions,
v0: ComplexArray | None = None,
) -> AcSolution
Solve the AC power flow of arr with the effective roles (module docstring).
v0 is the starting voltage (flat start when None); PV and slack magnitudes in it are
replaced by the effective setpoints. Never raises for a non-converged solve — the result
carries converged = False and a message.