mambo_power.io.pypsa¶
PyPSA export: to_network(net) returns a pypsa.Network built from a Network;
to_network_with_report also returns the ExportReport naming every field PyPSA cannot carry
(piecewise and degree > 2 costs, load bids, zones, generator reactive limits). PyPSA is
imported lazily. No generator ever carries p_set — PyPSA's optimiser reads it as a fixed
dispatch. CODES lists the report codes this module can emit.
mambo_power.io.pypsa
¶
PyPSA export (wave M8, W3): Network → pypsa.Network, plus what was dropped.
PyPSA is imported lazily, inside to_network, so the core package keeps its
zero-optional-dependency import (design item R9; pypsa is a dev extra).
Field map (m8-research.md §2; verified against opf.solve_dc_opf by
tests/parity/test_pypsa_export_vs_pypsa.py):
Bus→Bus:v_nom = base_kv,v_mag_pu_min/max,controlfromtype(Slack/PV/PQ),x/yfromgeo(lon, lat),v_mag_pu_setfrom the bus's in-service generators'v_set_pu(an out-of-service unit's setpoint is not what the bus holds);area/zoneride along as custom columns (PyPSA keeps unknown columns through CSV export/import). PyPSA buses have noactiveflag, soin_serviceis kept as a customin_servicecolumn and every element at an out-of-service bus is exportedactive = False— whatnumerics.NetworkArraysdoes with such elements, so the solvers on both sides see the same network.Branchwithis_transformerfalse (kind == "line"at a nominal tap) →Linein physical units on the from-bus base:Zb = base_kv² / base_mva;r, xin ohm (× Zb),bin siemens (÷ Zb).Branchwithis_transformer(kind == "transformer"or an off-nominal tap/shift, so a line mutated to carry a tap is not dropped) →Transformer(model="pi")withr, x, bper unit on the transformer's owns_nom(impedancesr, x× s_nom / base_mva; the admittanceb× base_mva / s_nom),tap_ratio,tap_side=0(mambo's tap is on the from side) andphase_shiftin degrees.rating_mva→s_nom; an unrated branch getsUNRATED_S_NOM_MVAbecause PyPSA's optimiser readss_nom == 0as "carries nothing", not "unlimited" -- an approximation, so the report names each such branch (PYPSA_UNRATED_S_NOM_DEFAULTED, M8 walk surprise 4).Generator→Generator:p_nom = max(|p_min_mw|, |p_max_mw|),p_min_pu/p_max_puas fractions of it (sop_nom == p_max_mwin the ordinary case and a negative-only range survives too),marginal_cost = c1,marginal_cost_quadratic = c2, the constantc0in the customCOST_CONSTANT_COLUMNcolumn (n.objectiveexcludes constants),ramp_limit_up/downas fractions ofp_nom,start_up_cost/shut_down_cost,control,active.p_setis never written: a non-NaNp_setpins the dispatch inoptimize()(tests/parity/test_opf_vs_pypsa.py's root cause).phase_shiftis carried faithfully and honoured by PyPSA's linear power flow (n.lpf()agrees withpf.solve_dcon a shifted loop, sign included), but PyPSA 1.2.4'soptimize()never reads it (pypsa/optimizationhas no reference tophase_shift; measured intests/parity/test_pypsa_export_vs_pypsa.py). The AC-3 parity claim is therefore for shift-free networks; a phase shifter is exported, not dropped, so it is not reported.Load→Load(p_set, q_set);Shunt→ShuntImpedance(g, b)in siemens (MW / kV²);Storage→StorageUnit(p_nom, max_hours, state_of_charge_initial, efficiency_store, efficiency_dispatch);base_mva→n.meta["base_mva"].
Dropped and reported (design item D1 — never approximated; each entry names the element id
and the field): piecewise costs, polynomial costs of effective degree > 2, load bids, zones,
generator reactive limits, a ramp on a zero-capacity generator, and disagreeing voltage
setpoints at one bus (PyPSA has one v_mag_pu_set per bus). A concave quadratic cost
(c2 < 0) is exported as it is and reported (PYPSA_COST_NONCONVEX): PyPSA hands it to
the solver, which rejects the non-convex QP with its own error rather than a mambo issue.
CODES lists the codes.
CODES
module-attribute
¶
CODES: tuple[str, ...] = (
"PYPSA_PWL_COST_DROPPED",
"PYPSA_COST_DEGREE_DROPPED",
"PYPSA_LOAD_BID_DROPPED",
"PYPSA_ZONE_DROPPED",
"PYPSA_GEN_Q_LIMITS_DROPPED",
"PYPSA_GEN_RAMP_DROPPED",
"PYPSA_GEN_VSET_CONFLICT",
"PYPSA_UNRATED_S_NOM_DEFAULTED",
"PYPSA_COST_NONCONVEX",
)
Every report code this exporter can emit (its documented limitations).
UNRATED_S_NOM_MVA
module-attribute
¶
s_nom written for a branch with rating_mva = None. PyPSA's optimiser bounds every
branch flow by s_nom, so 0 would mean "no flow", not "no limit"; this sentinel is far above
any flow on the fixtures (case300's whole dispatch is 2.4e4 MW) and small enough not to hurt the
LP's scaling.
COST_CONSTANT_COLUMN
module-attribute
¶
Custom generator column holding the polynomial cost's constant term c0 (cost per hour at
any dispatch). PyPSA has no attribute for it and n.objective excludes constants; a caller
comparing objectives adds n.generators[COST_CONSTANT_COLUMN].sum().
to_network
¶
to_network(net: Network) -> Network
Export net as a pypsa.Network, discarding the report.
See to_network_with_report for what may be dropped; this form is for callers who
already know their network is within what PyPSA can express.
Source code in src/mambo_power/io/pypsa.py
to_network_with_report
¶
to_network_with_report(
net: Network,
) -> tuple[Network, ExportReport]
Export net as a pypsa.Network and report every field PyPSA cannot carry.
An empty report means the export was lossless. The report only ever carries warnings — every
unsupported field is dropped, never refused (D1) — so raise_on_error is a no-op.