mambo_power.model¶
The network data model: the Network root, every entity, the named validation errors and
the public re-check. See the manual page for field tables and the
validation-code reference.
mambo_power.model
¶
Data model: the pydantic v2 Network and its entities, named validation errors, typed
import warnings, and the island repair every importer applies before validation.
BranchKind
module-attribute
¶
What a Branch is. Exporters route on it (a pandapower trafo row, a PyPSA
Transformer, a RAW transformer record); the solvers do not read it.
BusType
module-attribute
¶
Bus role in power flow. MATPOWER type 4 (isolated) maps to in_service=False instead.
ValidationCode
module-attribute
¶
ValidationCode = Literal[
"NO_SLACK",
"MULTIPLE_SLACK",
"DISCONNECTED_BUS",
"DUPLICATE_ID",
"DANGLING_REF",
"BAD_BASE",
"BAD_RANGE",
]
The closed set of network-level validation codes (wave M1 design item 5).
ImportIssueCode
module-attribute
¶
ImportIssueCode = Literal[
"ISLAND_DEACTIVATED",
"BASE_KV_REPLACED",
"GENCOST_REACTIVE_IGNORED",
"CSV_MANIFEST_INVALID",
"CSV_SCHEMA_VERSION",
"CSV_MISSING_TABLE",
"CSV_UNKNOWN_COLUMN",
"CSV_MISSING_COLUMN",
"CSV_DUPLICATE_ID",
"CSV_BAD_VALUE",
"CSV_ORPHAN_ROW",
"RAW_NO_COSTS",
"RAW_LOAD_ZIP_FOLDED",
"RAW_BRANCH_END_SHUNT_FOLDED",
"RAW_XFMR_MAGNETISING_FOLDED",
"RAW_THREE_WINDING_IGNORED",
"RAW_SWITCHED_SHUNT_IGNORED",
"RAW_SECTION_IGNORED",
"EXTRA_EXT_GRID_DEMOTED",
"GEN_SLACK_PROMOTED",
"TAP_CHANGER_TYPE_UNSUPPORTED",
"COLUMN_DROPPED",
"ELEMENT_DROPPED",
"FIELD_DEFAULTED",
"FIELD_DROPPED",
"COST_DROPPED",
"BID_DROPPED",
"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",
]
The closed set of importer/repair warning codes.
ISLAND_DEACTIVATED — mambo_power.model.repair_islands switched an island off.
BASE_KV_REPLACED — MATPOWER BASE_KV <= 0 replaced by the importer default.
GENCOST_REACTIVE_IGNORED — a 2 * ngen-row gencost had its reactive half dropped.
The CSV_* codes are mambo_power.io.csv_bundle import errors (never warnings):
CSV_MANIFEST_INVALID — manifest.json missing, unparsable, or disagreeing with the tables;
CSV_SCHEMA_VERSION — the manifest names a schema version this build does not read;
CSV_MISSING_TABLE — a table file the bundle must carry is absent;
CSV_UNKNOWN_COLUMN / CSV_MISSING_COLUMN — a table header is not the model's field list;
CSV_DUPLICATE_ID — an id appears twice in one table;
CSV_BAD_VALUE — a cell does not parse as its column's type (nan included), or an
entity fails its own field validation;
CSV_ORPHAN_ROW — a cost/bid side-table row whose owner is absent or carries no kind.
RAW v33 import (mambo_power.io.psse_raw, M8 W4):
RAW_NO_COSTS — RAW carries no economic data; every generator imports with cost=None
(once per file).
RAW_LOAD_ZIP_FOLDED — a load's constant-current/constant-admittance parts (IP IQ YP YQ)
were folded into p_mw/q_mvar at the bus's VM (MATPOWER's rule).
RAW_BRANCH_END_SHUNT_FOLDED — a branch end shunt (GI BI / GJ BJ) became a Shunt.
RAW_XFMR_MAGNETISING_FOLDED — a transformer's MAG1/MAG2 became a Shunt at the
from bus.
RAW_THREE_WINDING_IGNORED — a three-winding transformer record (K != 0) was skipped.
RAW_SWITCHED_SHUNT_IGNORED — a switched shunt record was skipped (BINIT not folded).
RAW_SECTION_IGNORED — a record of an unread section (owner, DC lines, FACTS, ...) was
skipped; the message names the section and the record.
Wave M8 interop codes (io.pandapower_json first; later format modules reuse them):
EXTRA_EXT_GRID_DEMOTED — a second in-service ext_grid imported as a PV generator.
GEN_SLACK_PROMOTED — with no in-service ext_grid, a gen with slack = True
(pandapower's ext_grid-less reference bus) imported as the slack generator.
TAP_CHANGER_TYPE_UNSUPPORTED — a pandapower trafo tap changer the model cannot express
(an unknown tap_changer_type, an Ideal shifter with both step columns set, a
tap_side that is neither hv nor lv); imported at the nominal tap.
COLUMN_DROPPED — an imported column value the model has no field for (element and field named).
ELEMENT_DROPPED — a whole row (either direction) with no counterpart on the other side.
FIELD_DEFAULTED — a required field the source left empty (or the target format demands),
filled with a stated default.
FIELD_DROPPED — an exported model field the target format has no column for.
COST_DROPPED — a generator cost the target format cannot express (never approximated).
BID_DROPPED — a load bid the target format cannot express.
PYPSA_* — fields mambo_power.io.pypsa dropped because PyPSA cannot carry them
(piecewise or degree > 2 costs, load bids, zones, generator Q limits, a ramp on a zero-capacity
generator), collapsed (disagreeing voltage setpoints at one bus) or approximated (an unrated
branch written with the s_nom sentinel, PYPSA_UNRATED_S_NOM_DEFAULTED), plus
PYPSA_COST_NONCONVEX for a concave quadratic cost (c2 < 0) exported unchanged that
PyPSA's solver will refuse.
Branch
¶
Bases: _Entity
Line or transformer between two buses. Tap is on the from side (MATPOWER model).
b
class-attribute
instance-attribute
¶
Total line charging susceptance, per unit on base_mva.
rating_mva
class-attribute
instance-attribute
¶
Thermal rating, MVA; None = none.
shift_deg
class-attribute
instance-attribute
¶
Phase shift, degrees; None = 0.
kind
class-attribute
instance-attribute
¶
kind: BranchKind = 'line'
'line' or 'transformer'. Defaults to 'transformer' when tap_ratio is not None/1.0 or shift_deg is not None/0.0, else 'line'; an explicit 'transformer' at nominal tap is kept; an explicit 'line' with a tap or shift is promoted to 'transformer' at validation. Assignment after construction does not re-run this rule: exporters route on is_transformer, which reads the fields.
is_transformer
property
¶
What exporters route on: kind == "transformer" or an off-nominal tap/shift.
kind is derived at validation and entities are mutable, so br.tap_ratio = 1.05
on a line leaves kind == "line" in memory until the next validation. Reading the
fields here means an exporter never drops such a tap, and every serialisation
(model_dump, the native and CSV dumps) writes this answer as kind so a file
never says line beside a tap (M8 critic nit 24).
Bus
¶
Generator
¶
Bases: _Entity
Dispatchable injection. cost is model-present and solver-ignored until M3.
q_mvar
class-attribute
instance-attribute
¶
Reactive power setpoint or dispatch, MVAr.
p_min_mw
class-attribute
instance-attribute
¶
Lower active limit, MW; must be <= p_max_mw.
q_min_mvar
class-attribute
instance-attribute
¶
Lower reactive limit, MVAr; must be <= q_max_mvar.
ramp_up_mw
class-attribute
instance-attribute
¶
Max increase from the previous period, MW; None = none.
ramp_down_mw
class-attribute
instance-attribute
¶
Max decrease from the previous period, MW; None = none.
Geo
¶
Load
¶
Bases: _Entity
Demand at a bus, fixed or elastic. bid is read by market.nodal and
market.multiperiod; opf.solve_dc_opf ignores it and serves p_mw in full.
p_mw
class-attribute
instance-attribute
¶
Active demand, MW. With a bid, this is the maximum quantity that can be served, and the bid decides where in [0, p_mw] the load actually clears; a Period override of it therefore moves that maximum too.
bid
class-attribute
instance-attribute
¶
Elastic-demand bid covering this Load's entire p_mw; market.nodal and market.multiperiod read it, opf.solve_dc_opf does not. There is no per-Load partial-capacity split -- bidding only part of a load's capacity, with the rest must-serve, means splitting it into two Load entities at the same bus (one bid=None fixed, one bid-carrying).
PiecewiseBid
¶
Bases: _Entity
Demand-side mirror of PiecewiseCost: piecewise-linear (p_mw, value) breakpoints.
Only the same structural checks PiecewiseCost gets (at least two points,
strictly increasing p_mw) apply here; the concavity direction of the value curve is
checked at solve time, not here (see PolynomialBid).
points
class-attribute
instance-attribute
¶
(p_mw, value) breakpoints, at least two and at most 200 (each adds one hypograph row to opf.dc_opf's LP); p_mw must be strictly increasing.
PiecewiseCost
¶
Bases: _Entity
MATPOWER gencost MODEL 1: piecewise-linear (p_mw, cost) breakpoints, increasing in p.
points
class-attribute
instance-attribute
¶
(p_mw, cost) breakpoints, at least two and at most 200 (each adds one epigraph row to opf.dc_opf's LP); p_mw must be strictly increasing.
PolynomialBid
¶
Bases: _Entity
Demand-side mirror of PolynomialCost: value(p_mw) = sum(c_k * p_mw**k).
Marginal value must be non-increasing (concave value) for a valid market clearing; that
convexity-direction check is opf.dc_opf's job at solve time
(NonConcaveBidError), not construction-time here — mirrors how
PolynomialCost also has no construction-time convexity check.
coefficients
class-attribute
instance-attribute
¶
Polynomial coefficients (at least one), highest order first, value per hour.
PolynomialCost
¶
Bases: _Entity
MATPOWER gencost MODEL 2: cost(p_mw) = sum(c_k * p_mw**k), highest order first.
coefficients
class-attribute
instance-attribute
¶
Polynomial coefficients (at least one), highest order first, cost per hour.
Shunt
¶
Storage
¶
Bases: _Entity
Energy storage. Read by market.multiperiod, which gives each unit a charge, a
discharge and a state-of-charge column per period; every other solver ignores it.
Zone
¶
Bases: _Entity
Named grouping of buses (MATPOWER loss zone, market zone, ...).
NetworkValidationError
¶
NetworkValidationError(issues: Iterable[ValidationIssue])
Bases: Exception
Raised by Network when one or more invariants fail.
Subclasses Exception rather than ValueError on purpose: pydantic-core
converts any ValueError raised inside a validator into its own ValidationError,
which would hide .issues behind a generic message. A plain Exception propagates
unchanged through Network(...), model_validate and model_validate_json.
Source code in src/mambo_power/model/errors.py
ValidationIssue
¶
Bases: BaseModel
One invariant violation: stable code, document path, readable message.
Network
¶
Bases: BaseModel
A complete power network. Construction validates every invariant in one pass.
Any violation raises NetworkValidationError listing every issue found. Mutating
a constructed network does not re-validate; call validate_network to re-check.
Period
¶
Bases: BaseModel
One period's load overrides within a multi-period Scenario.
load_p_mw is an id-keyed override of each Load's p_mw for this period, not
a scale factor: a load id absent from the dict falls back unchanged to that Load's own
p_mw. mambo_power.market.multiperiod.solve_multiperiod resolves it into that
period's fixed load and, for a load carrying a bid, the upper bound of its elastic
column — Load.p_mw means both, so an override of it moves both. Every key must resolve to
a real Load id in the scenario's network, checked by Scenario rather than here,
since a bare Period has no network to check against.
The value range is exactly Load.p_mw's, deliberately: an override may not be narrower
than the field it overrides, or a horizon could not express a load the network itself is
allowed to carry. Load.p_mw has no lower bound and case300 ships eight negative
loads (a net injection at a load bus), so a >= 0 rule here would reject even the
identity profile {ld.id: ld.p_mw for ld in case300.loads} — a horizon that changes
nothing — on a network market.nodal clears without complaint. allow_inf_nan=False
still holds: a non-finite override is meaningless in any period.
load_p_mw
class-attribute
instance-attribute
¶
Per-load active-power override for this period, MW, keyed by Load id. The same range as Load.p_mw, negatives included; must be finite.
Scenario
¶
Bases: BaseModel
A market scenario to clear: the network to clear it against, and its periods, if any.
Embeds network: Network directly, mirroring jobs.models.SolveRequest's
self-contained pattern rather than an id/path cross-reference — no such resolution
mechanism exists anywhere else in this codebase.
Network's own model_validator(mode="after") runs while Scenario is being
constructed (it is a nested pydantic model field), so every invariant Network already
checks — including dangling references — is checked here too, with no separate pass needed.
periods: list[Period] | None = None — None means single-period: market.nodal's
existing behaviour is unaffected (AC-4, wave M5). No agent-strategy fields this wave: their
eventual shape is genuinely undesigned by M7, unlike Storage's successful M1 stub, which
had a full spec before it shipped (design interview 2026-08-24, ratified; wave spec Design
item 3).
network
class-attribute
instance-attribute
¶
network: Network
The network to clear; the scenario is self-contained.
periods
class-attribute
instance-attribute
¶
periods: list[Period] | None
Per-period load overrides; None = single-period, market.nodal semantics unchanged. If given, must be non-empty and at most MAX_PERIODS (200) entries -- a wire-format decompression-bomb guard: constraint-matrix nonzeros scale with horizon length, and M5 measured a 33,997-byte request expanding to 20,088,000 of them.
ImportIssue
¶
Bases: BaseModel
One repair an importer performed: stable code, message, and the ids it touched.
repair_islands
¶
repair_islands(
net: Network,
) -> tuple[Network, list[ImportIssue]]
Return a new, validated Network with every island switched off, plus warnings.
net is not modified. On an already-connected network the result is an equal copy and
the warning list is empty.
Source code in src/mambo_power/model/islands.py
repair_islands_entities
¶
repair_islands_entities(
buses: Sequence[Bus],
branches: Sequence[Branch],
generators: Sequence[Generator],
loads: Sequence[Load],
shunts: Sequence[Shunt],
storage: Sequence[Storage],
) -> RepairedEntities
Deactivate island buses and their elements on raw entity lists; inputs are not mutated.
Returns new lists (copies only where an element changed) and one warning per island.
Works before validation, so it accepts entity lists that Network would reject
with DISCONNECTED_BUS.
Source code in src/mambo_power/model/islands.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
validate_network
¶
validate_network(net: Network) -> list[ValidationIssue]
Run every cross-entity invariant and return all issues found (empty = valid).
Source code in src/mambo_power/model/network.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | |
Entities¶
mambo_power.model.entities
¶
Network entities — the native file format IS these models.
Units are physical (wave M1 design item 1): MW, MVAr, kV, MWh, degrees; branch r/x/b
are per-unit on the network's base_mva. Per-unit conversion lives in numerics, never
here. Field names are snake_case with a unit suffix (design item 2). Every model rejects
unknown fields so a typo in a file is an error, not silently dropped data.
Cross-entity invariants (slack count, connectivity, references, ranges) are checked by
mambo_power.model.Network, not by the entities themselves, so that one validation
pass can report every problem at once.
BusType
module-attribute
¶
Bus role in power flow. MATPOWER type 4 (isolated) maps to in_service=False instead.
BranchKind
module-attribute
¶
What a Branch is. Exporters route on it (a pandapower trafo row, a PyPSA
Transformer, a RAW transformer record); the solvers do not read it.
Geo
¶
Bus
¶
Branch
¶
Bases: _Entity
Line or transformer between two buses. Tap is on the from side (MATPOWER model).
b
class-attribute
instance-attribute
¶
Total line charging susceptance, per unit on base_mva.
rating_mva
class-attribute
instance-attribute
¶
Thermal rating, MVA; None = none.
shift_deg
class-attribute
instance-attribute
¶
Phase shift, degrees; None = 0.
kind
class-attribute
instance-attribute
¶
kind: BranchKind = 'line'
'line' or 'transformer'. Defaults to 'transformer' when tap_ratio is not None/1.0 or shift_deg is not None/0.0, else 'line'; an explicit 'transformer' at nominal tap is kept; an explicit 'line' with a tap or shift is promoted to 'transformer' at validation. Assignment after construction does not re-run this rule: exporters route on is_transformer, which reads the fields.
is_transformer
property
¶
What exporters route on: kind == "transformer" or an off-nominal tap/shift.
kind is derived at validation and entities are mutable, so br.tap_ratio = 1.05
on a line leaves kind == "line" in memory until the next validation. Reading the
fields here means an exporter never drops such a tap, and every serialisation
(model_dump, the native and CSV dumps) writes this answer as kind so a file
never says line beside a tap (M8 critic nit 24).
PolynomialCost
¶
Bases: _Entity
MATPOWER gencost MODEL 2: cost(p_mw) = sum(c_k * p_mw**k), highest order first.
coefficients
class-attribute
instance-attribute
¶
Polynomial coefficients (at least one), highest order first, cost per hour.
PiecewiseCost
¶
Bases: _Entity
MATPOWER gencost MODEL 1: piecewise-linear (p_mw, cost) breakpoints, increasing in p.
points
class-attribute
instance-attribute
¶
(p_mw, cost) breakpoints, at least two and at most 200 (each adds one epigraph row to opf.dc_opf's LP); p_mw must be strictly increasing.
PolynomialBid
¶
Bases: _Entity
Demand-side mirror of PolynomialCost: value(p_mw) = sum(c_k * p_mw**k).
Marginal value must be non-increasing (concave value) for a valid market clearing; that
convexity-direction check is opf.dc_opf's job at solve time
(NonConcaveBidError), not construction-time here — mirrors how
PolynomialCost also has no construction-time convexity check.
coefficients
class-attribute
instance-attribute
¶
Polynomial coefficients (at least one), highest order first, value per hour.
PiecewiseBid
¶
Bases: _Entity
Demand-side mirror of PiecewiseCost: piecewise-linear (p_mw, value) breakpoints.
Only the same structural checks PiecewiseCost gets (at least two points,
strictly increasing p_mw) apply here; the concavity direction of the value curve is
checked at solve time, not here (see PolynomialBid).
points
class-attribute
instance-attribute
¶
(p_mw, value) breakpoints, at least two and at most 200 (each adds one hypograph row to opf.dc_opf's LP); p_mw must be strictly increasing.
Generator
¶
Bases: _Entity
Dispatchable injection. cost is model-present and solver-ignored until M3.
q_mvar
class-attribute
instance-attribute
¶
Reactive power setpoint or dispatch, MVAr.
p_min_mw
class-attribute
instance-attribute
¶
Lower active limit, MW; must be <= p_max_mw.
q_min_mvar
class-attribute
instance-attribute
¶
Lower reactive limit, MVAr; must be <= q_max_mvar.
ramp_up_mw
class-attribute
instance-attribute
¶
Max increase from the previous period, MW; None = none.
ramp_down_mw
class-attribute
instance-attribute
¶
Max decrease from the previous period, MW; None = none.
Load
¶
Bases: _Entity
Demand at a bus, fixed or elastic. bid is read by market.nodal and
market.multiperiod; opf.solve_dc_opf ignores it and serves p_mw in full.
p_mw
class-attribute
instance-attribute
¶
Active demand, MW. With a bid, this is the maximum quantity that can be served, and the bid decides where in [0, p_mw] the load actually clears; a Period override of it therefore moves that maximum too.
bid
class-attribute
instance-attribute
¶
Elastic-demand bid covering this Load's entire p_mw; market.nodal and market.multiperiod read it, opf.solve_dc_opf does not. There is no per-Load partial-capacity split -- bidding only part of a load's capacity, with the rest must-serve, means splitting it into two Load entities at the same bus (one bid=None fixed, one bid-carrying).
Shunt
¶
Storage
¶
Bases: _Entity
Energy storage. Read by market.multiperiod, which gives each unit a charge, a
discharge and a state-of-charge column per period; every other solver ignores it.
Validation errors¶
mambo_power.model.errors
¶
Named validation errors for mambo_power.model.Network.
Every invariant violation is reported as a ValidationIssue with a stable code,
a path into the network document (buses[3].base_kv) and a human-readable message.
NetworkValidationError carries all issues found in one pass, never just the first.
ValidationCode
module-attribute
¶
ValidationCode = Literal[
"NO_SLACK",
"MULTIPLE_SLACK",
"DISCONNECTED_BUS",
"DUPLICATE_ID",
"DANGLING_REF",
"BAD_BASE",
"BAD_RANGE",
]
The closed set of network-level validation codes (wave M1 design item 5).
ValidationIssue
¶
Bases: BaseModel
One invariant violation: stable code, document path, readable message.
NetworkValidationError
¶
NetworkValidationError(issues: Iterable[ValidationIssue])
Bases: Exception
Raised by Network when one or more invariants fail.
Subclasses Exception rather than ValueError on purpose: pydantic-core
converts any ValueError raised inside a validator into its own ValidationError,
which would hide .issues behind a generic message. A plain Exception propagates
unchanged through Network(...), model_validate and model_validate_json.
Source code in src/mambo_power/model/errors.py
Network and invariants¶
mambo_power.model.network
¶
The Network root model and its cross-entity invariants.
Network
¶
Bases: BaseModel
A complete power network. Construction validates every invariant in one pass.
Any violation raises NetworkValidationError listing every issue found. Mutating
a constructed network does not re-validate; call validate_network to re-check.
validate_network
¶
validate_network(net: Network) -> list[ValidationIssue]
Run every cross-entity invariant and return all issues found (empty = valid).
Source code in src/mambo_power/model/network.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | |