mambo_power.jobs¶
The stateless job surface. See the manual page for the guarantees, the failure codes and executed examples.
mambo_power.jobs
¶
Stateless, JSON-serialisable job surface: run(SolveRequest) -> SolveResult (ADR-004, W6).
The one function every analysis kind is reachable through, safe to call from a notebook, a
CLI, a worker or an HTTP handler — a service adds transport and persistence, never semantics.
run is a pure function of its input; every failure is a status = "failed" result with a
StructuredError, never an exception; KINDS is the capability list of the
installed version. The module-level entry points (pf.solve_ac, pf.solve_dc) remain the
notebook-friendly API and are what the registered runners call.
FailureCode
module-attribute
¶
FailureCode = Literal[
"UNKNOWN_KIND",
"BAD_REQUEST",
"BAD_OPTIONS",
"VALIDATION",
"NO_SLACK_GENERATOR",
"UNSOLVABLE_NETWORK",
"INFEASIBLE_LP",
"UNBOUNDED_LP",
"INTERNAL",
]
The codes mambo_power.jobs.run / mambo_power.jobs.run_json emit (M2, M3).
ResultModel
module-attribute
¶
ResultModel = (
AcPowerFlowResult
| DcPowerFlowResult
| OpfDcResult
| N1Result
| MarketNodalResult
| MarketMultiperiodResult
| MarketZonalResult
| MarketAgentsResult
)
The closed union of result types a SolveResult can carry (one per registered kind).
KINDS
module-attribute
¶
KINDS: dict[str, KindSpec] = {}
Every analysis kind the installed version can run, keyed by name (insertion order).
Runner
module-attribute
¶
Runner = Callable[[Scenario, BaseModel | None], BaseModel]
Signature every kind's runner has: (scenario, validated_options_or_None) -> result.
SolveRequest
¶
Bases: BaseModel
One analysis to run: the kind, the subject (inline) and the kind's options.
network/scenario — wave M5 design item D3 (2026-08-25) — exactly one must be given:
network is the original, still-supported shape (a bare
Network), and every pre-existing
SolveRequest(kind=..., network=...) construction and serialized JSON keeps working
unchanged. scenario is the new form, for a genuine multi-period
Scenario (market.multiperiod) or simply an explicit
single-period one. Neither or both given is a ValueError (a pydantic error at
construction time; mambo_power.jobs.run_json turns it into BAD_REQUEST).
resolved_scenario is what every Runner actually
receives — scenario itself, or network wrapped as Scenario(network=network)
(single-period, periods=None, exactly market.nodal's and every T=1 kind's existing
semantics) — never the raw fields, so widening this model changes no runner's contract.
options is validated by mambo_power.jobs.run against the kind's options model
(AcOptions for pf.ac; pf.dc takes none) — unknown keys are a BAD_OPTIONS
failure, never silently ignored. job_id is an opaque caller tag echoed on the result.
network
class-attribute
instance-attribute
¶
network: Network | None
The network to solve; mutually exclusive with scenario.
scenario
class-attribute
instance-attribute
¶
scenario: Scenario | None
The scenario to solve; mutually exclusive with network.
options
class-attribute
instance-attribute
¶
Kind-specific options, validated by run.
job_id
class-attribute
instance-attribute
¶
Caller's correlation id, echoed back.
resolved_scenario
property
¶
resolved_scenario: Scenario
This request as a Scenario: scenario itself when
given, or network wrapped as Scenario(network=network) — single-period,
periods=None. Recomputed on every access, not cached, so a network mutated in
place after construction (request.network.branches[0].to_bus = ... — Network
does not re-validate on mutation on its own) is reflected here too, exactly as it was
when jobs.registry._run_market_nodal did this same wrap internally pre-M5.
Constructing the wrapping Scenario does re-run Network's own after-validator —
nested-model construction re-checks every invariant (model/scenario.py's own
docstring) — so this can raise NetworkValidationError for a
network mutated into an invalid state; mambo_power.jobs.run catches that
itself, immediately, precisely so it stays a graceful VALIDATION failure rather than
an exception crossing its boundary. A directly-supplied scenario is returned as-is,
with no such re-check — mirroring network's own no-revalidation-on-mutation rule.
SolveResult
¶
Bases: BaseModel
Outcome of mambo_power.jobs.run: a typed result or a structured error, never both.
status == "ok" carries result (the kind's result model) and its provenance;
status == "failed" carries error and, when the kind was readable, a minimal
provenance (kind, version, elapsed time, solver = "none"). warnings holds every
warning emitted during the solve as "Category: message" strings — for a network with
conflicting generator setpoints that is the SetpointConflictWarning. A power flow that
did not converge is status == "ok" with result.converged == False: the partial
state is a result, not a failure.
result
class-attribute
instance-attribute
¶
result: ResultModel | None
The kind's result model; present when status == "ok".
error
class-attribute
instance-attribute
¶
error: StructuredError | None
Present when status == "failed".
provenance
class-attribute
instance-attribute
¶
provenance: ResultProvenance | None
The result's stamp, or a minimal one on failure.
warnings
class-attribute
instance-attribute
¶
Warnings emitted during the solve, as strings.
StructuredError
¶
Bases: BaseModel
A failure as data: stable code, readable message, optional structured detail.
issues is the network's full ValidationIssue list for
VALIDATION failures (every problem in one response); details is the pydantic error
list (loc, msg, type) for BAD_OPTIONS and BAD_REQUEST. code is a plain
string so later kinds can add codes without a schema change; M2's are
FailureCode.
message
class-attribute
instance-attribute
¶
Human-readable description; the exception text when any.
issues
class-attribute
instance-attribute
¶
issues: list[ValidationIssue] | None
Every network validation issue, for VALIDATION.
details
class-attribute
instance-attribute
¶
pydantic error records (loc, msg, type) for bad options/requests.
InfeasibleLpError
¶
Bases: Exception
opf.dc's, market.nodal's, market.multiperiod's, market.zonal's or
market.agents's runner found a non-Optimal, non-Unbounded status (e.g.
OpfDcResult.status == "Infeasible") — see _translate_non_optimal_status.
None of mambo_power.opf.solve_dc_opf, mambo_power.market.nodal.solve_nodal,
mambo_power.market.multiperiod.solve_multiperiod,
mambo_power.market.zonal.solve_zonal or
mambo_power.market.agents.solve_agents ever raises on a non-Optimal LP/QP status
(their own docstrings, mirroring mambo_power.pf.solve_ac's
never-raise-on-non-convergence convention) — each reports the status as data. But an
infeasible LP has no dispatch at all, unlike a non-converged AC iterate which still
carries a meaningful partial state; wave M3's design (item 7) draws that line deliberately,
so every such job kind reports it as a structured job failure (INFEASIBLE_LP) rather
than a "successful" result carrying a non-Optimal status. Raised only here, by the job
runners — not by
solve_dc_opf/solve_nodal/solve_multiperiod/solve_zonal/solve_agents
themselves.
KindSpec
dataclass
¶
KindSpec(
kind: str,
options_model: type[BaseModel] | None,
result_model: type[BaseModel],
runner: Runner,
)
UnboundedLpError
¶
Bases: Exception
opf.dc's, market.nodal's, market.multiperiod's, market.zonal's or
market.agents's runner found status "Unbounded"; see InfeasibleLpError for
why this is a job failure rather than an "ok" result.
kinds
¶
register
¶
register(spec: KindSpec) -> None
Add spec to KINDS; a kind already registered raises ValueError.
run_json
¶
JSON in, JSON out: parse text as a SolveRequest, run it.
Returns SolveResult.model_dump_json(). A request that does not parse is a failed result
too: an invalid network → VALIDATION with every issue; malformed JSON or a request of
the wrong shape → BAD_REQUEST with the pydantic errors in error.details; a JSON
object repeating a key at any depth → BAD_REQUEST naming the key and its path, for every
kind, because json would otherwise keep the last value silently. The kind
and job_id are echoed when they can be read from the text (kind = "" and no
provenance otherwise).
Source code in src/mambo_power/jobs/run.py
Models¶
mambo_power.jobs.models
¶
Request, result and error models of the job surface (ADR-004; wave M2 W6).
All three are pydantic v2 models with extra="forbid" and exact JSON round-trip, so the
body of an HTTP request is a SolveRequest and the body of the response is a
SolveResult — no translation layer.
How SolveResult.result is typed. Its annotation is the closed union of the registered
kinds' result models (AcPowerFlowResult | DcPowerFlowResult in M2, widened to add
OpfDcResult | N1Result in M3, MarketNodalResult in M4, MarketMultiperiodResult in
M5, MarketZonalResult in M6, MarketAgentsResult in M7). The type is not inferred from
the payload's shape: a
model_validator(mode="before") looks the request kind up in KINDS
and validates a dict result
with exactly that kind's result_model; a second validator (mode="after") then checks
that the instance type equals the kind's model, and that status agrees with which of
result / error is present. A pydantic discriminated union was not used because the
discriminator (kind) lives on the parent, not inside the result, and because the power-flow
results do not carry a tag field of their own. A wave that registers a new kind widens the union
annotation — the after validator will refuse a result whose class is not the kind's
result_model, and the field validation refuses a class outside the union, so the two cannot
silently drift apart.
ResultModel
module-attribute
¶
ResultModel = (
AcPowerFlowResult
| DcPowerFlowResult
| OpfDcResult
| N1Result
| MarketNodalResult
| MarketMultiperiodResult
| MarketZonalResult
| MarketAgentsResult
)
The closed union of result types a SolveResult can carry (one per registered kind).
FailureCode
module-attribute
¶
FailureCode = Literal[
"UNKNOWN_KIND",
"BAD_REQUEST",
"BAD_OPTIONS",
"VALIDATION",
"NO_SLACK_GENERATOR",
"UNSOLVABLE_NETWORK",
"INFEASIBLE_LP",
"UNBOUNDED_LP",
"INTERNAL",
]
The codes mambo_power.jobs.run / mambo_power.jobs.run_json emit (M2, M3).
StructuredError
¶
Bases: BaseModel
A failure as data: stable code, readable message, optional structured detail.
issues is the network's full ValidationIssue list for
VALIDATION failures (every problem in one response); details is the pydantic error
list (loc, msg, type) for BAD_OPTIONS and BAD_REQUEST. code is a plain
string so later kinds can add codes without a schema change; M2's are
FailureCode.
message
class-attribute
instance-attribute
¶
Human-readable description; the exception text when any.
issues
class-attribute
instance-attribute
¶
issues: list[ValidationIssue] | None
Every network validation issue, for VALIDATION.
details
class-attribute
instance-attribute
¶
pydantic error records (loc, msg, type) for bad options/requests.
SolveRequest
¶
Bases: BaseModel
One analysis to run: the kind, the subject (inline) and the kind's options.
network/scenario — wave M5 design item D3 (2026-08-25) — exactly one must be given:
network is the original, still-supported shape (a bare
Network), and every pre-existing
SolveRequest(kind=..., network=...) construction and serialized JSON keeps working
unchanged. scenario is the new form, for a genuine multi-period
Scenario (market.multiperiod) or simply an explicit
single-period one. Neither or both given is a ValueError (a pydantic error at
construction time; mambo_power.jobs.run_json turns it into BAD_REQUEST).
resolved_scenario is what every Runner actually
receives — scenario itself, or network wrapped as Scenario(network=network)
(single-period, periods=None, exactly market.nodal's and every T=1 kind's existing
semantics) — never the raw fields, so widening this model changes no runner's contract.
options is validated by mambo_power.jobs.run against the kind's options model
(AcOptions for pf.ac; pf.dc takes none) — unknown keys are a BAD_OPTIONS
failure, never silently ignored. job_id is an opaque caller tag echoed on the result.
network
class-attribute
instance-attribute
¶
network: Network | None
The network to solve; mutually exclusive with scenario.
scenario
class-attribute
instance-attribute
¶
scenario: Scenario | None
The scenario to solve; mutually exclusive with network.
options
class-attribute
instance-attribute
¶
Kind-specific options, validated by run.
job_id
class-attribute
instance-attribute
¶
Caller's correlation id, echoed back.
resolved_scenario
property
¶
resolved_scenario: Scenario
This request as a Scenario: scenario itself when
given, or network wrapped as Scenario(network=network) — single-period,
periods=None. Recomputed on every access, not cached, so a network mutated in
place after construction (request.network.branches[0].to_bus = ... — Network
does not re-validate on mutation on its own) is reflected here too, exactly as it was
when jobs.registry._run_market_nodal did this same wrap internally pre-M5.
Constructing the wrapping Scenario does re-run Network's own after-validator —
nested-model construction re-checks every invariant (model/scenario.py's own
docstring) — so this can raise NetworkValidationError for a
network mutated into an invalid state; mambo_power.jobs.run catches that
itself, immediately, precisely so it stays a graceful VALIDATION failure rather than
an exception crossing its boundary. A directly-supplied scenario is returned as-is,
with no such re-check — mirroring network's own no-revalidation-on-mutation rule.
SolveResult
¶
Bases: BaseModel
Outcome of mambo_power.jobs.run: a typed result or a structured error, never both.
status == "ok" carries result (the kind's result model) and its provenance;
status == "failed" carries error and, when the kind was readable, a minimal
provenance (kind, version, elapsed time, solver = "none"). warnings holds every
warning emitted during the solve as "Category: message" strings — for a network with
conflicting generator setpoints that is the SetpointConflictWarning. A power flow that
did not converge is status == "ok" with result.converged == False: the partial
state is a result, not a failure.
result
class-attribute
instance-attribute
¶
result: ResultModel | None
The kind's result model; present when status == "ok".
error
class-attribute
instance-attribute
¶
error: StructuredError | None
Present when status == "failed".
provenance
class-attribute
instance-attribute
¶
provenance: ResultProvenance | None
The result's stamp, or a minimal one on failure.
warnings
class-attribute
instance-attribute
¶
Warnings emitted during the solve, as strings.
Registry¶
mambo_power.jobs.registry
¶
The analysis-kinds registry: what the installed version can run (ADR-004, design item 6).
KINDS maps a kind name ("pf.ac", "pf.dc", "opf.dc", "n1", "market.nodal",
"market.multiperiod", "market.zonal", "market.agents") to a KindSpec — the
options model the request's options dict is validated against, the result model the runner
returns, and the runner itself. The registry is the capability list a service publishes, and the
contract test (AC-6/AC-8, wave M4 AC-7, wave M5 AC-7, wave M6 AC-7, wave M7 AC-6) asserts every
entry's models are importable and its runner callable. Later waves add kinds with register;
nothing else in the package changes.
market.nodal was the first kind whose subject is not a bare Network:
mambo_power.market.nodal.solve_nodal takes a Scenario. Wave M4 kept SolveRequest
network-shaped and had _run_market_nodal wrap the incoming Network into a
Scenario itself, since Scenario was then genuinely just network: Network. Wave M5
(design item D3) widened SolveRequest to accept either network or scenario — now
that Scenario also carries periods, a bare Network genuinely cannot supply everything
a caller may need — so that wrap moved outward, onto SolveRequest.resolved_scenario
(jobs/models.py): every Runner now has the one (Scenario, options) -> result shape,
and reads .network off the scenario when that is all it needs (pf.ac, pf.dc,
opf.dc, n1); _run_market_nodal no longer wraps anything itself.
Runner
module-attribute
¶
Runner = Callable[[Scenario, BaseModel | None], BaseModel]
Signature every kind's runner has: (scenario, validated_options_or_None) -> result.
KINDS
module-attribute
¶
KINDS: dict[str, KindSpec] = {}
Every analysis kind the installed version can run, keyed by name (insertion order).
InfeasibleLpError
¶
Bases: Exception
opf.dc's, market.nodal's, market.multiperiod's, market.zonal's or
market.agents's runner found a non-Optimal, non-Unbounded status (e.g.
OpfDcResult.status == "Infeasible") — see _translate_non_optimal_status.
None of mambo_power.opf.solve_dc_opf, mambo_power.market.nodal.solve_nodal,
mambo_power.market.multiperiod.solve_multiperiod,
mambo_power.market.zonal.solve_zonal or
mambo_power.market.agents.solve_agents ever raises on a non-Optimal LP/QP status
(their own docstrings, mirroring mambo_power.pf.solve_ac's
never-raise-on-non-convergence convention) — each reports the status as data. But an
infeasible LP has no dispatch at all, unlike a non-converged AC iterate which still
carries a meaningful partial state; wave M3's design (item 7) draws that line deliberately,
so every such job kind reports it as a structured job failure (INFEASIBLE_LP) rather
than a "successful" result carrying a non-Optimal status. Raised only here, by the job
runners — not by
solve_dc_opf/solve_nodal/solve_multiperiod/solve_zonal/solve_agents
themselves.
UnboundedLpError
¶
Bases: Exception
opf.dc's, market.nodal's, market.multiperiod's, market.zonal's or
market.agents's runner found status "Unbounded"; see InfeasibleLpError for
why this is a job failure rather than an "ok" result.
KindSpec
dataclass
¶
KindSpec(
kind: str,
options_model: type[BaseModel] | None,
result_model: type[BaseModel],
runner: Runner,
)
Runner¶
mambo_power.jobs.run
¶
run and run_json: the one pure entry point every analysis kind is reachable through.
Pipeline of run (design item 6):
- look the kind up in
KINDS— miss →UNKNOWN_KIND; - validate
request.optionsinto the kind's options model —BAD_OPTIONS(pydantic errors inerror.details); a kind without an options model rejects any key; - resolve
requestto aScenarioviarequest.resolved_scenario(wave M5 D3:scenarioas given, ornetworkwrapped) and re-check its network's invariants withmambo_power.model.validate_network—VALIDATIONwith every issue (aNetworkvalidates on construction but not on mutation, sorundoes not trust its input); - call the runner under
warnings.catch_warnings(record=True)and wrap what it raises:NetworkValidationError→VALIDATIONwith.issues;NoSlackGeneratorError→NO_SLACK_GENERATOR;UnsolvableNetworkError→UNSOLVABLE_NETWORK(a valid network the numerics it was handed to cannot solve, e.g. DC on anx == 0branch — user data, not a solver bug);InfeasibleLpError/:class:~mambo_power.jobs.registry. UnboundedLpError→INFEASIBLE_LP/UNBOUNDED_LP(theopf.dcrunner's own translation of a non-Optimalstatus; unlikepf.ac's non-convergence, an infeasible/unbounded LP has no dispatch at all, so it is a structured failure rather than an"ok"result); anything else →INTERNALwith"ExceptionType: message"; - check the runner returned the kind's
result_model(elseINTERNAL), copy its provenance and the captured warnings onto theSolveResult.
No exception crosses run: a runner whose result class is outside the result union
is an INTERNAL failure too. A solve
that does not converge is not a failure — solve_ac returns converged = False rather
than raising, and that result is passed through with status = "ok".
Warnings are captured with warnings.catch_warnings, which swaps the process-global
filter list for the duration of the runner; two run calls on different threads can
therefore see each other's warnings (Python ≥ 3.14 makes the context thread-local). Pure
means "a function of its input", which holds; warning attribution across threads is the one
caveat, and a worker process per job (the SaaS's deployment shape) does not hit it.
NO_SOLVER
module-attribute
¶
provenance.solver on a failed result: no linear-algebra backend ran to completion.
run
¶
run(request: SolveRequest) -> SolveResult
Run request and return a SolveResult; never raises.
See the module docstring for the pipeline and the failure codes. On success
result is the kind's result model, provenance is the stamp the solver put on it,
and warnings lists every warning the solve emitted (they are captured, not shown).
Source code in src/mambo_power/jobs/run.py
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
run_json
¶
JSON in, JSON out: parse text as a SolveRequest, run it.
Returns SolveResult.model_dump_json(). A request that does not parse is a failed result
too: an invalid network → VALIDATION with every issue; malformed JSON or a request of
the wrong shape → BAD_REQUEST with the pydantic errors in error.details; a JSON
object repeating a key at any depth → BAD_REQUEST naming the key and its path, for every
kind, because json would otherwise keep the last value silently. The kind
and job_id are echoed when they can be read from the text (kind = "" and no
provenance otherwise).