Algorithm¶
simplenet performs a DC modified-Ward reduction of a power system
model. The pipeline matches the MATLAB toolbox in
/matlab/NetworkReduction2/MPReduction.m
step for step, but uses an idiomatic
Kron reduction for the
core math (see MATLAB Comparison for the
rationale).
Pipeline¶
flowchart TD
raw["Input case<br/>xlsx / .m / pypower dict"] --> pp["Preprocess<br/>drop isolated buses, OOS branches"]
pp --> ymat["Build B matrix<br/>DC susceptance + shunts"]
ymat --> kron1["Kron reduction 1<br/>eliminate ALL external buses"]
ymat --> kron2["Kron reduction 2<br/>eliminate non-generator externals only"]
kron2 --> move["Move external generators<br/>multi-source Dijkstra"]
kron1 --> assemble["Assemble reduced PowerCase<br/>equivalent branches + bus shunts"]
move --> assemble
pp --> dcpf["DC power flow on full model<br/>only when pf_flag=True"]
dcpf --> redist["Redistribute loads<br/>so reduced DC PF matches full angles"]
assemble --> redist
redist --> prune["Drop |x| >= 10 * max(|x|_orig) equivalents"]
prune --> out["Reduced PowerCase<br/>+ bcirc + Link + Summary"]
1. Preprocess¶
Implemented in simplenet.preprocess.
Drops:
- Isolated buses (
type == 4). - Out-of-service branches (
status == 0). - Branches touching isolated buses.
- Generators on isolated buses.
- HVDC lines touching isolated buses.
The external bus list is filtered to exclude any newly removed buses.
2. Build the susceptance matrix¶
For the DC Ward reduction we use the bus-susceptance matrix B with:
- Off-diagonal
B[i, j] = -1 / xfor every branch between busesiandj(parallel branches summed). - Diagonal
B[i, i] = (sum of 1/x for branches at i) + (sum of b/2 for branch shunts at i) + (Bs_i / baseMVA)whereBs_iis the original bus shunt.
Note that for the reduction step we ignore branch tap ratios (this
matches Initiation.m exactly — the tap is reinstated for DC PF and
load redistribution).
3. Identify boundary buses¶
A boundary bus is a retained bus that shares at least one branch with an external bus.
Only entries between boundary buses can become equivalent branches in the reduced model.
4. Kron reduction (the math)¶
Partition the bus set into external (e) and internal (i) buses.
Block-decompose B and theta:
Solving the second row for theta_e and substituting into the first
row gives
kron_reduce computes this with a
single scipy.sparse.linalg.spsolve call. The reduced matrix
B_red is dense (size n_internal x n_internal), which is typically
much smaller than B.
5. Extract equivalent branches¶
For each pair of boundary buses (i, j) with i < j:
If |Delta_ij| > 1e-12 the reduction introduced a new admittance
between i and j; we add an equivalent branch with
and circuit number 99 (or a larger sentinel if any original branch
already used 99, computed as
max(99, 10**ceil(log10(max_orig_BCIRC - 1)) - 1)).
6. Compute bus shunts¶
The reduced bus shunt at bus i absorbs both the original bus shunt
and all branch-shunt contributions that the Kron reduction folded into
the diagonal:
Branch shunts on the reduced model are zeroed (branch[:, 4] = 0)
because their contribution now lives on the bus.
7. Second reduction + generator placement¶
External generators sit on buses that the first reduction eliminates. To find where they should "move to", we run a second Kron reduction that retains external generator buses (eliminating only external buses without a generator). The resulting graph has every external generator still connected to the internal network through a chain of branches.
move_external_generators
then:
- Collapses parallel branches via inverse-sum (so the multi-graph becomes simple).
- Builds a sparse weighted graph with edge weights
|x|(orsqrt(r^2 + x^2)ifac_flag=True). - Runs
scipy.sparse.csgraph.dijkstra(..., min_only=True)from the true internal bus set as multi-source. - Reads off
sources[k]— the closest internal bus — for every external-with-generator bus.
The first column of the reduced gen matrix is then overwritten with
this mapping, which is what mpcreduced.gen(:,1) = NewGenBus does in
MATLAB.
8. Load redistribution¶
After step 7 the reduced model still has the original Pd values. A
DC power flow on the reduced model would not match the full-model
solution. redistribute_loads
fixes that:
- Run a DC PF on the full model (
pf_flag=True) or reuse the existingVacolumn (pf_flag=False). - For each retained bus copy
VmandVafrom the full solution. - Build the reduced DC PF matrix
B_r(this one does include tap ratios, matching the originalLoadRedistribution.m). - Compute the per-bus injection vector `P_inj = B_r * theta * baseMVA
- P_shift` (phase-shifter contributions added).
- Set
Pd_new = Pg_total_at_bus - P_inj. - Correct for HVDC line injections at retained bus endpoints.
After this step a DC PF on the reduced model reproduces the retained buses' angles exactly (modulo the slack-shift; see MATLAB Comparison).
9. Prune large equivalent branches¶
The Kron reduction can manufacture equivalent branches whose reactance
is effectively infinite (very weak coupling between two faraway
boundary buses). These add nothing to the model's electrical behavior
and inflate the line count. Following MPReduction.m, the pipeline
drops every branch (equivalent or not) with
The threshold is taken before the reduction runs, against the original branch list.