mambo_power.results¶
Typed, id-keyed solver results with provenance. See the manual page for field semantics, the JSON round-trip and the positional view.
mambo_power.results
¶
Typed, id-keyed solver results with provenance.
Results are values produced by pf (and later opf, contingency, market) and
consumed by jobs and user code. They are pydantic v2 models — exact JSON round-trip,
unknown fields and non-finite numbers rejected — keyed by the network's stable ids, with a
positional to_arrays() view. They are never attached to a Network.
TerminationReason
module-attribute
¶
How mambo_power.market.agents.solve_agents' loop ended (spec A7). converged: the
offer vector repeated and the repetition's amplitude is within offer_tol. cycle: it
repeated with an amplitude wider than that -- a genuine cycle, which is not the iteration cap.
iteration_cap: max_iterations update rounds passed without any repetition at all.
BusRole
module-attribute
¶
The role a bus was solved with — the effective role, which may differ from the declared.
QLimitSide
module-attribute
¶
Which reactive limit a generator was pinned at by AC Q-limit enforcement; none for DC.
AgentOfferResult
¶
Bases: BaseModel
One agent's final-round offer, beside the true cost it was allowed to depart from.
Both curves are carried whole, as GeneratorCost objects, because
the whole point of the overlay is that they are two separate objects: true_cost is the
generator's own Generator.cost, untouched by the run (AC-2), and offer is what the
strategy handed the clearing. "Markup" is the difference, which only means anything because
neither one overwrote the other.
strategy
class-attribute
instance-attribute
¶
Which bidding rule produced this offer: the StrategyConfig.kind ("price_taker", "markup") when solve_agents built the strategy from MarketAgentsOptions.strategies, or the class name of the object an in-process caller passed to solve_agents' own strategies argument.
offer
class-attribute
instance-attribute
¶
The cost curve this agent offered in the final round -- what the clearing actually minimised against, never written back to the network.
true_cost
class-attribute
instance-attribute
¶
The generator's own Generator.cost, unchanged by the run.
cleared_mw
class-attribute
instance-attribute
¶
This agent's dispatch in the final round's clearing, MW; the same figure its GenDispatchResult row carries, repeated here because the markup identity below is stated in terms of it.
markup
class-attribute
instance-attribute
¶
offer(cleared_mw) - true_cost(cleared_mw), $/h: what the agent's departure from its own cost is worth at the quantity it actually cleared. Not independent content -- it is exactly that identity in the other three fields (spec A6), and tests/unit/test_market_agents.py asserts it as one.
MarketAgentsResult
¶
Bases: BaseModel
Result of mambo_power.market.agents.solve_agents.
When status != "Optimal" the clearing fields are left at their empty/zero defaults and
message carries the diagnostic, mirroring
MarketNodalResult's own convention; iterations still
reports the round the clearing failed in, since that is a fact about the loop rather than
about the clearing.
status
class-attribute
instance-attribute
¶
HiGHS model status of the final round's clearing: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim. This is the LP's verdict and says nothing about whether the loop converged -- see converged.
branches
class-attribute
instance-attribute
¶
branches: list[OpfBranchFlowResult]
Per-branch flow and flow-limit shadow price at the final round's dispatch -- the same field name and row type as MarketNodalResult.branches and MarketZonalResult.branches.
offers
class-attribute
instance-attribute
¶
offers: list[AgentOfferResult]
One row per agent -- a generator that MarketAgentsOptions.strategies (or solve_agents' strategies argument) named -- in NetworkArrays generator order. A generator with no strategy is not an agent: it clears at its own true cost and appears under generators only.
iterations
class-attribute
instance-attribute
¶
The final round's index: the number of best-response update rounds the loop ran after round 0. Round 0 is the initial offer and responds to nothing, so it is not an iteration; the loop therefore cleared the market iterations + 1 times. A fixed point is confirmed only after two identical updates (the loop's state is the pair of consecutive offer vectors), so iterations is at least 2 on any converged run -- an all-price-taker market, in which nothing moves, still reports 2.
converged
class-attribute
instance-attribute
¶
Whether the loop settled -- the offer vector repeated with an amplitude within offer_tol. Never a statement about the LP: see status. True exactly when termination_reason == "converged".
termination_reason
class-attribute
instance-attribute
¶
termination_reason: TerminationReason | None
How the loop ended (spec A7): converged | iteration_cap | cycle. None exactly when status != Optimal -- a clearing that failed produced no loop outcome to report, and inventing a fourth value here would fold the LP's verdict into the loop's, which is what this result exists to keep apart.
total_load_payment
class-attribute
instance-attribute
¶
Sum over every load of LMP(bus_d)*p_d in the final round's clearing, $/h; 0.0 when not Optimal.
total_generator_receipts
class-attribute
instance-attribute
¶
Sum over every generator of LMP(bus_g)*p_g in the final round's clearing, $/h -- paid at the final round's prices, on the final round's offers; 0.0 when not Optimal.
congestion_rent
class-attribute
instance-attribute
¶
total_load_payment - total_generator_receipts, $/h; 0.0 when not Optimal.
FeasibilityReport
¶
Bases: BaseModel
AC-feasibility check result: convergence plus thermal/voltage violations of a dispatch.
ThermalViolation
¶
VoltageViolation
¶
Bases: _Row
A bus outside its declared voltage-magnitude limits.
LoadDispatchResult
¶
Bases: BaseModel
One load's market.nodal dispatch and its bound's shadow price.
p_mw is the load's actual served demand: for a bid load, its solved elastic dispatch
(demand_dispatch_mw); for a load with no bid,
its own fixed historical Load.p_mw (it never became an LP column, so it has no reduced
cost -- bound_dual is 0.0). Every load in the network gets a row, bid or not, since
the settlement identity sums LMP·p_d over every load, not just the elastic ones -- the
identity's own derivation never assumes p_d is a decision variable.
bound_dual
class-attribute
instance-attribute
¶
Reduced cost of the load's [0, p_mw] bid bound; 0.0 for a non-bid load, since it is not a decision variable.
MarketNodalResult
¶
Bases: BaseModel
Result of mambo_power.market.nodal.solve_nodal.
When status != "Optimal" the dispatch/LMP/settlement fields are left at their empty/zero
defaults; message carries the diagnostic (mirrors
OpfDcResult's own convention for a non-converged/infeasible
solve).
status
class-attribute
instance-attribute
¶
HiGHS model status: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim.
branches
class-attribute
instance-attribute
¶
branches: list[OpfBranchFlowResult]
Per-branch flow and flow-limit shadow price at the solved dispatch -- the same field name and row type as MarketZonalResult.branches (module docstring, AC-8). Makes the settlement identity's flow-dual side, -sum_k(mu_k * f_k), computable from this object alone.
total_load_payment
class-attribute
instance-attribute
¶
Sum over every load of LMP(bus_d)*p_d, $/h -- computed directly from dispatch and LMPs, not asserted equal to the settlement identity's other side by construction (proved, not assumed, in tests/unit/test_market_nodal.py). 0.0 when not Optimal.
total_generator_receipts
class-attribute
instance-attribute
¶
Sum over every generator of LMP(bus_g)*p_g, $/h; 0.0 when not Optimal.
congestion_rent
class-attribute
instance-attribute
¶
total_load_payment - total_generator_receipts, $/h -- equals -sum_k(mu_k * flow_k) at the optimum (the settlement identity); 0.0 when not Optimal.
GenPeriodDispatchResult
¶
Bases: GenDispatchResult
One generator's dispatch in one period, plus the ramp row that reaches into that period.
Extends GenDispatchResult rather than replacing it: id,
bus, p_mw and bound_dual mean exactly what they mean in a single-period DC-OPF
result, so a reader who knows one knows the other.
ramp_dual
class-attribute
instance-attribute
¶
Dual of the two-sided ramp row coupling the previous period to this one, $/MWh: negative when the ramp-up side binds, positive when the ramp-down side does. 0.0 in period 0 (no row reaches into it) and for any generator whose ramp_up_mw and ramp_down_mw are both None (no row is built at all).
MarketMultiperiodResult
¶
Bases: BaseModel
Result of mambo_power.market.multiperiod.solve_multiperiod.
When status != "Optimal" periods is empty and every total is left at zero;
message carries the diagnostic, mirroring
MarketNodalResult's own convention for a non-converged solve.
status
class-attribute
instance-attribute
¶
HiGHS model status: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim.
n_periods
class-attribute
instance-attribute
¶
Number of periods cleared: len(Scenario.periods), or 1 for a period-less scenario.
periods
class-attribute
instance-attribute
¶
periods: list[MarketPeriodResult]
One entry per period, in scenario order; empty when not Optimal.
objective_cost
class-attribute
instance-attribute
¶
Total generation cost over the whole horizon, $. Storage is costless in the objective -- model.Storage carries no cost field, so a unit's only economic footprint is the round-trip loss it imposes on generation. 0.0 when not Optimal.
total_load_payment
class-attribute
instance-attribute
¶
Horizon sum of the per-period load payments, $.
total_generator_receipts
class-attribute
instance-attribute
¶
Horizon sum of the per-period generator receipts, $.
total_storage_charge_payment
class-attribute
instance-attribute
¶
Horizon sum of the per-period storage charge payments, $.
total_storage_discharge_revenue
class-attribute
instance-attribute
¶
Horizon sum of the per-period storage discharge revenues, $.
congestion_rent
class-attribute
instance-attribute
¶
Horizon sum of the per-period congestion rents, $.
MarketPeriodResult
¶
Bases: BaseModel
One period of a mambo_power.market.multiperiod.solve_multiperiod horizon.
Carries the same four things market.nodal reports for a single solve -- generator
dispatch, every load's served demand, per-bus LMPs, settlement -- plus the storage rows a
single-period clearing has no place for. The settlement identity (module docstring) holds on
this object, period by period; the horizon totals above it are a convenience sum, not the
level at which the identity is claimed.
period
class-attribute
instance-attribute
¶
Zero-based index of this period within the horizon.
total_load_payment
class-attribute
instance-attribute
¶
Sum over every load of LMP(bus_d)*p_d in this period, $/h.
total_generator_receipts
class-attribute
instance-attribute
¶
Sum over every generator of LMP(bus_g)*p_g in this period, $/h.
total_storage_charge_payment
class-attribute
instance-attribute
¶
Sum over every storage unit of LMP(bus_s)*charge_mw, $/h -- what storage pays the market for the energy it stores. 0.0 with no storage.
total_storage_discharge_revenue
class-attribute
instance-attribute
¶
Sum over every storage unit of LMP(bus_s)*discharge_mw, $/h -- what the market pays storage for the energy it returns. 0.0 with no storage.
congestion_rent
class-attribute
instance-attribute
¶
(load payment + storage charge payment) - (generator receipts + storage discharge revenue), $/h: the market operator's merchandising surplus for this period, computed directly from prices and quantities and never asserted equal to the identity's flow-dual side by construction. It is congestion rent proper -- exactly -sum_k(mu_k * f_k) -- on a network with no bus shunt conductance and no phase-shifting transformer; where either exists the surplus also carries that unsettled withdrawal, and the module docstring gives the full identity.
StorageDispatchResult
¶
Bases: BaseModel
One storage unit's charge, discharge and state of charge in one period.
charge_mw and discharge_mw are both nonnegative and are separate columns of the LP,
not two signs of one column: the charge and discharge efficiencies enter the SoC balance row
with different coefficients, an asymmetry a single signed column cannot express (see
mambo_power.opf.multiperiod). Simultaneous charge and discharge is therefore
representable and is bounded rather than banned, so both fields can be non-zero at once --
rare, but real on a network where forbidding it would make the problem infeasible.
charge_mw
class-attribute
instance-attribute
¶
Charging power in this period, MW; >= 0.
discharge_mw
class-attribute
instance-attribute
¶
Discharging power in this period, MW; >= 0.
soc_mwh
class-attribute
instance-attribute
¶
State of charge at the end of this period, MWh.
soc_dual
class-attribute
instance-attribute
¶
Dual of the unit's SoC balance row for this period, $/MWh, in the solver's own row-dual sign: the negative of the marginal value of stored energy, so it is negative wherever one more MWh in this unit is worth having (-LMP/efficiency_charge while the unit charges on an interior column, -efficiency_discharge*LMP while it discharges on one). The worth of an MWh is -soc_dual.
energy_bound_dual
class-attribute
instance-attribute
¶
Reduced cost of the unit's [0, energy_mwh] state-of-charge bound, non-zero at either end of it: a unit sitting empty binds that bound as much as a unit sitting full. 0 only when the state of charge is strictly between the two.
power_limit_dual
class-attribute
instance-attribute
¶
Dual of the shared charge + discharge <= p_max_mw row; 0 unless the unit's combined throughput is at its converter rating.
N1BranchFlag
¶
Bases: BaseModel
One (outage, monitored branch) pair the LODF screen flagged, with the confirming re-solve.
estimated_flow_mw is the LODF-screen's estimate (|base_flow + lodf[:, k] *
base_flow[k]|); confirmed_flow_mw is the actual flow from the real DC re-solve with
the outage branch taken out of service — the ground truth the screen is checked against.
branch_id
class-attribute
instance-attribute
¶
The monitored branch id whose flow was flagged.
rating_mva
class-attribute
instance-attribute
¶
The monitored branch's thermal rating, MVA.
estimated_flow_mw
class-attribute
instance-attribute
¶
LODF-screen estimated |flow| on this branch after the outage, MW.
confirmed_flow_mw
class-attribute
instance-attribute
¶
Actual |flow| on this branch from the confirming DC re-solve, MW.
confirmed_violating
class-attribute
instance-attribute
¶
Whether the re-solve confirms confirmed_flow_mw exceeds rating_mva.
N1OutageResult
¶
Bases: BaseModel
The LODF screen's verdict on one branch outage, plus its confirming re-solve.
Only outages the screen flagged (at least one monitored branch estimated over its rating)
are re-solved and appear here at all — an unflagged outage is asserted, not re-solved, to
be non-violating, which a brute-force agreement test in tests/ proves is safe.
outage_branch_id
class-attribute
instance-attribute
¶
The branch id taken out of service.
flagged_branches
class-attribute
instance-attribute
¶
flagged_branches: list[N1BranchFlag]
Other branches the LODF screen flagged for this outage, at least one.
confirmed_violating
class-attribute
instance-attribute
¶
Whether the DC re-solve confirms at least one flagged branch violates.
N1Result
¶
Bases: BaseModel
N-1 branch-contingency screen-then-confirm result (contingency.n1).
bridge_branch_ids names branches whose outage would disconnect the network
(numerics.bridges) — LODF is undefined for them, so they are skipped by the screen
entirely and never appear as an outages entry. Branch outages only this wave;
generator-outage contingencies are an explicit carry-over (wave spec Not Doing).
outages
class-attribute
instance-attribute
¶
outages: list[N1OutageResult]
One entry per outage the LODF screen flagged, in branch order.
bridge_branch_ids
class-attribute
instance-attribute
¶
Branch ids skipped because their outage disconnects the network.
BusLmpResult
¶
Bases: _Row
One bus's locational marginal price, decomposed (opf.dc_opf.lmp_decomposition).
GenDispatchResult
¶
Bases: _Row
One generator's DC-OPF dispatch and its bound's shadow price.
bound_dual
class-attribute
instance-attribute
¶
Reduced cost of the generator's [p_min, p_max] bound; 0 unless pinned.
OpfBranchFlowResult
¶
OpfDcResult
¶
Bases: BaseModel
Result of mambo_power.opf.solve_dc_opf.
When status != "Optimal" the dispatch/LMP/flow rows and objective_cost/
balance_dual are meaningless and left at their empty/zero defaults; message carries
the diagnostic (mirrors AcPowerFlowResult's message
pattern for a non-converged solve).
status
class-attribute
instance-attribute
¶
HiGHS model status: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim.
objective_cost
class-attribute
instance-attribute
¶
Total generation cost, $/h; 0.0 when not Optimal.
balance_dual
class-attribute
instance-attribute
¶
System-wide energy price, $/MWh; 0.0 when not Optimal.
ac_check
class-attribute
instance-attribute
¶
ac_check: FeasibilityReport | None
AC-feasibility check of the dispatch; None unless options.ac_check is true and the LP/QP solved to Optimal.
AcPowerFlowResult
¶
Bases: PowerFlowResultBase
Result of the AC Newton-Raphson solve, with the iteration diagnostics.
iterations
class-attribute
instance-attribute
¶
Newton iterations summed over all Q-limit rounds.
max_mismatch_mva
class-attribute
instance-attribute
¶
Final power-mismatch infinity norm, MVA.
q_limit_rounds
class-attribute
instance-attribute
¶
Outer Q-limit enforcement rounds run.
message
class-attribute
instance-attribute
¶
Diagnostic when converged is False (singular Jacobian, iteration or Q-limit-round exhaustion, naming the still-violating buses); None otherwise.
DcPowerFlowResult
¶
Bases: PowerFlowResultBase
Result of mambo_power.pf.solve_dc: lossless, reactive columns are 0, vm_pu 1.
PowerFlowArrays
dataclass
¶
PowerFlowArrays(
bus_ids: tuple[str, ...],
vm_pu: FloatArray,
va_deg: FloatArray,
p_bus_mw: FloatArray,
q_bus_mvar: FloatArray,
branch_ids: tuple[str, ...],
p_from_mw: FloatArray,
q_from_mvar: FloatArray,
p_to_mw: FloatArray,
q_to_mvar: FloatArray,
loading_pct: FloatArray,
gen_ids: tuple[str, ...],
p_gen_mw: FloatArray,
q_gen_mvar: FloatArray,
)
Positional view of a power-flow result; one array per column, rows in table order.
loading_pct holds nan where the branch is unrated (None in the table).
PowerFlowResultBase
¶
Bases: BaseModel
Fields common to DC and AC power-flow results.
converged
class-attribute
instance-attribute
¶
Whether the solve met its tolerance (always True for DC).
buses
class-attribute
instance-attribute
¶
buses: list[BusResult]
One row per solved bus, solver order.
branches
class-attribute
instance-attribute
¶
branches: list[BranchResult]
One row per solved branch, solver order.
generators
class-attribute
instance-attribute
¶
generators: list[GenResult]
One row per solved generator, solver order.
to_arrays
¶
to_arrays() -> PowerFlowArrays
The positional view: one numpy array per column, rows in table order.
Source code in src/mambo_power/results/power_flow.py
ResultProvenance
¶
Bases: BaseModel
Provenance stamp attached to every solver result.
started_at must be timezone-aware; it is normalised to UTC on validation so the JSON
form is always a Z-suffixed instant.
engine
class-attribute
instance-attribute
¶
Producing engine; always this package.
solver
class-attribute
instance-attribute
¶
Linear-algebra backend, e.g. scipy.sparse.linalg.splu.
started_at
class-attribute
instance-attribute
¶
Wall-clock start of the solve, UTC.
elapsed_s
class-attribute
instance-attribute
¶
Wall-clock duration of the solve, seconds.
options
class-attribute
instance-attribute
¶
The options the solver ran with, JSON-native values only.
BranchResult
¶
Bases: _Row
Flows on one branch, measured into the branch at each end.
p_from_mw is positive when power leaves from_bus into the branch; a lossless (DC)
solve has p_to_mw == -p_from_mw.
p_from_mw
class-attribute
instance-attribute
¶
Active power entering the branch at the from bus, MW.
q_from_mvar
class-attribute
instance-attribute
¶
Reactive power entering at the from bus, MVAr.
p_to_mw
class-attribute
instance-attribute
¶
Active power entering the branch at the to bus, MW.
q_to_mvar
class-attribute
instance-attribute
¶
Reactive power entering at the to bus, MVAr.
loading_pct
class-attribute
instance-attribute
¶
Apparent from-side flow over rating_mva in percent; None when unrated.
BusResult
¶
Bases: _Row
Solved state of one bus.
p_mw/q_mvar are the net injection into the network: generation minus load minus
shunt consumption (MATPOWER bus-equation sign; pandapower's res_bus is the negative).
vm_pu
class-attribute
instance-attribute
¶
Voltage magnitude, per unit (1.0 on every bus for DC).
q_mvar
class-attribute
instance-attribute
¶
Net reactive injection into the network, MVAr (0 for DC).
role_effective
class-attribute
instance-attribute
¶
role_effective: BusRole
Role the bus was solved with.
in_service
class-attribute
instance-attribute
¶
Whether the bus was part of the solve.
GenResult
¶
Bases: _Row
Dispatch of one generator after the solve.
p_mw
class-attribute
instance-attribute
¶
Active output, MW (slack-bus generators absorb the balance).
q_limited
class-attribute
instance-attribute
¶
q_limited: QLimitSide
Reactive limit the generator was pinned at.
GenRedispatchResult
¶
Bases: _Row
One generator's move from the zonal schedule to the redispatched one.
Two nonnegative fields rather than one signed one, following
StorageDispatchResult's split of charge from
discharge: a signed net number erases which direction was actually instructed, and
"instructed up" and "instructed down" are different products a real redispatch mechanism
settles differently. Here at most one of the two is nonzero for any generator, because
RedispatchSolution reports the netted canonical
representative rather than whichever split the solver happened to return.
LoadRedispatchResult
¶
Bases: _Row
One load's move from the zonal schedule to the redispatched one — the demand-side mirror of
GenRedispatchResult (LoadDispatchResult carries
a served quantity, not a delta).
Every load gets a row, bid or not, exactly as
LoadDispatchResult gives every load a row. A load with no
bid is not a decision variable in either stage, so it cannot be curtailed or restored and both
fields are 0.0 -- which is a fact worth reporting rather than a row worth omitting: it is
how a reader tells "this load was not moved" from "this load could not be moved".
delta_restore_mw
class-attribute
instance-attribute
¶
Served demand restored above the zonal schedule, MW; >= 0. d_final = d_zonal + delta_restore_mw - delta_curtail_mw exactly.
delta_curtail_mw
class-attribute
instance-attribute
¶
Served demand curtailed below the zonal schedule, MW; >= 0. 0.0 for a load with no bid, which is not a decision variable in either stage.
MarketZonalResult
¶
Bases: BaseModel
Result of mambo_power.market.zonal.solve_zonal (module docstring).
When status != "Optimal" every row list is empty and every figure is 0.0; message
carries the diagnostic, naming which of the three stages did not solve. The chain never
raises for an infeasible or unbounded stage -- this package's standing convention, shared with
MarketNodalResult and
MarketMultiperiodResult.
status
class-attribute
instance-attribute
¶
HiGHS model status: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim from whichever stage did not reach Optimal.
message
class-attribute
instance-attribute
¶
Diagnostic when status != Optimal, naming the stage (zonal clearing, redispatch, or the nodal reference) that produced it.
zones
class-attribute
instance-attribute
¶
zones: list[ZonePriceResult]
One clearing price per zone, from the zonal stage.
generators
class-attribute
instance-attribute
¶
generators: list[GenDispatchResult]
The zonal clearing's generator schedule -- what the market sold, before the network was consulted.
loads
class-attribute
instance-attribute
¶
loads: list[LoadDispatchResult]
The zonal clearing's served demand, every load in the network.
redispatch_generators
class-attribute
instance-attribute
¶
redispatch_generators: list[GenRedispatchResult]
Per-generator move from the zonal schedule to the final one.
redispatch_loads
class-attribute
instance-attribute
¶
redispatch_loads: list[LoadRedispatchResult]
Per-load curtailment/restoration between the zonal schedule and the final one.
generators_final
class-attribute
instance-attribute
¶
generators_final: list[GenDispatchResult]
The redispatched generator dispatch -- what the network actually delivers, and -- by the redispatch LP's own theorem -- the nodal optimum's dispatch. bound_dual is that generator's [p_min, p_max] reduced cost at the final point.
loads_final
class-attribute
instance-attribute
¶
loads_final: list[LoadDispatchResult]
The redispatched served demand, every load in the network.
branches
class-attribute
instance-attribute
¶
branches: list[OpfBranchFlowResult]
Per-branch flow and flow-limit shadow price at the final point -- the first market result type to carry them. Makes the settlement identity's flow-dual side, -sum_k(mu_k * f_k), computable from this object alone.
buses
class-attribute
instance-attribute
¶
buses: list[BusLmpResult]
Per-bus LMP at the final point, decomposed into energy and congestion. These are nodal prices; the zonal prices the market actually cleared at are in zones, and the two differing is the whole subject of this result.
redispatch_payment
class-attribute
instance-attribute
¶
Settlement figure, $/h: what the operator pays to move from the zonal schedule to the final one -- the extra generation cost, cost(final) - cost(zonal), plus compensation to curtailed load at its own bid value, value(d_zonal) - value(d_final) (a load restored above its zonal schedule contributes negatively, paying back at the same bid value). Equivalently and exactly, welfare(zonal) - welfare(final): the welfare the zonal clearing promised and the network could not deliver, which is why this figure is >= 0 whenever the zonal LP is a relaxation of the nodal one. Adding generation_cost_gap to this cancels the cost term and leaves the compensation alone: redispatch_payment + generation_cost_gap == value(d_zonal) - value(d_final). 0.0 when not Optimal.
welfare_gap
class-attribute
instance-attribute
¶
Exactness row, $/h: welfare(nodal) - welfare(final), both evaluated on the true cost and bid curves at their own dispatch. The redispatch LP carries the true curves in the redispatch objective, which makes the redispatched point the nodal optimum itself -- so this is 0 to solver tolerance, and a nonzero value means the chain is wrong, not that zonal clearing is expensive (that figure is redispatch_payment). 0.0 when not Optimal.
generation_cost_gap
class-attribute
instance-attribute
¶
Diagnostic, $/h: cost(zonal) - cost(nodal), true generation cost at each point, never a payment. Not sign-constrained -- the relaxation argument orders welfare, not generation cost, and a zonal clearing that serves less (or less valuable) demand can have strictly lower generation cost than the nodal optimum while being welfare-worse. Do not read it as 'how far zonal lands from nodal'; only welfare answers that. Because the redispatch LP lands on the nodal optimum, so that cost(final) == cost(nodal), this is exactly -(cost(final) - cost(zonal)) -- minus redispatch_payment's leading term -- so the two fields sum to the curtailment compensation value(d_zonal) - value(d_final) and are equal and opposite whenever no load carries a bid curve. 0.0 when not Optimal.
ZonePriceResult
¶
Bases: _Row
One zone's clearing price in the zonal stage.
The price is that zone's own balance-row dual in the zonal LP
(zone_price), which is this package's single source
of truth for the "zone price" concept. It is emphatically not an
average or a rollup of the bus LMPs in MarketZonalResult.buses: those are the final,
post-redispatch nodal prices, and the whole content of a nodal-versus-zonal comparison is that
the two disagree. Two zones joined by a corridor that does not bind necessarily price
identically; prices separate exactly where a corridor binds, and by that corridor's own
capacity shadow price.
price
class-attribute
instance-attribute
¶
Zonal clearing price, $/MWh: this zone's own balance-row dual in the zonal clearing LP. One price for the whole zone, by construction -- that is what makes it a zonal market.
feasibility_report
¶
feasibility_report(
ac: AcPowerFlowResult, net: Network
) -> FeasibilityReport
Build a FeasibilityReport from a solved AC state and the network it bounds.
ac carries the dispatched, solved state (BranchResult.loading_pct, BusResult.
vm_pu); net carries the declared bounds (Branch.rating_mva indirectly via ac's
already-computed loading_pct, Bus.v_min_pu/v_max_pu directly) — matched by id.
converged/message are passed through from ac unchanged, never recomputed.
A branch with no rating (loading_pct is None) never contributes a thermal violation —
"unmeasurable" is not "violating". A bus with neither bound set never contributes a voltage
violation. When both bounds are set and a bus is on the wrong side of both (a misconfigured
network with v_min_pu > v_max_pu), the low-side check wins; that misconfiguration is not
this function's job to guard against.
Source code in src/mambo_power/results/feasibility.py
ac_result_from_arrays
¶
ac_result_from_arrays(
arr: NetworkArrays,
*,
v: ComplexArray,
s_bus_pu: ComplexArray,
s_from_pu: ComplexArray,
s_to_pu: ComplexArray,
gen_p_pu: FloatArray,
gen_q_pu: FloatArray,
bus_type: IntArray,
q_limited: IntArray,
converged: bool,
iterations: int,
max_mismatch_pu: float,
q_limit_rounds: int,
provenance: ResultProvenance,
message: str | None = None,
) -> AcPowerFlowResult
Map an AC solution in arr order to an AcPowerFlowResult in MW/MVAr/degrees.
v and s_bus_pu are per bus (complex voltage; realised net injection
V·conj(Y V)), s_from_pu/s_to_pu per branch (complex power entering the branch at
each end), gen_p_pu/gen_q_pu per generator. bus_type is the effective role after
Q-limit pinning and q_limited the per-bus pin side (0 / +1 max / -1 min), which every
generator at the bus inherits (limits are enforced on the bus aggregate). Non-finite
voltages are rejected by the result model, so callers pass the last finite iterate.
message is message, threaded through
unchanged (set when converged is False, None otherwise).
Source code in src/mambo_power/results/from_arrays.py
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 | |
dc_result_from_arrays
¶
dc_result_from_arrays(
arr: NetworkArrays,
*,
theta_rad: FloatArray,
p_from_pu: FloatArray,
p_inj_pu: FloatArray,
gen_p_pu: FloatArray,
provenance: ResultProvenance,
bus_type: IntArray | None = None,
) -> DcPowerFlowResult
Map a DC solution in arr order to a DcPowerFlowResult in MW.
theta_rad/p_inj_pu are per bus, p_from_pu per branch, gen_p_pu per generator
(already carrying the slack balance). Reactive columns are 0, vm_pu is 1.0 (MATPOWER
rundcpf sets VM = 1 everywhere). role_effective comes from bus_type — the
effective roles when the caller passes them, the declared arr.bus_type otherwise.
Source code in src/mambo_power/results/from_arrays.py
Row models¶
mambo_power.results.tables
¶
Per-element result rows keyed by the network's stable ids.
Units are physical, matching mambo_power.model: MW, MVAr, per unit, degrees. Rows cover
the in-service subset the solver saw (the same elements
NetworkArrays holds, in the same order), so in_service is
True on every row a solver emits today; the field exists so a later wave can report
deactivated elements without a schema change. inf and nan are rejected — a quantity
that does not exist is None (loading_pct on an unrated branch), never a sentinel number.
BusRole
module-attribute
¶
The role a bus was solved with — the effective role, which may differ from the declared.
QLimitSide
module-attribute
¶
Which reactive limit a generator was pinned at by AC Q-limit enforcement; none for DC.
BusResult
¶
Bases: _Row
Solved state of one bus.
p_mw/q_mvar are the net injection into the network: generation minus load minus
shunt consumption (MATPOWER bus-equation sign; pandapower's res_bus is the negative).
vm_pu
class-attribute
instance-attribute
¶
Voltage magnitude, per unit (1.0 on every bus for DC).
q_mvar
class-attribute
instance-attribute
¶
Net reactive injection into the network, MVAr (0 for DC).
role_effective
class-attribute
instance-attribute
¶
role_effective: BusRole
Role the bus was solved with.
in_service
class-attribute
instance-attribute
¶
Whether the bus was part of the solve.
BranchResult
¶
Bases: _Row
Flows on one branch, measured into the branch at each end.
p_from_mw is positive when power leaves from_bus into the branch; a lossless (DC)
solve has p_to_mw == -p_from_mw.
p_from_mw
class-attribute
instance-attribute
¶
Active power entering the branch at the from bus, MW.
q_from_mvar
class-attribute
instance-attribute
¶
Reactive power entering at the from bus, MVAr.
p_to_mw
class-attribute
instance-attribute
¶
Active power entering the branch at the to bus, MW.
q_to_mvar
class-attribute
instance-attribute
¶
Reactive power entering at the to bus, MVAr.
loading_pct
class-attribute
instance-attribute
¶
Apparent from-side flow over rating_mva in percent; None when unrated.
GenResult
¶
Bases: _Row
Dispatch of one generator after the solve.
p_mw
class-attribute
instance-attribute
¶
Active output, MW (slack-bus generators absorb the balance).
q_limited
class-attribute
instance-attribute
¶
q_limited: QLimitSide
Reactive limit the generator was pinned at.
Provenance¶
mambo_power.results.provenance
¶
ResultProvenance: who produced a result, with what, when, and how long it took.
Every result model carries one of these: engine version, solver, timings and diagnostics, typed
per kind. version is stamped from mambo_power.__version__ by the solver entry points,
never typed by hand, so that a stored result can always be traced to the code that produced it.
ResultProvenance
¶
Bases: BaseModel
Provenance stamp attached to every solver result.
started_at must be timezone-aware; it is normalised to UTC on validation so the JSON
form is always a Z-suffixed instant.
engine
class-attribute
instance-attribute
¶
Producing engine; always this package.
solver
class-attribute
instance-attribute
¶
Linear-algebra backend, e.g. scipy.sparse.linalg.splu.
started_at
class-attribute
instance-attribute
¶
Wall-clock start of the solve, UTC.
elapsed_s
class-attribute
instance-attribute
¶
Wall-clock duration of the solve, seconds.
options
class-attribute
instance-attribute
¶
The options the solver ran with, JSON-native values only.
Power-flow results¶
mambo_power.results.power_flow
¶
Power-flow result models and their positional view.
DcPowerFlowResult and AcPowerFlowResult share the three id-keyed tables
(BusResult, BranchResult, GenResult) and a
ResultProvenance; the AC model adds the Newton
diagnostics. Both expose PowerFlowResultBase.to_arrays, a frozen bundle of numpy
arrays in the order the rows were emitted — the NetworkArrays
order when the result came from a solver — for numeric consumers that want positions rather
than ids. A result is a value: it is never stored on the Network.
PowerFlowArrays
dataclass
¶
PowerFlowArrays(
bus_ids: tuple[str, ...],
vm_pu: FloatArray,
va_deg: FloatArray,
p_bus_mw: FloatArray,
q_bus_mvar: FloatArray,
branch_ids: tuple[str, ...],
p_from_mw: FloatArray,
q_from_mvar: FloatArray,
p_to_mw: FloatArray,
q_to_mvar: FloatArray,
loading_pct: FloatArray,
gen_ids: tuple[str, ...],
p_gen_mw: FloatArray,
q_gen_mvar: FloatArray,
)
Positional view of a power-flow result; one array per column, rows in table order.
loading_pct holds nan where the branch is unrated (None in the table).
PowerFlowResultBase
¶
Bases: BaseModel
Fields common to DC and AC power-flow results.
converged
class-attribute
instance-attribute
¶
Whether the solve met its tolerance (always True for DC).
buses
class-attribute
instance-attribute
¶
buses: list[BusResult]
One row per solved bus, solver order.
branches
class-attribute
instance-attribute
¶
branches: list[BranchResult]
One row per solved branch, solver order.
generators
class-attribute
instance-attribute
¶
generators: list[GenResult]
One row per solved generator, solver order.
to_arrays
¶
to_arrays() -> PowerFlowArrays
The positional view: one numpy array per column, rows in table order.
Source code in src/mambo_power/results/power_flow.py
DcPowerFlowResult
¶
Bases: PowerFlowResultBase
Result of mambo_power.pf.solve_dc: lossless, reactive columns are 0, vm_pu 1.
AcPowerFlowResult
¶
Bases: PowerFlowResultBase
Result of the AC Newton-Raphson solve, with the iteration diagnostics.
iterations
class-attribute
instance-attribute
¶
Newton iterations summed over all Q-limit rounds.
max_mismatch_mva
class-attribute
instance-attribute
¶
Final power-mismatch infinity norm, MVA.
q_limit_rounds
class-attribute
instance-attribute
¶
Outer Q-limit enforcement rounds run.
message
class-attribute
instance-attribute
¶
Diagnostic when converged is False (singular Jacobian, iteration or Q-limit-round exhaustion, naming the still-violating buses); None otherwise.
Builders¶
mambo_power.results.from_arrays
¶
Builders that turn positional solver arrays back into id-keyed result tables.
This is the one place that walks from NetworkArrays positions
back to ids and multiplies per-unit quantities by base_mva on the way out. Solvers hand in
plain arrays; nothing here re-derives physics.
dc_result_from_arrays
¶
dc_result_from_arrays(
arr: NetworkArrays,
*,
theta_rad: FloatArray,
p_from_pu: FloatArray,
p_inj_pu: FloatArray,
gen_p_pu: FloatArray,
provenance: ResultProvenance,
bus_type: IntArray | None = None,
) -> DcPowerFlowResult
Map a DC solution in arr order to a DcPowerFlowResult in MW.
theta_rad/p_inj_pu are per bus, p_from_pu per branch, gen_p_pu per generator
(already carrying the slack balance). Reactive columns are 0, vm_pu is 1.0 (MATPOWER
rundcpf sets VM = 1 everywhere). role_effective comes from bus_type — the
effective roles when the caller passes them, the declared arr.bus_type otherwise.
Source code in src/mambo_power/results/from_arrays.py
ac_result_from_arrays
¶
ac_result_from_arrays(
arr: NetworkArrays,
*,
v: ComplexArray,
s_bus_pu: ComplexArray,
s_from_pu: ComplexArray,
s_to_pu: ComplexArray,
gen_p_pu: FloatArray,
gen_q_pu: FloatArray,
bus_type: IntArray,
q_limited: IntArray,
converged: bool,
iterations: int,
max_mismatch_pu: float,
q_limit_rounds: int,
provenance: ResultProvenance,
message: str | None = None,
) -> AcPowerFlowResult
Map an AC solution in arr order to an AcPowerFlowResult in MW/MVAr/degrees.
v and s_bus_pu are per bus (complex voltage; realised net injection
V·conj(Y V)), s_from_pu/s_to_pu per branch (complex power entering the branch at
each end), gen_p_pu/gen_q_pu per generator. bus_type is the effective role after
Q-limit pinning and q_limited the per-bus pin side (0 / +1 max / -1 min), which every
generator at the bus inherits (limits are enforced on the bus aggregate). Non-finite
voltages are rejected by the result model, so callers pass the last finite iterate.
message is message, threaded through
unchanged (set when converged is False, None otherwise).
Source code in src/mambo_power/results/from_arrays.py
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 | |
Multiperiod market results¶
Per-period dispatch, LMPs and settlement, per-storage charge/discharge/SoC, and horizon totals. Its module docstring states the settlement identity in its general form, including the phase-shift and shunt correction terms.
mambo_power.results.multiperiod
¶
market.multiperiod clearing result: per-period dispatch, LMPs and settlement, per-storage
charge/discharge/SoC, and horizon totals.
The multiperiod sibling of mambo_power.results.market, and shaped the same way: id-keyed
rows plus ResultProvenance, never attached to a
Network. LoadDispatchResult and
BusLmpResult are reused verbatim (ADR-006's reuse
discipline); only the per-period container, the storage row and the generator row's one extra
field are new.
The settlement identity this result reports, and where storage sits in it. market.nodal
proved, per solve, that total load payment minus total generator receipts equals the congestion
rent -sum_k(mu_k * flow_k). A storage unit injects and withdraws at a bus, so it is a
third settlement participant: it pays LMP * charge_mw and is paid LMP * discharge_mw, and
the identity does not close if a dispatched unit is left unsettled -- it is then wrong by
exactly the unit's net revenue, which is the whole of its arbitrage profit. So the identity this
module claims, and which tests/unit/test_market_multiperiod.py proves per period with the
right-hand side computed by a separate code path, is::
load_payment + storage_charge_payment - generator_receipts - storage_discharge_revenue
== -sum_k(mu_k * f_k) + sum_k(mu_k * pf_shift_k) - sum_n(LMP_n * g_shunt_n)
The two trailing terms are the general form's corrections for phase-shifting transformers and
for bus shunt conductance -- both fixed, unsettled withdrawals from the network itself rather
than from a market participant. They are exactly zero on every MATPOWER fixture this repository
ships except case300, whose g_shunt is non-zero; market.nodal's M4-era statement of
the identity omitted them and was correct only because its own fixtures had none.
Every quantity is per period: nothing here is a horizon average. $/h figures are that
period's rate, and the horizon totals on MarketMultiperiodResult are their plain sum,
which is an energy-weighted total only because every period is one hour long (the wave carries
no period-duration field).
GenPeriodDispatchResult
¶
Bases: GenDispatchResult
One generator's dispatch in one period, plus the ramp row that reaches into that period.
Extends GenDispatchResult rather than replacing it: id,
bus, p_mw and bound_dual mean exactly what they mean in a single-period DC-OPF
result, so a reader who knows one knows the other.
ramp_dual
class-attribute
instance-attribute
¶
Dual of the two-sided ramp row coupling the previous period to this one, $/MWh: negative when the ramp-up side binds, positive when the ramp-down side does. 0.0 in period 0 (no row reaches into it) and for any generator whose ramp_up_mw and ramp_down_mw are both None (no row is built at all).
StorageDispatchResult
¶
Bases: BaseModel
One storage unit's charge, discharge and state of charge in one period.
charge_mw and discharge_mw are both nonnegative and are separate columns of the LP,
not two signs of one column: the charge and discharge efficiencies enter the SoC balance row
with different coefficients, an asymmetry a single signed column cannot express (see
mambo_power.opf.multiperiod). Simultaneous charge and discharge is therefore
representable and is bounded rather than banned, so both fields can be non-zero at once --
rare, but real on a network where forbidding it would make the problem infeasible.
charge_mw
class-attribute
instance-attribute
¶
Charging power in this period, MW; >= 0.
discharge_mw
class-attribute
instance-attribute
¶
Discharging power in this period, MW; >= 0.
soc_mwh
class-attribute
instance-attribute
¶
State of charge at the end of this period, MWh.
soc_dual
class-attribute
instance-attribute
¶
Dual of the unit's SoC balance row for this period, $/MWh, in the solver's own row-dual sign: the negative of the marginal value of stored energy, so it is negative wherever one more MWh in this unit is worth having (-LMP/efficiency_charge while the unit charges on an interior column, -efficiency_discharge*LMP while it discharges on one). The worth of an MWh is -soc_dual.
energy_bound_dual
class-attribute
instance-attribute
¶
Reduced cost of the unit's [0, energy_mwh] state-of-charge bound, non-zero at either end of it: a unit sitting empty binds that bound as much as a unit sitting full. 0 only when the state of charge is strictly between the two.
power_limit_dual
class-attribute
instance-attribute
¶
Dual of the shared charge + discharge <= p_max_mw row; 0 unless the unit's combined throughput is at its converter rating.
MarketPeriodResult
¶
Bases: BaseModel
One period of a mambo_power.market.multiperiod.solve_multiperiod horizon.
Carries the same four things market.nodal reports for a single solve -- generator
dispatch, every load's served demand, per-bus LMPs, settlement -- plus the storage rows a
single-period clearing has no place for. The settlement identity (module docstring) holds on
this object, period by period; the horizon totals above it are a convenience sum, not the
level at which the identity is claimed.
period
class-attribute
instance-attribute
¶
Zero-based index of this period within the horizon.
total_load_payment
class-attribute
instance-attribute
¶
Sum over every load of LMP(bus_d)*p_d in this period, $/h.
total_generator_receipts
class-attribute
instance-attribute
¶
Sum over every generator of LMP(bus_g)*p_g in this period, $/h.
total_storage_charge_payment
class-attribute
instance-attribute
¶
Sum over every storage unit of LMP(bus_s)*charge_mw, $/h -- what storage pays the market for the energy it stores. 0.0 with no storage.
total_storage_discharge_revenue
class-attribute
instance-attribute
¶
Sum over every storage unit of LMP(bus_s)*discharge_mw, $/h -- what the market pays storage for the energy it returns. 0.0 with no storage.
congestion_rent
class-attribute
instance-attribute
¶
(load payment + storage charge payment) - (generator receipts + storage discharge revenue), $/h: the market operator's merchandising surplus for this period, computed directly from prices and quantities and never asserted equal to the identity's flow-dual side by construction. It is congestion rent proper -- exactly -sum_k(mu_k * f_k) -- on a network with no bus shunt conductance and no phase-shifting transformer; where either exists the surplus also carries that unsettled withdrawal, and the module docstring gives the full identity.
MarketMultiperiodResult
¶
Bases: BaseModel
Result of mambo_power.market.multiperiod.solve_multiperiod.
When status != "Optimal" periods is empty and every total is left at zero;
message carries the diagnostic, mirroring
MarketNodalResult's own convention for a non-converged solve.
status
class-attribute
instance-attribute
¶
HiGHS model status: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim.
n_periods
class-attribute
instance-attribute
¶
Number of periods cleared: len(Scenario.periods), or 1 for a period-less scenario.
periods
class-attribute
instance-attribute
¶
periods: list[MarketPeriodResult]
One entry per period, in scenario order; empty when not Optimal.
objective_cost
class-attribute
instance-attribute
¶
Total generation cost over the whole horizon, $. Storage is costless in the objective -- model.Storage carries no cost field, so a unit's only economic footprint is the round-trip loss it imposes on generation. 0.0 when not Optimal.
total_load_payment
class-attribute
instance-attribute
¶
Horizon sum of the per-period load payments, $.
total_generator_receipts
class-attribute
instance-attribute
¶
Horizon sum of the per-period generator receipts, $.
total_storage_charge_payment
class-attribute
instance-attribute
¶
Horizon sum of the per-period storage charge payments, $.
total_storage_discharge_revenue
class-attribute
instance-attribute
¶
Horizon sum of the per-period storage discharge revenues, $.
congestion_rent
class-attribute
instance-attribute
¶
Horizon sum of the per-period congestion rents, $.
Zonal market results¶
Zone prices, both dispatch layers, the redispatch deltas on both sides, per-branch flows with their shadow prices, and the three separated gap figures. Its module docstring explains why the result carries two dispatch layers rather than one and what each of the three figures is for.
mambo_power.results.zonal
¶
solve_zonal's result: a zonal clearing, the redispatch that
makes it network-feasible, and what the pair costs against the nodal optimum.
The third market result type, and shaped like the other two: id-keyed rows plus
ResultProvenance, never attached to a
Network, every row model extra="forbid"/frozen=True/
allow_inf_nan=False. GenDispatchResult,
LoadDispatchResult,
BusLmpResult and
OpfBranchFlowResult are reused verbatim (ADR-006's reuse
discipline); only the zone-price row, the two delta rows and the three gap figures are new.
Two layers, both reported. market.zonal runs three solves — a zonal clearing, a min-cost
redispatch from that clearing's point, and market.nodal as the reference — and its content is
their relationship, so a result that reported only the final point would have thrown away the
comparison it exists to make. Hence the two dispatch layers: generators
/ loads carry the zonal clearing's own schedule (what the market
sold), generators_final / loads_final carry
the redispatched one (what the network actually delivers), and
redispatch_generators /
redispatch_loads carry the move between them, per participant and per
direction.
The first market result carrying branch rows. MarketNodalResult and
MarketPeriodResult carry prices and quantities but no per-branch surface, so the settlement
identity's flow-dual side (-sum_k mu_k * f_k) could not be recomputed from either object.
MarketZonalResult.branches closes that: with
p_from_mw and flow_limit_dual per branch alongside the per-bus LMPs and the final
dispatch, both sides of the identity are computable from this object alone, with no second
solve. tests/unit/test_market_zonal.py proves exactly that, in a test that imports nothing
from mambo_power.numerics or mambo_power.opf.
Three separated figures, and why separating them is the point. Welfare and generation cost order differently between a zonal clearing and the nodal optimum — a zonal clearing that serves less, or less valuable, demand can be strictly cheaper to generate while being welfare-worse — so a result type conflating them would let a reader draw the wrong conclusion from either. Hence three fields, not one:
redispatch_payment— a settlement figure: what the operator pays out to move from the sold schedule to the deliverable one.welfare_gap— the exactness row:0by theorem, and therefore a check on the chain rather than a measurement of it.generation_cost_gap— a diagnostic, explicitly not sign-constrained.
Each field's own description says which of the three it is.
How the three relate, stated so a reader does not have to derive it. They are two independent
quantities and a combination, not three independent ones. Writing A = cost_final − cost_zonal
and B = value_zonal − value_final, the fields are A + B, 0 and −A, so
``redispatch_payment + generation_cost_gap == value_zonal − value_final``
exactly — the curtailment compensation, the bid value elastic demand was scheduled and did not
receive. On a network with no bid curves it is identically zero and generation_cost_gap is
exactly −redispatch_payment; on one with elastic demand it is the whole of what the third field
adds (measured: 0.94 of a 14.51 $/h payment on rated case30). The identity is asserted in
tests/unit/test_market_zonal.py on both a bid fixture and its fixed-load pair.
ZonePriceResult
¶
Bases: _Row
One zone's clearing price in the zonal stage.
The price is that zone's own balance-row dual in the zonal LP
(zone_price), which is this package's single source
of truth for the "zone price" concept. It is emphatically not an
average or a rollup of the bus LMPs in MarketZonalResult.buses: those are the final,
post-redispatch nodal prices, and the whole content of a nodal-versus-zonal comparison is that
the two disagree. Two zones joined by a corridor that does not bind necessarily price
identically; prices separate exactly where a corridor binds, and by that corridor's own
capacity shadow price.
price
class-attribute
instance-attribute
¶
Zonal clearing price, $/MWh: this zone's own balance-row dual in the zonal clearing LP. One price for the whole zone, by construction -- that is what makes it a zonal market.
GenRedispatchResult
¶
Bases: _Row
One generator's move from the zonal schedule to the redispatched one.
Two nonnegative fields rather than one signed one, following
StorageDispatchResult's split of charge from
discharge: a signed net number erases which direction was actually instructed, and
"instructed up" and "instructed down" are different products a real redispatch mechanism
settles differently. Here at most one of the two is nonzero for any generator, because
RedispatchSolution reports the netted canonical
representative rather than whichever split the solver happened to return.
LoadRedispatchResult
¶
Bases: _Row
One load's move from the zonal schedule to the redispatched one — the demand-side mirror of
GenRedispatchResult (LoadDispatchResult carries
a served quantity, not a delta).
Every load gets a row, bid or not, exactly as
LoadDispatchResult gives every load a row. A load with no
bid is not a decision variable in either stage, so it cannot be curtailed or restored and both
fields are 0.0 -- which is a fact worth reporting rather than a row worth omitting: it is
how a reader tells "this load was not moved" from "this load could not be moved".
delta_restore_mw
class-attribute
instance-attribute
¶
Served demand restored above the zonal schedule, MW; >= 0. d_final = d_zonal + delta_restore_mw - delta_curtail_mw exactly.
delta_curtail_mw
class-attribute
instance-attribute
¶
Served demand curtailed below the zonal schedule, MW; >= 0. 0.0 for a load with no bid, which is not a decision variable in either stage.
MarketZonalResult
¶
Bases: BaseModel
Result of mambo_power.market.zonal.solve_zonal (module docstring).
When status != "Optimal" every row list is empty and every figure is 0.0; message
carries the diagnostic, naming which of the three stages did not solve. The chain never
raises for an infeasible or unbounded stage -- this package's standing convention, shared with
MarketNodalResult and
MarketMultiperiodResult.
status
class-attribute
instance-attribute
¶
HiGHS model status: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim from whichever stage did not reach Optimal.
message
class-attribute
instance-attribute
¶
Diagnostic when status != Optimal, naming the stage (zonal clearing, redispatch, or the nodal reference) that produced it.
zones
class-attribute
instance-attribute
¶
zones: list[ZonePriceResult]
One clearing price per zone, from the zonal stage.
generators
class-attribute
instance-attribute
¶
generators: list[GenDispatchResult]
The zonal clearing's generator schedule -- what the market sold, before the network was consulted.
loads
class-attribute
instance-attribute
¶
loads: list[LoadDispatchResult]
The zonal clearing's served demand, every load in the network.
redispatch_generators
class-attribute
instance-attribute
¶
redispatch_generators: list[GenRedispatchResult]
Per-generator move from the zonal schedule to the final one.
redispatch_loads
class-attribute
instance-attribute
¶
redispatch_loads: list[LoadRedispatchResult]
Per-load curtailment/restoration between the zonal schedule and the final one.
generators_final
class-attribute
instance-attribute
¶
generators_final: list[GenDispatchResult]
The redispatched generator dispatch -- what the network actually delivers, and -- by the redispatch LP's own theorem -- the nodal optimum's dispatch. bound_dual is that generator's [p_min, p_max] reduced cost at the final point.
loads_final
class-attribute
instance-attribute
¶
loads_final: list[LoadDispatchResult]
The redispatched served demand, every load in the network.
branches
class-attribute
instance-attribute
¶
branches: list[OpfBranchFlowResult]
Per-branch flow and flow-limit shadow price at the final point -- the first market result type to carry them. Makes the settlement identity's flow-dual side, -sum_k(mu_k * f_k), computable from this object alone.
buses
class-attribute
instance-attribute
¶
buses: list[BusLmpResult]
Per-bus LMP at the final point, decomposed into energy and congestion. These are nodal prices; the zonal prices the market actually cleared at are in zones, and the two differing is the whole subject of this result.
redispatch_payment
class-attribute
instance-attribute
¶
Settlement figure, $/h: what the operator pays to move from the zonal schedule to the final one -- the extra generation cost, cost(final) - cost(zonal), plus compensation to curtailed load at its own bid value, value(d_zonal) - value(d_final) (a load restored above its zonal schedule contributes negatively, paying back at the same bid value). Equivalently and exactly, welfare(zonal) - welfare(final): the welfare the zonal clearing promised and the network could not deliver, which is why this figure is >= 0 whenever the zonal LP is a relaxation of the nodal one. Adding generation_cost_gap to this cancels the cost term and leaves the compensation alone: redispatch_payment + generation_cost_gap == value(d_zonal) - value(d_final). 0.0 when not Optimal.
welfare_gap
class-attribute
instance-attribute
¶
Exactness row, $/h: welfare(nodal) - welfare(final), both evaluated on the true cost and bid curves at their own dispatch. The redispatch LP carries the true curves in the redispatch objective, which makes the redispatched point the nodal optimum itself -- so this is 0 to solver tolerance, and a nonzero value means the chain is wrong, not that zonal clearing is expensive (that figure is redispatch_payment). 0.0 when not Optimal.
generation_cost_gap
class-attribute
instance-attribute
¶
Diagnostic, $/h: cost(zonal) - cost(nodal), true generation cost at each point, never a payment. Not sign-constrained -- the relaxation argument orders welfare, not generation cost, and a zonal clearing that serves less (or less valuable) demand can have strictly lower generation cost than the nodal optimum while being welfare-worse. Do not read it as 'how far zonal lands from nodal'; only welfare answers that. Because the redispatch LP lands on the nodal optimum, so that cost(final) == cost(nodal), this is exactly -(cost(final) - cost(zonal)) -- minus redispatch_payment's leading term -- so the two fields sum to the curtailment compensation value(d_zonal) - value(d_final) and are equal and opposite whenever no load carries a bid curve. 0.0 when not Optimal.
Agent market results¶
The final round's clearing — mirroring MarketNodalResult field for field and row type for row
type, because they are the same clearing quantities computed the same way — plus one offers row
per agent and the three fields that describe how the loop ended.
status and converged are never the same thing and never fold into each other: status is
HiGHS's verdict on the final round's clearing, converged is whether the best-response iteration
settled, and a run can be Optimal every round without converging. termination_reason is
required and enumerated rather than inferred from the flag, because reporting a genuine cycle as
an iteration-cap hit would be a confident wrong diagnosis. AgentOfferResult.markup is not
independent content: it is exactly offer(cleared_mw) - true_cost(cleared_mw), and the wave's
tests assert it as that identity.
mambo_power.results.agents
¶
market.agents result: the final round's clearing, plus what each agent offered to get it
and how the loop that produced it ended (wave M7 W4).
Two different things called "did it work", and they are never the same field. status is
the LP's -- HiGHS's own model status for the final round's clearing, exactly as
MarketNodalResult reports it. MarketAgentsResult.converged
is the loop's -- whether the best-response iteration settled. A run can be Optimal every
round and still not converge (the agents keep re-bidding), and that combination is the one the
wave most needs to report honestly rather than round off to "it worked". Neither field is derived
from the other, and no docstring or message here uses one word for both.
Why the loop's end needs three words, not a flag (spec A7). converged alone cannot
distinguish the two shapes of non-convergence, and reporting a genuine cycle as an iteration-cap
hit would be a confident wrong diagnosis. termination_reason is therefore required and
enumerated -- converged | iteration_cap | cycle -- with iterations readable
beside it.
The clearing fields mirror MarketNodalResult field for field and
row type for row type (generators, loads, buses, branches, and the three settlement figures),
because they are the same clearing quantities computed the same way -- one clearing, the final
round's, settled at the final round's prices. This result is a sibling of that one, not a
subclass: a market.agents result is not a market.nodal result, and nothing should be able
to pass one off as the other.
TerminationReason
module-attribute
¶
How mambo_power.market.agents.solve_agents' loop ended (spec A7). converged: the
offer vector repeated and the repetition's amplitude is within offer_tol. cycle: it
repeated with an amplitude wider than that -- a genuine cycle, which is not the iteration cap.
iteration_cap: max_iterations update rounds passed without any repetition at all.
AgentOfferResult
¶
Bases: BaseModel
One agent's final-round offer, beside the true cost it was allowed to depart from.
Both curves are carried whole, as GeneratorCost objects, because
the whole point of the overlay is that they are two separate objects: true_cost is the
generator's own Generator.cost, untouched by the run (AC-2), and offer is what the
strategy handed the clearing. "Markup" is the difference, which only means anything because
neither one overwrote the other.
strategy
class-attribute
instance-attribute
¶
Which bidding rule produced this offer: the StrategyConfig.kind ("price_taker", "markup") when solve_agents built the strategy from MarketAgentsOptions.strategies, or the class name of the object an in-process caller passed to solve_agents' own strategies argument.
offer
class-attribute
instance-attribute
¶
The cost curve this agent offered in the final round -- what the clearing actually minimised against, never written back to the network.
true_cost
class-attribute
instance-attribute
¶
The generator's own Generator.cost, unchanged by the run.
cleared_mw
class-attribute
instance-attribute
¶
This agent's dispatch in the final round's clearing, MW; the same figure its GenDispatchResult row carries, repeated here because the markup identity below is stated in terms of it.
markup
class-attribute
instance-attribute
¶
offer(cleared_mw) - true_cost(cleared_mw), $/h: what the agent's departure from its own cost is worth at the quantity it actually cleared. Not independent content -- it is exactly that identity in the other three fields (spec A6), and tests/unit/test_market_agents.py asserts it as one.
MarketAgentsResult
¶
Bases: BaseModel
Result of mambo_power.market.agents.solve_agents.
When status != "Optimal" the clearing fields are left at their empty/zero defaults and
message carries the diagnostic, mirroring
MarketNodalResult's own convention; iterations still
reports the round the clearing failed in, since that is a fact about the loop rather than
about the clearing.
status
class-attribute
instance-attribute
¶
HiGHS model status of the final round's clearing: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim. This is the LP's verdict and says nothing about whether the loop converged -- see converged.
branches
class-attribute
instance-attribute
¶
branches: list[OpfBranchFlowResult]
Per-branch flow and flow-limit shadow price at the final round's dispatch -- the same field name and row type as MarketNodalResult.branches and MarketZonalResult.branches.
offers
class-attribute
instance-attribute
¶
offers: list[AgentOfferResult]
One row per agent -- a generator that MarketAgentsOptions.strategies (or solve_agents' strategies argument) named -- in NetworkArrays generator order. A generator with no strategy is not an agent: it clears at its own true cost and appears under generators only.
iterations
class-attribute
instance-attribute
¶
The final round's index: the number of best-response update rounds the loop ran after round 0. Round 0 is the initial offer and responds to nothing, so it is not an iteration; the loop therefore cleared the market iterations + 1 times. A fixed point is confirmed only after two identical updates (the loop's state is the pair of consecutive offer vectors), so iterations is at least 2 on any converged run -- an all-price-taker market, in which nothing moves, still reports 2.
converged
class-attribute
instance-attribute
¶
Whether the loop settled -- the offer vector repeated with an amplitude within offer_tol. Never a statement about the LP: see status. True exactly when termination_reason == "converged".
termination_reason
class-attribute
instance-attribute
¶
termination_reason: TerminationReason | None
How the loop ended (spec A7): converged | iteration_cap | cycle. None exactly when status != Optimal -- a clearing that failed produced no loop outcome to report, and inventing a fourth value here would fold the LP's verdict into the loop's, which is what this result exists to keep apart.
total_load_payment
class-attribute
instance-attribute
¶
Sum over every load of LMP(bus_d)*p_d in the final round's clearing, $/h; 0.0 when not Optimal.
total_generator_receipts
class-attribute
instance-attribute
¶
Sum over every generator of LMP(bus_g)*p_g in the final round's clearing, $/h -- paid at the final round's prices, on the final round's offers; 0.0 when not Optimal.
congestion_rent
class-attribute
instance-attribute
¶
total_load_payment - total_generator_receipts, $/h; 0.0 when not Optimal.