Tutorial 1 · Your first power flow¶
Difficulty: beginner · ~5 min read
This is the first of four tutorials that walk through mambo-power end to end. Each one builds
on the last: this one loads a network and asks "what happens right now, given how it's already
being run?" — tutorial 2 asks "what should happen, if we're free to
choose the dispatch?", tutorial 3 asks "what happens when participants
get to bid?", and tutorial 4 points you toward the two places the story
forks from there.
If you haven't installed the package yet, see Getting started for
current instructions. All four tutorials need a clone of the repository on disk, not just
pip install mambo-power — the MATPOWER fixtures they load (case14.m and friends) ship in the
repository, not in the installed wheel, so mambo_power.io.matpower.load needs the source tree
next to the notebook regardless of how you installed the package itself.
Load a case¶
mambo_power doesn't ship synthetic toy grids as its primary teaching tool — it ships real
MATPOWER test cases, the ones the power-systems research community has
used for decades. We'll use case14, the IEEE 14-bus system: small enough to read in full,
large enough to have real structure (a loop, several voltage levels, five generators).
io.matpower.load reads the .m file and returns a validated Network — the same model every
solver, market clearing, and file format in this package speaks.
from mambo_power.io import matpower
net = matpower.load("../../fixtures/matpower/case14.m")
print(
f"{len(net.buses)} buses, {len(net.branches)} branches, "
f"{len(net.generators)} generators, {len(net.loads)} loads"
)
14 buses, 20 branches, 5 generators, 11 loads
The Network model, briefly¶
A Network is a plain, physical-units description of a grid: buses (nodes), branches (lines and
transformers), generators, loads, and a few less common entities. Every field carries a real
unit — megawatts, kilovolts, per-unit — and every reference between entities (a branch's
from_bus, a generator's bus) is a stable string id, never a positional index. The full
class-by-class reference, including every field and validation rule, is in the
Network model manual page — this tutorial only needs enough to read what
comes back from a solve.
Here's one bus and one branch from case14, to see the shape:
print(net.buses[0])
print(net.branches[0])
id='bus-1' base_kv=1.0 type='slack' in_service=True vm_pu=1.06 va_deg=0.0 v_min_pu=0.94 v_max_pu=1.06 area='1' zone='1' geo=None id='branch-1' from_bus='bus-1' to_bus='bus-2' r=0.01938 x=0.05917 b=0.0528 rating_mva=None tap_ratio=None shift_deg=None in_service=True kind='line'
net is immutable in spirit even though pydantic models are technically mutable: no solver
in this package ever modifies the network you hand it. Every solve returns a new, separate
result object.
DC vs AC power flow¶
mambo_power.pf holds two power-flow solvers, and the difference between them is the first real
modeling choice you'll meet in this package.
DC power flow (pf.solve_dc) is a linearised approximation: it assumes voltage magnitudes
are flat at 1.0 pu everywhere, angle differences across any branch are small, and it ignores
resistance and line charging entirely. In exchange for those approximations it's a single linear
solve — fast, robust, no iteration, no risk of non-convergence — and it's exact for what it
models. It's the workhorse behind PTDF-based sensitivity analysis and, in the next tutorial,
optimal dispatch.
AC power flow (pf.solve_ac) solves the real nonlinear power-balance equations by
Newton-Raphson: real voltage magnitudes, real angles, real losses. It's the ground truth — the
question DC power flow only approximates an answer to — but it's iterative and, on a harder
network, can fail to converge.
A rule of thumb: reach for DC when you want a fast, well-behaved linear model (especially for anything built on sensitivities, like N-1 screening or DC-OPF in the next tutorial); reach for AC when you need real voltages, real reactive power, or real losses. See Power flow for the full formulation of both.
from mambo_power import pf
dc_result = pf.solve_dc(net)
print("DC converged:", dc_result.converged)
for bus in dc_result.buses[:3]:
print(
f" {bus.id:8s} va={bus.va_deg:8.3f} deg p={bus.p_mw:8.2f} MW role={bus.role_effective}"
)
DC converged: True bus-1 va= 0.000 deg p= 219.00 MW role=slack bus-2 va= -5.012 deg p= 18.30 MW role=pv bus-3 va= -12.954 deg p= -94.20 MW role=pv
ac_result = pf.solve_ac(net, options=pf.AcOptions(init="flat"))
print("AC converged:", ac_result.converged, " iterations:", ac_result.iterations)
for bus in ac_result.buses[:3]:
print(
f" {bus.id:8s} vm={bus.vm_pu:.4f} pu va={bus.va_deg:8.3f} deg q={bus.q_mvar:7.2f} MVAr"
)
AC converged: True iterations: 4 bus-1 vm=1.0600 pu va= 0.000 deg q= -16.55 MVAr bus-2 vm=1.0450 pu va= -4.983 deg q= 30.86 MVAr bus-3 vm=1.0100 pu va= -12.725 deg q= 6.08 MVAr
Notice the angles are close between the two solves (DC's linearisation is a good approximation here) but not identical — and only the AC solve has voltage magnitudes at all, since DC assumes them all flat at 1.0 pu by construction. The AC solve also reports real active losses, which the lossless DC model cannot have:
losses_mw = sum(b.p_from_mw + b.p_to_mw for b in ac_result.branches)
print(f"AC active losses: {losses_mw:.3f} MW")
print("DC model has no losses by construction (r is ignored in the linearisation).")
AC active losses: 13.393 MW DC model has no losses by construction (r is ignored in the linearisation).
Branch flows¶
Both result types carry a per-branch flow table, keyed by the network's own branch ids:
for branch in dc_result.branches[:5]:
print(f" {branch.id:9s} {branch.from_bus}->{branch.to_bus} p_from={branch.p_from_mw:8.2f} MW")
branch-1 bus-1->bus-2 p_from= 147.84 MW branch-2 bus-1->bus-5 p_from= 71.16 MW branch-3 bus-2->bus-3 p_from= 70.01 MW branch-4 bus-2->bus-4 p_from= 55.15 MW branch-5 bus-2->bus-5 p_from= 40.97 MW
Try it yourself¶
The repository bundles five more MATPOWER cases under fixtures/matpower/: case30.m,
case_ieee30.m, case57.m, case118.m, and case300.m. As a quick exercise, try swapping
"case14.m" for "case30.m" in the first cell and re-running the notebook top to bottom — same
API, a different (still small) network. (This cell is left as a note, not executed code, so this
tutorial's own output stays fixture-independent and reproducible.)
Next¶
Tutorial 2 — DC-OPF and N-1 screening picks up exactly here: instead of solving a power flow at a dispatch someone else already chose, it lets the solver choose the cheapest dispatch, and then asks what happens if any one branch trips.