Skip to content

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

TerminationReason = Literal[
    "converged", "iteration_cap", "cycle"
]

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

BusRole = Literal['slack', 'pv', 'pq']

The role a bus was solved with — the effective role, which may differ from the declared.

QLimitSide module-attribute

QLimitSide = Literal['none', 'min', 'max']

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.

id class-attribute instance-attribute

id: str

Generator id from the network.

strategy class-attribute instance-attribute

strategy: str

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

offer: GeneratorCost

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

true_cost: GeneratorCost

The generator's own Generator.cost, unchanged by the run.

cleared_mw class-attribute instance-attribute

cleared_mw: float

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

markup: float

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

status: str

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.

message class-attribute instance-attribute

message: str | None

Diagnostic when status != Optimal.

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

iterations: int = 0

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

converged: bool = False

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

total_load_payment: float = 0.0

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

total_generator_receipts: float = 0.0

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

congestion_rent: float = 0.0

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.

converged class-attribute instance-attribute

converged: bool

Whether the AC re-solve converged (mirrors AcPowerFlowResult.converged).

message class-attribute instance-attribute

message: str | None

Diagnostic when converged is False.

ThermalViolation

Bases: _Row

A branch loaded beyond its thermal rating.

branch_id class-attribute instance-attribute

branch_id: str

Branch id from the network.

loading_pct class-attribute instance-attribute

loading_pct: float

Measured apparent-flow loading, percent of rating.

limit_pct class-attribute instance-attribute

limit_pct: float

The loading limit exceeded, percent.

VoltageViolation

Bases: _Row

A bus outside its declared voltage-magnitude limits.

bus_id class-attribute instance-attribute

bus_id: str

Bus id from the network.

vm_pu class-attribute instance-attribute

vm_pu: float

Measured voltage magnitude, per unit.

limit_pu class-attribute instance-attribute

limit_pu: float

The voltage limit exceeded (v_min_pu or v_max_pu), pu.

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.

id class-attribute instance-attribute

id: str

Load id from the network.

bus class-attribute instance-attribute

bus: str

Bus id the load is connected to.

p_mw class-attribute instance-attribute

p_mw: float

Served demand, MW.

bound_dual class-attribute instance-attribute

bound_dual: float

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

status: str

HiGHS model status: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim.

message class-attribute instance-attribute

message: str | None

Diagnostic when status != Optimal.

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

total_load_payment: float = 0.0

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

total_generator_receipts: float = 0.0

Sum over every generator of LMP(bus_g)*p_g, $/h; 0.0 when not Optimal.

congestion_rent class-attribute instance-attribute

congestion_rent: float = 0.0

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

ramp_dual: float = 0.0

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

status: str

HiGHS model status: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim.

message class-attribute instance-attribute

message: str | None

Diagnostic when status != Optimal.

n_periods class-attribute instance-attribute

n_periods: int

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

objective_cost: float = 0.0

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

total_load_payment: float = 0.0

Horizon sum of the per-period load payments, $.

total_generator_receipts class-attribute instance-attribute

total_generator_receipts: float = 0.0

Horizon sum of the per-period generator receipts, $.

total_storage_charge_payment class-attribute instance-attribute

total_storage_charge_payment: float = 0.0

Horizon sum of the per-period storage charge payments, $.

total_storage_discharge_revenue class-attribute instance-attribute

total_storage_discharge_revenue: float = 0.0

Horizon sum of the per-period storage discharge revenues, $.

congestion_rent class-attribute instance-attribute

congestion_rent: float = 0.0

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

period: int

Zero-based index of this period within the horizon.

total_load_payment class-attribute instance-attribute

total_load_payment: float = 0.0

Sum over every load of LMP(bus_d)*p_d in this period, $/h.

total_generator_receipts class-attribute instance-attribute

total_generator_receipts: float = 0.0

Sum over every generator of LMP(bus_g)*p_g in this period, $/h.

total_storage_charge_payment class-attribute instance-attribute

total_storage_charge_payment: float = 0.0

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

total_storage_discharge_revenue: float = 0.0

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

congestion_rent: float = 0.0

(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.

id class-attribute instance-attribute

id: str

Storage id from the network.

bus class-attribute instance-attribute

bus: str

Bus id the unit is connected to.

charge_mw class-attribute instance-attribute

charge_mw: float

Charging power in this period, MW; >= 0.

discharge_mw class-attribute instance-attribute

discharge_mw: float

Discharging power in this period, MW; >= 0.

soc_mwh class-attribute instance-attribute

soc_mwh: float

State of charge at the end of this period, MWh.

soc_dual class-attribute instance-attribute

soc_dual: float

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

energy_bound_dual: float

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

power_limit_dual: float

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

branch_id: str

The monitored branch id whose flow was flagged.

rating_mva class-attribute instance-attribute

rating_mva: float

The monitored branch's thermal rating, MVA.

estimated_flow_mw class-attribute instance-attribute

estimated_flow_mw: float

LODF-screen estimated |flow| on this branch after the outage, MW.

confirmed_flow_mw class-attribute instance-attribute

confirmed_flow_mw: float

Actual |flow| on this branch from the confirming DC re-solve, MW.

confirmed_violating class-attribute instance-attribute

confirmed_violating: bool

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

outage_branch_id: str

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

confirmed_violating: bool

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

bridge_branch_ids: list[str]

Branch ids skipped because their outage disconnects the network.

BusLmpResult

Bases: _Row

One bus's locational marginal price, decomposed (opf.dc_opf.lmp_decomposition).

id class-attribute instance-attribute

id: str

Bus id from the network.

lmp class-attribute instance-attribute

lmp: float

Locational marginal price, $/MWh: energy + congestion.

energy class-attribute instance-attribute

energy: float

Energy component: the system-wide balance dual.

congestion class-attribute instance-attribute

congestion: float

Congestion component: Σ(flow-limit duals × PTDF).

GenDispatchResult

Bases: _Row

One generator's DC-OPF dispatch and its bound's shadow price.

id class-attribute instance-attribute

id: str

Generator id from the network.

bus class-attribute instance-attribute

bus: str

Bus id the generator is connected to.

p_mw class-attribute instance-attribute

p_mw: float

Optimal dispatch, MW.

bound_dual class-attribute instance-attribute

bound_dual: float

Reduced cost of the generator's [p_min, p_max] bound; 0 unless pinned.

OpfBranchFlowResult

Bases: _Row

One branch's DC-OPF flow and its flow-limit row's shadow price.

id class-attribute instance-attribute

id: str

Branch id from the network.

from_bus class-attribute instance-attribute

from_bus: str

Bus id of the from (tap) side.

to_bus class-attribute instance-attribute

to_bus: str

Bus id of the to side.

p_from_mw class-attribute instance-attribute

p_from_mw: float

From-side active flow at the optimal dispatch, MW.

flow_limit_dual class-attribute instance-attribute

flow_limit_dual: float

Shadow price of the branch's [-rating, rating] row; 0 unless binding.

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

status: str

HiGHS model status: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim.

message class-attribute instance-attribute

message: str | None

Diagnostic when status != Optimal.

objective_cost class-attribute instance-attribute

objective_cost: float = 0.0

Total generation cost, $/h; 0.0 when not Optimal.

balance_dual class-attribute instance-attribute

balance_dual: float = 0.0

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

iterations: int

Newton iterations summed over all Q-limit rounds.

max_mismatch_mva class-attribute instance-attribute

max_mismatch_mva: float

Final power-mismatch infinity norm, MVA.

q_limit_rounds class-attribute instance-attribute

q_limit_rounds: int

Outer Q-limit enforcement rounds run.

message class-attribute instance-attribute

message: str | None

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

converged: bool

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
def to_arrays(self) -> PowerFlowArrays:
    """The positional view: one numpy array per column, rows in table order."""

    def column(values: list[float]) -> FloatArray:
        return np.asarray(values, dtype=np.float64)

    return PowerFlowArrays(
        bus_ids=tuple(b.id for b in self.buses),
        vm_pu=column([b.vm_pu for b in self.buses]),
        va_deg=column([b.va_deg for b in self.buses]),
        p_bus_mw=column([b.p_mw for b in self.buses]),
        q_bus_mvar=column([b.q_mvar for b in self.buses]),
        branch_ids=tuple(b.id for b in self.branches),
        p_from_mw=column([b.p_from_mw for b in self.branches]),
        q_from_mvar=column([b.q_from_mvar for b in self.branches]),
        p_to_mw=column([b.p_to_mw for b in self.branches]),
        q_to_mvar=column([b.q_to_mvar for b in self.branches]),
        loading_pct=column(
            [np.nan if b.loading_pct is None else b.loading_pct for b in self.branches]
        ),
        gen_ids=tuple(g.id for g in self.generators),
        p_gen_mw=column([g.p_mw for g in self.generators]),
        q_gen_mvar=column([g.q_mvar for g in self.generators]),
    )

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

engine: Literal['mambo-power']

Producing engine; always this package.

version class-attribute instance-attribute

version: str

mambo_power.__version__ at solve time.

kind class-attribute instance-attribute

kind: str

Analysis kind, e.g. pf.dc or pf.ac.

solver class-attribute instance-attribute

solver: str

Linear-algebra backend, e.g. scipy.sparse.linalg.splu.

started_at class-attribute instance-attribute

started_at: datetime

Wall-clock start of the solve, UTC.

elapsed_s class-attribute instance-attribute

elapsed_s: float

Wall-clock duration of the solve, seconds.

options class-attribute instance-attribute

options: dict[str, Any]

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.

id class-attribute instance-attribute

id: str

Branch id from the network.

from_bus class-attribute instance-attribute

from_bus: str

Bus id of the from (tap) side.

to_bus class-attribute instance-attribute

to_bus: str

Bus id of the to side.

p_from_mw class-attribute instance-attribute

p_from_mw: float

Active power entering the branch at the from bus, MW.

q_from_mvar class-attribute instance-attribute

q_from_mvar: float

Reactive power entering at the from bus, MVAr.

p_to_mw class-attribute instance-attribute

p_to_mw: float

Active power entering the branch at the to bus, MW.

q_to_mvar class-attribute instance-attribute

q_to_mvar: float

Reactive power entering at the to bus, MVAr.

loading_pct class-attribute instance-attribute

loading_pct: float | None

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).

id class-attribute instance-attribute

id: str

Bus id from the network.

vm_pu class-attribute instance-attribute

vm_pu: float

Voltage magnitude, per unit (1.0 on every bus for DC).

va_deg class-attribute instance-attribute

va_deg: float

Voltage angle, degrees; the slack is 0.

p_mw class-attribute instance-attribute

p_mw: float

Net active injection into the network, MW.

q_mvar class-attribute instance-attribute

q_mvar: float

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

in_service: bool

Whether the bus was part of the solve.

GenResult

Bases: _Row

Dispatch of one generator after the solve.

id class-attribute instance-attribute

id: str

Generator id from the network.

bus class-attribute instance-attribute

bus: str

Bus id the generator is connected to.

p_mw class-attribute instance-attribute

p_mw: float

Active output, MW (slack-bus generators absorb the balance).

q_mvar class-attribute instance-attribute

q_mvar: float

Reactive output, MVAr (0 for DC).

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.

id class-attribute instance-attribute

id: str

Generator id from the network.

bus class-attribute instance-attribute

bus: str

Bus id the generator is connected to.

delta_up_mw class-attribute instance-attribute

delta_up_mw: float

Instructed increase above the zonal schedule, MW; >= 0. p_final = p_zonal + delta_up_mw - delta_down_mw exactly.

delta_down_mw class-attribute instance-attribute

delta_down_mw: float

Instructed decrease below the zonal schedule, MW; >= 0.

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".

id class-attribute instance-attribute

id: str

Load id from the network.

bus class-attribute instance-attribute

bus: str

Bus id the load is connected to.

delta_restore_mw class-attribute instance-attribute

delta_restore_mw: float

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

delta_curtail_mw: float

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

status: str

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

message: str | None

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

redispatch_payment: float = 0.0

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

welfare_gap: float = 0.0

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

generation_cost_gap: float = 0.0

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.

id class-attribute instance-attribute

id: str

Zone id from the network (Zone.id / Bus.zone).

price class-attribute instance-attribute

price: float

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
def feasibility_report(ac: AcPowerFlowResult, net: Network) -> FeasibilityReport:
    """Build a :class:`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.
    """
    thermal = [
        ThermalViolation(branch_id=b.id, loading_pct=b.loading_pct, limit_pct=THERMAL_LIMIT_PCT)
        for b in ac.branches
        if b.loading_pct is not None and b.loading_pct > THERMAL_LIMIT_PCT
    ]
    bounds_by_id = {bus.id: bus for bus in net.buses}
    voltage: list[VoltageViolation] = []
    for bus in ac.buses:
        bound = bounds_by_id.get(bus.id)
        if bound is None:
            continue
        if bound.v_min_pu is not None and bus.vm_pu < bound.v_min_pu:
            voltage.append(
                VoltageViolation(bus_id=bus.id, vm_pu=bus.vm_pu, limit_pu=bound.v_min_pu)
            )
        elif bound.v_max_pu is not None and bus.vm_pu > bound.v_max_pu:
            voltage.append(
                VoltageViolation(bus_id=bus.id, vm_pu=bus.vm_pu, limit_pu=bound.v_max_pu)
            )
    return FeasibilityReport(
        converged=ac.converged,
        message=ac.message,
        thermal_violations=thermal,
        voltage_violations=voltage,
    )

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
def 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 :class:`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 :attr:`~mambo_power.pf.ac_newton.AcSolution.message`, threaded through
    unchanged (set when ``converged`` is False, ``None`` otherwise).
    """
    if v.shape != (arr.n_bus,) or s_bus_pu.shape != (arr.n_bus,):
        raise ValueError("bus arrays must have shape (n_bus,)")
    if bus_type.shape != (arr.n_bus,) or q_limited.shape != (arr.n_bus,):
        raise ValueError("bus_type and q_limited must have shape (n_bus,)")
    if s_from_pu.shape != (arr.n_branch,) or s_to_pu.shape != (arr.n_branch,):
        raise ValueError("branch arrays must have shape (n_branch,)")
    n_gen = len(arr.gen_ids)
    if gen_p_pu.shape != (n_gen,) or gen_q_pu.shape != (n_gen,):
        raise ValueError("generator arrays must have shape (n_gen,)")
    base = arr.base_mva

    buses = [
        BusResult(
            id=arr.bus_ids[i],
            vm_pu=float(np.abs(v[i])),
            va_deg=math.degrees(float(np.angle(v[i]))),
            p_mw=float(s_bus_pu[i].real) * base,
            q_mvar=float(s_bus_pu[i].imag) * base,
            role_effective=_ROLE_BY_CODE[int(bus_type[i])],
            in_service=True,
        )
        for i in range(arr.n_bus)
    ]
    branches = [
        BranchResult(
            id=arr.branch_ids[k],
            from_bus=arr.bus_ids[int(arr.f[k])],
            to_bus=arr.bus_ids[int(arr.t[k])],
            p_from_mw=float(s_from_pu[k].real) * base,
            q_from_mvar=float(s_from_pu[k].imag) * base,
            p_to_mw=float(s_to_pu[k].real) * base,
            q_to_mvar=float(s_to_pu[k].imag) * base,
            loading_pct=_loading_pct(float(np.abs(s_from_pu[k])), float(arr.rating_pu[k])),
        )
        for k in range(arr.n_branch)
    ]
    generators = [
        GenResult(
            id=arr.gen_ids[g],
            bus=arr.bus_ids[int(arr.gen_bus[g])],
            p_mw=float(gen_p_pu[g]) * base,
            q_mvar=float(gen_q_pu[g]) * base,
            q_limited=_Q_LIMIT_SIDE[int(q_limited[int(arr.gen_bus[g])])],
        )
        for g in range(n_gen)
    ]
    return AcPowerFlowResult(
        provenance=provenance,
        converged=converged,
        buses=buses,
        branches=branches,
        generators=generators,
        iterations=iterations,
        max_mismatch_mva=max_mismatch_pu * base,
        q_limit_rounds=q_limit_rounds,
        message=message,
    )

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
def 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 :class:`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.
    """
    if theta_rad.shape != (arr.n_bus,) or p_inj_pu.shape != (arr.n_bus,):
        raise ValueError("bus arrays must have shape (n_bus,)")
    if p_from_pu.shape != (arr.n_branch,):
        raise ValueError("p_from_pu must have shape (n_branch,)")
    if gen_p_pu.shape != (len(arr.gen_ids),):
        raise ValueError("gen_p_pu must have shape (n_gen,)")
    base = arr.base_mva
    roles = arr.bus_type if bus_type is None else bus_type

    buses = [
        BusResult(
            id=arr.bus_ids[i],
            vm_pu=1.0,
            va_deg=math.degrees(float(theta_rad[i])),
            p_mw=float(p_inj_pu[i]) * base,
            q_mvar=0.0,
            role_effective=_ROLE_BY_CODE[int(roles[i])],
            in_service=True,
        )
        for i in range(arr.n_bus)
    ]
    branches = [
        BranchResult(
            id=arr.branch_ids[k],
            from_bus=arr.bus_ids[int(arr.f[k])],
            to_bus=arr.bus_ids[int(arr.t[k])],
            p_from_mw=float(p_from_pu[k]) * base,
            q_from_mvar=0.0,
            p_to_mw=-float(p_from_pu[k]) * base,
            q_to_mvar=0.0,
            loading_pct=_loading_pct(float(p_from_pu[k]), float(arr.rating_pu[k])),
        )
        for k in range(arr.n_branch)
    ]
    generators = [
        GenResult(
            id=arr.gen_ids[g],
            bus=arr.bus_ids[int(arr.gen_bus[g])],
            p_mw=float(gen_p_pu[g]) * base,
            q_mvar=0.0,
            q_limited="none",
        )
        for g in range(len(arr.gen_ids))
    ]
    return DcPowerFlowResult(
        provenance=provenance,
        converged=True,
        buses=buses,
        branches=branches,
        generators=generators,
    )

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

BusRole = Literal['slack', 'pv', 'pq']

The role a bus was solved with — the effective role, which may differ from the declared.

QLimitSide module-attribute

QLimitSide = Literal['none', 'min', 'max']

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).

id class-attribute instance-attribute

id: str

Bus id from the network.

vm_pu class-attribute instance-attribute

vm_pu: float

Voltage magnitude, per unit (1.0 on every bus for DC).

va_deg class-attribute instance-attribute

va_deg: float

Voltage angle, degrees; the slack is 0.

p_mw class-attribute instance-attribute

p_mw: float

Net active injection into the network, MW.

q_mvar class-attribute instance-attribute

q_mvar: float

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

in_service: bool

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.

id class-attribute instance-attribute

id: str

Branch id from the network.

from_bus class-attribute instance-attribute

from_bus: str

Bus id of the from (tap) side.

to_bus class-attribute instance-attribute

to_bus: str

Bus id of the to side.

p_from_mw class-attribute instance-attribute

p_from_mw: float

Active power entering the branch at the from bus, MW.

q_from_mvar class-attribute instance-attribute

q_from_mvar: float

Reactive power entering at the from bus, MVAr.

p_to_mw class-attribute instance-attribute

p_to_mw: float

Active power entering the branch at the to bus, MW.

q_to_mvar class-attribute instance-attribute

q_to_mvar: float

Reactive power entering at the to bus, MVAr.

loading_pct class-attribute instance-attribute

loading_pct: float | None

Apparent from-side flow over rating_mva in percent; None when unrated.

GenResult

Bases: _Row

Dispatch of one generator after the solve.

id class-attribute instance-attribute

id: str

Generator id from the network.

bus class-attribute instance-attribute

bus: str

Bus id the generator is connected to.

p_mw class-attribute instance-attribute

p_mw: float

Active output, MW (slack-bus generators absorb the balance).

q_mvar class-attribute instance-attribute

q_mvar: float

Reactive output, MVAr (0 for DC).

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

engine: Literal['mambo-power']

Producing engine; always this package.

version class-attribute instance-attribute

version: str

mambo_power.__version__ at solve time.

kind class-attribute instance-attribute

kind: str

Analysis kind, e.g. pf.dc or pf.ac.

solver class-attribute instance-attribute

solver: str

Linear-algebra backend, e.g. scipy.sparse.linalg.splu.

started_at class-attribute instance-attribute

started_at: datetime

Wall-clock start of the solve, UTC.

elapsed_s class-attribute instance-attribute

elapsed_s: float

Wall-clock duration of the solve, seconds.

options class-attribute instance-attribute

options: dict[str, Any]

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

converged: bool

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
def to_arrays(self) -> PowerFlowArrays:
    """The positional view: one numpy array per column, rows in table order."""

    def column(values: list[float]) -> FloatArray:
        return np.asarray(values, dtype=np.float64)

    return PowerFlowArrays(
        bus_ids=tuple(b.id for b in self.buses),
        vm_pu=column([b.vm_pu for b in self.buses]),
        va_deg=column([b.va_deg for b in self.buses]),
        p_bus_mw=column([b.p_mw for b in self.buses]),
        q_bus_mvar=column([b.q_mvar for b in self.buses]),
        branch_ids=tuple(b.id for b in self.branches),
        p_from_mw=column([b.p_from_mw for b in self.branches]),
        q_from_mvar=column([b.q_from_mvar for b in self.branches]),
        p_to_mw=column([b.p_to_mw for b in self.branches]),
        q_to_mvar=column([b.q_to_mvar for b in self.branches]),
        loading_pct=column(
            [np.nan if b.loading_pct is None else b.loading_pct for b in self.branches]
        ),
        gen_ids=tuple(g.id for g in self.generators),
        p_gen_mw=column([g.p_mw for g in self.generators]),
        q_gen_mvar=column([g.q_mvar for g in self.generators]),
    )

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

iterations: int

Newton iterations summed over all Q-limit rounds.

max_mismatch_mva class-attribute instance-attribute

max_mismatch_mva: float

Final power-mismatch infinity norm, MVA.

q_limit_rounds class-attribute instance-attribute

q_limit_rounds: int

Outer Q-limit enforcement rounds run.

message class-attribute instance-attribute

message: str | None

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
def 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 :class:`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.
    """
    if theta_rad.shape != (arr.n_bus,) or p_inj_pu.shape != (arr.n_bus,):
        raise ValueError("bus arrays must have shape (n_bus,)")
    if p_from_pu.shape != (arr.n_branch,):
        raise ValueError("p_from_pu must have shape (n_branch,)")
    if gen_p_pu.shape != (len(arr.gen_ids),):
        raise ValueError("gen_p_pu must have shape (n_gen,)")
    base = arr.base_mva
    roles = arr.bus_type if bus_type is None else bus_type

    buses = [
        BusResult(
            id=arr.bus_ids[i],
            vm_pu=1.0,
            va_deg=math.degrees(float(theta_rad[i])),
            p_mw=float(p_inj_pu[i]) * base,
            q_mvar=0.0,
            role_effective=_ROLE_BY_CODE[int(roles[i])],
            in_service=True,
        )
        for i in range(arr.n_bus)
    ]
    branches = [
        BranchResult(
            id=arr.branch_ids[k],
            from_bus=arr.bus_ids[int(arr.f[k])],
            to_bus=arr.bus_ids[int(arr.t[k])],
            p_from_mw=float(p_from_pu[k]) * base,
            q_from_mvar=0.0,
            p_to_mw=-float(p_from_pu[k]) * base,
            q_to_mvar=0.0,
            loading_pct=_loading_pct(float(p_from_pu[k]), float(arr.rating_pu[k])),
        )
        for k in range(arr.n_branch)
    ]
    generators = [
        GenResult(
            id=arr.gen_ids[g],
            bus=arr.bus_ids[int(arr.gen_bus[g])],
            p_mw=float(gen_p_pu[g]) * base,
            q_mvar=0.0,
            q_limited="none",
        )
        for g in range(len(arr.gen_ids))
    ]
    return DcPowerFlowResult(
        provenance=provenance,
        converged=True,
        buses=buses,
        branches=branches,
        generators=generators,
    )

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
def 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 :class:`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 :attr:`~mambo_power.pf.ac_newton.AcSolution.message`, threaded through
    unchanged (set when ``converged`` is False, ``None`` otherwise).
    """
    if v.shape != (arr.n_bus,) or s_bus_pu.shape != (arr.n_bus,):
        raise ValueError("bus arrays must have shape (n_bus,)")
    if bus_type.shape != (arr.n_bus,) or q_limited.shape != (arr.n_bus,):
        raise ValueError("bus_type and q_limited must have shape (n_bus,)")
    if s_from_pu.shape != (arr.n_branch,) or s_to_pu.shape != (arr.n_branch,):
        raise ValueError("branch arrays must have shape (n_branch,)")
    n_gen = len(arr.gen_ids)
    if gen_p_pu.shape != (n_gen,) or gen_q_pu.shape != (n_gen,):
        raise ValueError("generator arrays must have shape (n_gen,)")
    base = arr.base_mva

    buses = [
        BusResult(
            id=arr.bus_ids[i],
            vm_pu=float(np.abs(v[i])),
            va_deg=math.degrees(float(np.angle(v[i]))),
            p_mw=float(s_bus_pu[i].real) * base,
            q_mvar=float(s_bus_pu[i].imag) * base,
            role_effective=_ROLE_BY_CODE[int(bus_type[i])],
            in_service=True,
        )
        for i in range(arr.n_bus)
    ]
    branches = [
        BranchResult(
            id=arr.branch_ids[k],
            from_bus=arr.bus_ids[int(arr.f[k])],
            to_bus=arr.bus_ids[int(arr.t[k])],
            p_from_mw=float(s_from_pu[k].real) * base,
            q_from_mvar=float(s_from_pu[k].imag) * base,
            p_to_mw=float(s_to_pu[k].real) * base,
            q_to_mvar=float(s_to_pu[k].imag) * base,
            loading_pct=_loading_pct(float(np.abs(s_from_pu[k])), float(arr.rating_pu[k])),
        )
        for k in range(arr.n_branch)
    ]
    generators = [
        GenResult(
            id=arr.gen_ids[g],
            bus=arr.bus_ids[int(arr.gen_bus[g])],
            p_mw=float(gen_p_pu[g]) * base,
            q_mvar=float(gen_q_pu[g]) * base,
            q_limited=_Q_LIMIT_SIDE[int(q_limited[int(arr.gen_bus[g])])],
        )
        for g in range(n_gen)
    ]
    return AcPowerFlowResult(
        provenance=provenance,
        converged=converged,
        buses=buses,
        branches=branches,
        generators=generators,
        iterations=iterations,
        max_mismatch_mva=max_mismatch_pu * base,
        q_limit_rounds=q_limit_rounds,
        message=message,
    )

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

ramp_dual: float = 0.0

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.

id class-attribute instance-attribute

id: str

Storage id from the network.

bus class-attribute instance-attribute

bus: str

Bus id the unit is connected to.

charge_mw class-attribute instance-attribute

charge_mw: float

Charging power in this period, MW; >= 0.

discharge_mw class-attribute instance-attribute

discharge_mw: float

Discharging power in this period, MW; >= 0.

soc_mwh class-attribute instance-attribute

soc_mwh: float

State of charge at the end of this period, MWh.

soc_dual class-attribute instance-attribute

soc_dual: float

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

energy_bound_dual: float

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

power_limit_dual: float

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

period: int

Zero-based index of this period within the horizon.

total_load_payment class-attribute instance-attribute

total_load_payment: float = 0.0

Sum over every load of LMP(bus_d)*p_d in this period, $/h.

total_generator_receipts class-attribute instance-attribute

total_generator_receipts: float = 0.0

Sum over every generator of LMP(bus_g)*p_g in this period, $/h.

total_storage_charge_payment class-attribute instance-attribute

total_storage_charge_payment: float = 0.0

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

total_storage_discharge_revenue: float = 0.0

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

congestion_rent: float = 0.0

(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

status: str

HiGHS model status: "Optimal", "Infeasible", "Unbounded", or another HiGHS status string passed through verbatim.

message class-attribute instance-attribute

message: str | None

Diagnostic when status != Optimal.

n_periods class-attribute instance-attribute

n_periods: int

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

objective_cost: float = 0.0

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

total_load_payment: float = 0.0

Horizon sum of the per-period load payments, $.

total_generator_receipts class-attribute instance-attribute

total_generator_receipts: float = 0.0

Horizon sum of the per-period generator receipts, $.

total_storage_charge_payment class-attribute instance-attribute

total_storage_charge_payment: float = 0.0

Horizon sum of the per-period storage charge payments, $.

total_storage_discharge_revenue class-attribute instance-attribute

total_storage_discharge_revenue: float = 0.0

Horizon sum of the per-period storage discharge revenues, $.

congestion_rent class-attribute instance-attribute

congestion_rent: float = 0.0

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: 0 by 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.

id class-attribute instance-attribute

id: str

Zone id from the network (Zone.id / Bus.zone).

price class-attribute instance-attribute

price: float

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.

id class-attribute instance-attribute

id: str

Generator id from the network.

bus class-attribute instance-attribute

bus: str

Bus id the generator is connected to.

delta_up_mw class-attribute instance-attribute

delta_up_mw: float

Instructed increase above the zonal schedule, MW; >= 0. p_final = p_zonal + delta_up_mw - delta_down_mw exactly.

delta_down_mw class-attribute instance-attribute

delta_down_mw: float

Instructed decrease below the zonal schedule, MW; >= 0.

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".

id class-attribute instance-attribute

id: str

Load id from the network.

bus class-attribute instance-attribute

bus: str

Bus id the load is connected to.

delta_restore_mw class-attribute instance-attribute

delta_restore_mw: float

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

delta_curtail_mw: float

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

status: str

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

message: str | None

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

redispatch_payment: float = 0.0

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

welfare_gap: float = 0.0

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

generation_cost_gap: float = 0.0

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

TerminationReason = Literal[
    "converged", "iteration_cap", "cycle"
]

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.

id class-attribute instance-attribute

id: str

Generator id from the network.

strategy class-attribute instance-attribute

strategy: str

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

offer: GeneratorCost

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

true_cost: GeneratorCost

The generator's own Generator.cost, unchanged by the run.

cleared_mw class-attribute instance-attribute

cleared_mw: float

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

markup: float

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

status: str

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.

message class-attribute instance-attribute

message: str | None

Diagnostic when status != Optimal.

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

iterations: int = 0

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

converged: bool = False

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

total_load_payment: float = 0.0

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

total_generator_receipts: float = 0.0

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

congestion_rent: float = 0.0

total_load_payment - total_generator_receipts, $/h; 0.0 when not Optimal.