Python API¶
This page is the reference for running a spec from Python: every public name, rendered from its docstring. A spec is the YAML file; what it may contain is the language.
import specsolve as sps
sps.check('spec.yaml') # compiles? no data needed
result = sps.solve('spec.yaml', sources)
result.objective
result.primal('p') # a polars.DataFrame
result.dual('power_balance')
Reference¶
Every public name, rendered from its docstring. The glossary defines model, result, sink and the other house terms the entries use.
Run a spec¶
check ¶
Parse, validate and lower a spec; attach no data.
The CI verb: with no data and no solver, a spec repository validates every commit. Every other verb reads the spec through the same door, so what this refuses they refuse too.
With sink, also: will that sink take it? Bare check says nothing
about portability. The answer is read off a declared table with no data
attached, so it needs no solver installed, and solve and
write read the same table, so the refusal comes whether or not it was
asked for. The solver-independent advice is issued either way.
| PARAMETER | DESCRIPTION |
|---|---|
spec
|
A YAML path, a mapping, or a
TYPE:
|
sink
|
A solver name (
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Program
|
The lowered program: what a build reads rows off, for reading the plan. |
Program
|
No verb takes it back; keep the |
Program
|
language's own type — typeset it, or read its declarations, through |
Program
|
|
| RAISES | DESCRIPTION |
|---|---|
LanguageError
|
A construct outside the streaming language, or a
|
SpecsolveError
|
A sink that cannot take this spec, naming the construct and the sinks that do; a name belonging to no sink; or two declarations whose names differ only by case. |
ValueError
|
A schema or expression that does not parse. |
| WARNS | DESCRIPTION |
|---|---|
SpecsolveWarning
|
Advice short of an error — a declared dimension nothing uses as an axis, a variable the objective drives to infinity with nothing to stop it. Issued here and nowhere else. |
build ¶
Attach sources to spec and build it — the model with your data on it.
| PARAMETER | DESCRIPTION |
|---|---|
spec
|
As
TYPE:
|
sources
|
Parameter names to parquet paths or in-memory tables, and dimension names to their labels — an index table, a parquet path, or a bare sequence — wherever the YAML declares none. The whole of the build's input: the shapes a value may take, and what attaching refuses, are the data contract.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Model
|
The built model. It feeds any number of sinks — |
Model
|
|
Model
|
puts new numbers on it. |
| RAISES | DESCRIPTION |
|---|---|
LanguageError
|
A construct outside the streaming language. |
DataError
|
A source that is missing, unreadable, or the wrong shape. |
solve ¶
Build spec and solve it in one call.
The one-shot spelling: a caller who will solve the same spec again with
new numbers wants build and Model.update.
There is no keep here — this builds the model it solves, so the solve
is the first of that model's life and
kept is always nothing.
Choosing what to keep is Model.solve.
| PARAMETER | DESCRIPTION |
|---|---|
spec
|
As
TYPE:
|
sources
|
As
TYPE:
|
solver_name
|
As
TYPE:
|
solver_options
|
As
TYPE:
|
archive
|
Where to write the spec, its data and this answer, as
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Result
|
The solution, self-contained: it owns the frames it reads, so the built |
Result
|
model and the solver are released before this returns and there is |
Result
|
nothing to manage. |
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
A solver name nothing serves — checked before the build. |
write ¶
Build spec and stream it to a file, in the format out's suffix names.
| PARAMETER | DESCRIPTION |
|---|---|
spec
|
As
TYPE:
|
sources
|
As
TYPE:
|
out
|
Where to write;
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Path
|
The path written. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
A suffix nothing writes — checked before the build. |
SpecsolveError
|
A construct the format has no section for, which is
|
evaluate ¶
The value of expression over a spec with no variables — arithmetic, no solver.
A spec that declares no variables is a calculation, not an optimisation:
dimensions, parameters, relations and expressions:. Each expression reads
only the attached data, so it has a value with no solve and no chosen point.
This attaches sources and values one expression, the way
evaluate does at a solution. The
language it is read through — what loads, what is refused, how a construct
prints and lowers — is the one a spec that solves is read through; only the
variables are absent.
A spec that declares variables is a problem to solve, and belongs to solve: an
expression over a decision has no value until the decision is made.
| PARAMETER | DESCRIPTION |
|---|---|
spec
|
As
TYPE:
|
sources
|
As
TYPE:
|
expression
|
What one
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DataFrame
|
The value, |
DataFrame
|
this expression is compiled: a declared one nothing asks for costs |
DataFrame
|
nothing. |
| RAISES | DESCRIPTION |
|---|---|
LanguageError
|
A construct outside the streaming language, or a name the spec does not declare. |
SpecsolveError
|
A spec that declares variables, constraints or an objective — a problem to solve, not a calculation to evaluate. |
DataError
|
A source that is missing, unreadable, or the wrong shape, or a divisor with no value where the expression divides. |
Run it many times¶
The fold and its two axes; sweeps says how a sweep is cut and read.
solve_over ¶
solve_over(spec, sources, axis, *, carry=None, key_name=None, executor=None, workers_share_fs=None, solver_options=None, solver_name='highs', keep='solver', spill_to=None, archive=None)
Solve spec once per slice of axis and fold the answers together.
The rules — what a carry copies, how the key column is named, which executor to choose — are sweeps.
| PARAMETER | DESCRIPTION |
|---|---|
spec
|
As
TYPE:
|
sources
|
As
TYPE:
|
axis
|
TYPE:
|
carry
|
TYPE:
|
key_name
|
What to call the slice column; a class axis names its own, a hand-built list has to be told.
TYPE:
|
executor
|
Any
TYPE:
|
workers_share_fs
|
Whether the executor's workers can read this process's paths. Decided for the stdlib pools; anything else is assumed not to, and paths travel as bytes.
TYPE:
|
solver_options
|
As
TYPE:
|
solver_name
|
As
TYPE:
|
keep
|
As
TYPE:
|
spill_to
|
A directory to write each slice's frames to as the fold goes,
so the sweep's memory stays at one slice however many there
are. Read back through
TYPE:
|
archive
|
Where to write the whole thing — the model, the sources the
sweep was cut from, the axis that cut them, and every slice's
answer — so that
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Sweep
|
Every slice's answers, keyed by slice. |
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
A carry that cannot line up, has no seed, collapses a dimension the axis does not advance along, or is asked together with an executor; a key that collides with a column the frames carry; an axis the program does not allow; a spill_to directory holding another sweep. All refused before a slice is taken, and every one answerable from the declarations before a source is read. |
DataError
|
No source carries the axis, or the axis produced no slices. |
| WARNS | DESCRIPTION |
|---|---|
SpecsolveWarning
|
A source carrying the axis that is short of a coordinate another has — that slice builds it empty — or a position the model counts, which every window restarts. |
EachCoordinate
dataclass
¶
One slice per coordinate of dim — a column the sources carry.
Scenarios, draws, investment periods. Sources carrying dim are filtered
to one coordinate and the column dropped, so the model never mentions it —
a dim the spec declares is refused; every other source passes through
untouched. The slices run in the coordinates' sorted order, which is the
order a carry chains them in.
EachWindow
dataclass
¶
One slice per window of consecutive coordinates of dim.
steps is what each window keeps and lookahead is what it sees beyond
that, so a window is steps + lookahead coordinates long and a
lookahead above zero is overlap. An int keeps the same number every
window; a sequence keeps those numbers in order, which is a telescoping
horizon or a month at a time. Both count coordinates rather than coordinate
values, so dim need only be orderable — datetimes, strings and gapped
integers all work. The dimension is re-indexed rather than dropped, into a
dense 0..n-1 column the model addresses by the name into gives it,
which the spec has to declare.
Whether the model can be cut this way is asked before a slice is taken
(separability):
a coupling along into is refused, naming the declaration and the
change that would lift it; lookahead has to cover what the rows read
ahead; and a position() the model counts warns, since every window
restarts it. What the rows read behind is the rolling-horizon seed, met
by the edge policy, and is not refused.
slices ¶
The (key, sources) list this axis would run — what axis= takes hand-built.
For building one window alone: sps.build(spec, axis.slices(sources)[37][1]).
Pairs, so a window's ownership is not in them: solved as a list the
slices key by key_name=, original_index is refused and a
carry cannot collapse a dimension.
What comes back¶
Model ¶
A spec with your data attached to it — what build returns.
Three nouns, each arrow adding one thing: a Program is the math,
a Model is the math with your data, a Result is one answer:
check → Program → build → Model → solve → Result.
One build feeds any number of sinks — solve and write on
the same object — update puts new numbers on it without re-reading
the YAML or re-lowering the plan, and diagnostics says what it did.
Nothing has to be released; close hands a large model back early.
diagnostics ¶
What this build and its solves did that the answer does not show.
Answerable after close, and after a build that raised: every
field is a count, a clock or a small frame the engine keeps, not a read
of the model it releases. A raise leaves the sizes at zero — they are
taken once a model is whole — and everything measured before it stands.
evaluator ¶
An ad-hoc expression reader over a saved solution, put back against this build.
What an archive and a sweep hand evaluate
for a quantity the file never named: the saved frames are laid back in
this build's label order, and the reader is the one a live solve gives.
A build, never a solve.
| PARAMETER | DESCRIPTION |
|---|---|
primals
|
The saved
TYPE:
|
duals
|
The same per constraint, or
TYPE:
|
no_duals
|
Why there are no duals, or
TYPE:
|
row ¶
One built constraint row at one coordinate — its terms, sense and right-hand side.
The verb for this row is wrong and I do not know why. to_latex
and its siblings render the spec as math before any data, and
dual gives a row's number
without its terms; this gives the row the build actually produced, at
the coordinate you name.
Reads the built model and needs no solve, so it answers on a model
that never reached a solver — and it is the built row, so a term whose
variable was absent is missing from it and a row a where masked out
is not there at all. It shows what the model says rather than what the
file appears to say. A column has no reader: a variable's bounds are in
the spec, and its coefficients are this read transposed.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
A declared constraint. Positional, so that a dimension may
be called
TYPE:
|
coordinate
|
One label per dim of that declaration, all of them — a partial coordinate names a set of rows rather than one.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ConstraintRow
|
The terms as |
ConstraintRow
|
comparison and the right-hand side. |
| RAISES | DESCRIPTION |
|---|---|
KeyError
|
No constraint is called name. |
SpecsolveError
|
The coordinate names the wrong dims, holds a label its dimension cannot hold, matches no row the build produced, or the model has been closed. |
Example
print(model.row('balance', snapshot=1)) # doctest: +SKIP balance[snapshot=1]: +1 p[1, wind] +50 p[1, gas] >= 60
solve ¶
Hand the built model to a solver and solve it.
A solver that can stay loaded is kept between calls, so an updated
model skips the hand-off and only its numbers are pushed. Whether the
work that solver did is kept too is keep, off by default. How much
this solve actually kept is its
kept.
| PARAMETER | DESCRIPTION |
|---|---|
solver_name
|
TYPE:
|
solver_options
|
Forwarded to the solver verbatim, in its own
vocabulary, so a time limit is
TYPE:
|
keep
|
How much of the session this solve may keep:
TYPE:
|
archive
|
Where to write the whole thing — the spec, the data
attached to it now, and this answer — so that
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Result
|
The solution, holding this model. |
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
A solver name nothing serves, one this environment cannot run, or a keep other than those three. |
LayoutError
|
An archive directory that already holds something, refused before the solve rather than after it. |
update ¶
Put new numbers on the same model, in place.
::
model.update({'cap_hat': capacity}).solve()
Any new data is accepted: model.update(x) answers what
build(spec, sources | x) answers, whatever changed. Data that moves
a mask renumbers labels, so the model is rebuilt and solved cold
instead of pushed onto a loaded solver, and
loads says which ran.
Results taken before the update keep reading: each owns the frames it
reads, and an update builds new ones rather than touching those. A
retained result keeps its build's label frames alive until it is
dropped or close is called.
A loop whose next numbers depend on the last answer is this; a sweep, a
rolling horizon or a myopic pathway is solve_over,
which runs the loop.
| PARAMETER | DESCRIPTION |
|---|---|
sources
|
Only what changed; the rest keeps what
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Model
|
This object, so a driver can chain. |
| RAISES | DESCRIPTION |
|---|---|
DataError
|
A name the spec does not declare, since an update that named nothing would solve the old numbers again. An update that raises releases the model, as a build that raises does. |
write ¶
Stream the built model to path, in the format its suffix names.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
A suffix nothing writes. |
SpecsolveError
|
A construct the format has no section for, the same as
|
ConstraintRow
dataclass
¶
One built constraint row, spelled back out — what row returns.
The row a model actually built at one coordinate: every term with its
coefficient, and the comparison and right-hand side it was built against.
Read off the built model, so it needs no solve — and it is the built
row, after where masking, after any term whose variable was absent
dropped out, and after a coefficient the data made exactly zero stopped
being a term at all. Those
three are why a row can be shorter than the file suggests, and why reading
one is worth it when a model says something other than what its author
wrote.
Printing it gives the row as one line of math in linopy's format, which is
what reading a row usually means. A row wider than display_terms
prints instead how many terms each variable contributes and the span of
their coefficients. terms is the same content as a frame, for the
row too wide to read and for anything that filters or joins.
| ATTRIBUTE | DESCRIPTION |
|---|---|
name |
The constraint this row belongs to.
TYPE:
|
coordinate |
Where in that declaration it sits.
TYPE:
|
terms |
TYPE:
|
sense |
TYPE:
|
rhs |
What the left-hand side is compared against.
TYPE:
|
display_terms
class-attribute
instance-attribute
¶
How many terms a line spells out before it summarises instead.
Result
dataclass
¶
Result(_status, _objective, _primals, _duals, _activities, _kept, _expressions=None, _evaluate=None, _no_duals=None, _dual_rays=None, _no_dual_ray=None, _spec_digest=None, _solved_at=None, _model_digest=None, _run=None)
What a solve returned — the outcome, and access to any values.
Returned whatever the solve concluded: test has_primal before
reading values, or catch NoSolutionError. The
values are this result's own, so a later solve on the same model does not
rewrite them, and there is no lifetime to manage — close releases
what this result holds early, and nothing breaks without it.
An update is no exception. A result owns everything it reads — one finished
frame per declaration, its own values already laid out over the label frames
of the build it answered — so it outlives anything done to the model
afterwards: an update, another solve, model.close(). What retaining one
costs is those label frames staying alive, which matters once a caller keeps
several, as a sweep, a rolling horizon and Benders all do.
has_primal
property
¶
Whether there are values to read — what the accessors gate on.
Narrower than is_ok: a run stopped at a time limit before any
incumbent is ok with nothing to read.
kept
property
¶
How much of the session this solve kept: solver, progress or nothing.
What happened, not what was asked: keep= is a preference, and a
first solve or a structure that moved keeps nothing whatever it
requested, the solver having been loaded again. So a driver that asked
to keep progress and reads nothing back is being told its
labels moved. Advisory, like Diagnostics: no answer depends
on it.
record
property
¶
How this solve terminated, as the one row save writes for it.
The fields above in one value, and the same row a sweep keeps per slice
in record. objective is None
rather than nan where there are no values. Asking computes
model_digest once, as a save does.
solved_at
property
¶
When the solver returned, in UTC — None where the solve carried no clock.
What orders a table concatenated from runs solved apart, so that a comparison is not left reading the timestamps of the files.
spec_digest
property
¶
Which spec this answered — a digest of the file, not its name.
Two answers carrying one digest answered the same document, so a table
of saved cases says whether it is comparing like with like. The data
may differ entirely: two scenarios of one spec share this. None
where the solve ran off a lowered program, which has no document.
termination_condition
property
¶
What the solver said — optimal, infeasible, time_limit and so on.
activity ¶
The left-hand side of constraint name at the solution — (dims…, value).
dual's shape and order, and the other half of a row's story:
how far each row's Σ aᵢxᵢ sits from its bound. The solver's own
number, not a recomputation. Readable whenever there is a solution —
unlike dual it is well-defined on a mixed-integer model. On an
== row it equals the right-hand side up to solver tolerance by
construction.
| RAISES | DESCRIPTION |
|---|---|
NoSolutionError
|
The solve left no values to read. |
SpecsolveError
|
This result was closed. |
KeyError
|
No constraint is called name. |
close ¶
Release what this result holds early. Optional.
Its frames, which carry both its own values and its hold on the label
frames of the build it answered. Frames already read stay valid. Never
the model or the solver, which are the
Model's to close.
dual ¶
Shadow prices of constraint name — (dims…, value).
primal's shape and order, over constraint rows. Duals exist only
where a solver ran here: a model written to a file and solved elsewhere
never passes back through this package. Reduced costs and slacks are
not read.
| RAISES | DESCRIPTION |
|---|---|
NoSolutionError
|
The solve left no values at all. |
SpecsolveError
|
This result was closed, or it left primals but no
duals — an integer variable makes them undefined, and so does
an |
KeyError
|
No constraint is called name. |
dual_ray ¶
Constraint name's share of the certificate that this model has no solution — (dims…, value).
The one thing an infeasible solve has to say, and the only reader that
answers on one: primal, dual and activity all
raise there, because there is no solution behind them. Weight every
row by its value here and add them together, and the combined row
demands more than the columns can deliver inside their bounds — which
is the proof that nothing satisfies all of them at once. That is what
a Benders feasibility cut is built from, and it is why a driver no
longer needs a second model to ask how far from feasible a
subproblem was.
dual's shape and order. The sign is the row's own, one
convention across every sink, so a driver never asks who solved — a
sink whose solver signs the other way negates what it reads. Where
every column is held only by a lower bound of zero, as a dispatch
variable is, the bounds deliver nothing and the proof is the simpler
Σ weight * right-hand side > 0.
A certificate is computed only where it was asked for. highs
always produces one; gurobi needs {'InfUnbdInfo': 1} and
xpress needs {'presolve': 0} in solver_options, set before the
solve. A ray is live only: save writes none, and no sweep
spills one.
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
This result was closed; or the solve was not infeasible, so there is nothing to certify; or the sink produced no ray, in which case the message names the solver option that would have. |
KeyError
|
No constraint is called name. |
Example
answer.dual_ray('balance') # doctest: +SKIP shape: (4, 2) ┌──────────┬───────┐ │ snapshot ┆ value │ ╞══════════╪═══════╡ │ 0 ┆ 1.0 │ └──────────┴───────┘
evaluate ¶
The value of expression at this solution — (dims…, value).
expression is what one expressions: entry takes: a name the file
declares, an expression string, or the mapping carrying cases:
with dims: and otherwise:. It may use every name the model
declares and only those. The value is aggregated to the expression's
own dims, in declaration order, rows in label order over them —
primal's shape and order.
A declared name is served by its own reader, compiled on this call and
never lowered again, so a spec whose expressions go unread compiles
none of them. Anything else lowers the spec as written, which costs
what check costs. An undeclared expression names nothing, so it is
not a kind: save does not write it and a sweep does not spill it.
To keep a quantity, declare it under expressions:.
| RAISES | DESCRIPTION |
|---|---|
NoSolutionError
|
The solve left no values to read. |
SpecsolveError
|
This result was closed; the model was built from an
already-lowered |
LanguageError
|
A construct outside the language, or a name the spec does not declare — a new parameter is a build, not a read. |
model_digest ¶
Which model this answered — the document and the data it was attached to.
spec_digest names the document alone, so two scenarios of one
spec share that and differ here. Computed on the first ask and kept,
which is what keeps a solve that never asks free of it.
primal ¶
The tidy solution of variable name — (dims…, value).
Rows come back in label order, row-major over the variable's coordinate product, so two reads and two runs agree.
| RAISES | DESCRIPTION |
|---|---|
NoSolutionError
|
The solve left no values to read. |
SpecsolveError
|
This result was closed. |
KeyError
|
No variable is called name. |
save ¶
Every kind this solve answered with, one file per name, into directory.
record.parquet holds the
Record — how the solve terminated
and what it reached, in the columns a sweep keys and folds. A solve
that reached no objective writes null there rather than nan, so a
directory per case is a table an aggregate reads. Then
primal/<name>.parquet for every variable, dual/<name>.parquet
for every constraint where the duals are defined, and
expression/<name>.parquet for every named expression this data
can evaluate — an integer variable leaves the duals out, and an
expression that fails on this data is left out, evaluate
still saying why. The primals are streamed to disk in
primal's order, so the same model and data write the same
bytes.
activity/<name>.parquet goes beside them for every constraint,
which no kind= names — a sweep folds three kinds and never holds
these, so a saved result carries them under a name of their own.
reasons.parquet holds (kind, name, reason) for whatever is
deliberately not here, and is absent when everything is: one row per
expression that failed, and one with an empty name for the duals,
whose absence is never per-constraint. Written because a directory
that simply lacks a file cannot tell "there is none, and here is why"
from "no such name", which is the one thing dual and
evaluate do say.
format.json stamps the directory with the layout it is written in
and the specsolve that wrote it: {"layout": 1, "specsolve": "…"}.
A release that changes what a result, a sweep or an archive writes
raises the layout, and its notes say so. Nothing reads another layout
back, and an answer 0.1.0 or earlier wrote carries none, so every
reader refuses it with a LayoutError
that says to solve the model again and save it.
A solve that left no values writes the record and nothing else. A run that came back infeasible is an answer a set of saved cases needs on disk, rather than a directory that does not exist.
The directory holds this answer and no other. Whatever a previous save left there is removed first, so a re-run cannot leave one model's frames beside another's record. Files that are not part of the layout are left alone.
| RETURNS | DESCRIPTION |
|---|---|
Path
|
The directory. |
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
This result was closed. |
to_dataarray ¶
One name's values as a labelled xarray.DataArray, to_pandas's arguments.
Dense over the name's dims: a masked coordinate comes back NaN.
to_dataset ¶
The named values of one kind as one xarray.Dataset; all of that kind by default.
One kind per call: a dual and a variable of the same name would collide, and mean something else per row. Each arrives dense over its own dims, all at once — on a large model name the few you need.
| PARAMETER | DESCRIPTION |
|---|---|
names
|
What to include; none means every name of kind.
TYPE:
|
kind
|
TYPE:
|
to_pandas ¶
One name's values as a tidy pandas.DataFrame.
Needs pandas, which specsolve does not install; the xarray bridges need xarray too.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
A variable, a constraint or a named expression, as kind says.
TYPE:
|
kind
|
TYPE:
|
Sweep
dataclass
¶
Sweep(key_name, record, metrics, _primals=dict(), _duals=dict(), _expressions=dict(), _no_duals=None, _no_expressions=dict(), _original=None, _hand_built=False, _spill=None, _evaluate=None)
What a fold returned: frames keyed by slice, never a scalar.
Result's readers one dimension wider —
same names, same shapes, the slice key prepended. Nothing is combined
across slices: each row says which slice computed it. A windowed sweep
reads over that key unless a reader asks original_index=True, which
gives the dimension the axis sliced and drops the lookahead rows every
overlapping window recomputed.
metrics
instance-attribute
¶
One SliceMetrics per slice, keyed
and in slice order — diagnostics one dimension
wider, its counts and clocks only. loaded says the solver took the
model from scratch: under a serial fold the first slice does and the
rest are pushed values, so a later True is a slice whose data moved
a mask; under an executor every slice builds alone and every one loads.
The _seconds columns are this slice's own share, so a slow sweep
says which slice, and which phase of it.
record
instance-attribute
¶
One Record per slice, the key
column first, in slice order — how every slice terminated, whether or
not it produced an answer. A slice that reached no objective holds null
there rather than nan, so the column aggregates over the slices that
solved.
dual ¶
One constraint's shadow prices across every slice, the key prepended.
primal's shape and arguments. A slice whose model had an
integer variable contributes no duals; over the original index each
coordinate carries the price of the window that owns it, never a blend
of several.
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
No slice produced duals for name — the message says which of the two it was. |
evaluate ¶
The value of expression at every slice's solution, the slice key prepended.
evaluate one dimension wider,
and primal's shape and arguments. expression is what one
expressions: entry takes: a name the file declares, an expression
string, or the mapping carrying cases: with dims: and
otherwise:.
A declared name was valued at each slice's solution when the fold read
it, so it is stitched from what the sweep holds, live or off disk, and
never lowered again. Anything else is valued at each slice's own
solution with no re-solve: the slice's model is rebuilt from the
archive's spec and that slice's cut of the sources, and its saved
primal put back against it — so it is available on the sweep
load_archive hands back, which carries the
spec, sources and axis, and a Sweep a live solve returned says it retains
no model. It reads only what an archive can put back: an expression over
a parameter the sweep carried is refused, that value being a
previous slice's answer rather than stored data.
Over the original index each coordinate carries the value of the window that owns it — the recomputed lookahead rows are dropped, which is what makes summing the stitched frame safe where summing per-window values double-counts.
| PARAMETER | DESCRIPTION |
|---|---|
expression
|
A declared name, an expression string, or the
TYPE:
|
original_index
|
Read over the dimension the axis sliced instead of over the slice key.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
No slice produced a declared expression — an
evaluation that failed on every slice carries its own reason —
a spilled sweep, which |
LanguageError
|
A construct outside the language, or a name the spec does not declare. |
primal ¶
One variable's values across every slice, the slice key prepended.
A slice that reached no solution contributes no rows, so this can be
shorter than the sweep; record is one row per slice always.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
A variable the sweep's spec declares.
TYPE:
|
original_index
|
Read over the dimension the axis sliced instead of over the slice key.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
No slice of the sweep produced name, or
|
save ¶
Everything the sweep holds, written as spill_to= would have written it.
The same layout: <kind>/<name>/<position>.parquet for every
primal, dual and expression, the slice key a column of each, with
record/, metrics/ and the manifest beside them. So the
directory is a spilled sweep: scan reads it, and the call
that made this sweep, pointed at it with spill_to=, reads it back
without solving a slice.
| RETURNS | DESCRIPTION |
|---|---|
Path
|
The directory. |
A sweep whose every slice terminated without values writes each slice's record and no frames, as one such solve does, rather than refusing.
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
The sweep is spilled — its frames are in a directory already. |
scan ¶
One name's values across every slice as a polars.LazyFrame, the slice key prepended.
The reader for a sweep solved with spill_to=, whose frames are on disk;
on one held in memory it is primal, dual or
evaluate made lazy, so the same line reads either.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
A variable, a constraint or a named expression the spec declares, as kind says.
TYPE:
|
kind
|
TYPE:
|
original_index
|
Read over the dimension the axis sliced instead of over the slice key.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
No slice produced name, or a kind that names no reader. |
to_dataarray ¶
One name's values as a xarray.DataArray, the slice key a dimension; to_pandas's arguments.
The extra dimension is named by the axis — a scenario sweep gives
(scenario, …) and a window (<dim>_start, …). A slice that
reached no solution has no rows and comes back NaN, the same answer a
masked coordinate gets from Result. original_index=True gives
the array over the dimension the axis sliced instead, so a rolling
horizon's dispatch, or its price, comes back indexed by time.
to_dataset ¶
The named values of one kind as one xarray.Dataset; all of that kind by default.
One kind per call: a dual and a variable of the same name would
collide, and mean something else per row. Name the few you need, or
use save, which writes every kind.
No original_index: this and save export what the sweep
holds, lookahead rows included.
| PARAMETER | DESCRIPTION |
|---|---|
names
|
What to include; none means every name of kind some slice produced.
TYPE:
|
kind
|
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
The sweep holds no values of kind at all, or is spilled — its frames are on disk already. |
to_pandas ¶
One name's values across every slice as a tidy pandas.DataFrame.
The name is resolved before pandas is imported, so a sweep that never held name says so on any install.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
A variable, a constraint or a named expression, as kind says.
TYPE:
|
kind
|
TYPE:
|
original_index
|
Read over the dimension the axis sliced instead of over the slice key.
TYPE:
|
The rows and frames those hand back: how a solve terminated, what the build and its solves took, and what a slice of a sweep took.
Diagnostics
dataclass
¶
Diagnostics(columns, rows, nonzeros, omissions, sparse_parameters, coefficient_range, bound_range, rhs_range, objective_range, solves, loads, seconds)
What a build and its solves did that the answer does not show.
Advisory, all of it: no answer depends on any field. Read them when a loop is slower or smaller than it should be.
bound_range
instance-attribute
¶
(variable, smallest, largest) — the bound magnitudes each variable
block put on its columns, one row per block that declared a finite one.
The axis a solver reports and does not repair: HiGHS prints a Bound
range beside its Matrix one, equilibrates the matrix automatically,
and answers the bounds with Consider scaling the bounds by … — so a
model can be clean on coefficient_range and still be the one the
solver is complaining about. Zero and infinity are excluded, an
unbounded side and a lower: 0 being nothing the solver represents. A
large largest is usually a big number standing in for "uncapped", and
wants no upper bound at all rather than a rounder one.
coefficient_range
instance-attribute
¶
(constraint, smallest, largest) — the coefficient magnitudes each
constraint block put in the matrix, one row per block that kept a term,
in build order. A solver's own Matrix range line answers this for the
whole model; what it cannot say, and what a caller can act on, is which
declaration holds the outlier. largest / smallest over the frame is
the conditioning to compare against the solver's. A block whose every row
went (the absence rules) has no entry, the same way it has no rows.
columns
instance-attribute
¶
The shape the build produced: columns, rows, and matrix entries. The thing to report when a model is bigger than its author expected — a broadcast that multiplied rows shows up here first.
objective_range
instance-attribute
¶
The same pair for the objective's coefficients, or None where the
spec declares no objective and where every term of one cancelled.
omissions
instance-attribute
¶
(constraint, rows_not_built) — every declared row that did not reach
the solver (the absence rules), by either route: one emptied of all its
terms, and one a propagated absence deleted while its other terms were
still live. Empty for a model whose every declared row was built — a
recurrence's first coordinate counting as a row it declared and did not
get, so a shift against the horizon's edge reports here and is the
boundary rather than a fault. Counts rather than coordinates: the label
of an unbuilt row does not exist.
rhs_range
instance-attribute
¶
(constraint, smallest, largest) — the same for each block's
right-hand sides, over the rows that survived. The fourth of the four
ranges a solver reports, and the last of them this can answer per
declaration rather than per model.
seconds
instance-attribute
¶
Cumulative wall-clock seconds per phase, keyed by the phase's name:
attach (the caller's sources onto the plan), build (declarations
into the model frames), handoff (the built model into a solver),
solve (the solver's own run), write (the built model to a
file). A phase that never ran has no key; one that ran again holds the
sum — an update's attach and build land on top of the first's, the way
solves keeps counting. Clocks rather than a profile: enough to say
which phase a slow loop spends its time in, not why.
solves
instance-attribute
¶
How many times this model has been solved, and how many of those solves
loaded the solver from scratch instead of pushing values onto one that
already held it. Read together: loads == 1 is a driver on the fast
path — the first solve had nothing to keep — and loads == solves on
an iterating driver is the difference between "specsolve is slow" and
"this model masks on a parameter that varies", unless the driver asked
for keep='nothing', which loads by construction. loads ticks on
exactly the solves that report Result.kept of nothing —
the same event, counted here and named there.
sparse_parameters
instance-attribute
¶
(parameter, coordinates, rows, missing) — one row per parameter whose
source is short of the coordinates its dims reach, in declaration order,
and empty where every one is complete. Sparsity is the ordinary case
here — absence is how a model masks — so this reports it rather than
judging it: what a missing row means is the absence rules', and whether
it was meant is the caller's to say.
A parameter over no dims has one coordinate and attaching already refuses a source that does not carry exactly one row for it, so it is never here.
metrics ¶
The sizes, counters and clocks as one value — the row an archive records.
What archive= records beside the answer, and what a caller feeding
its own store reads off a model it solved. Which fields reach it and
what it means cumulatively are
Metrics's to say; a phase this
build never entered reads zero there. run is null: the name is the
publisher's, and nothing has published this yet.
Record ¶
How a solve terminated, what it reached, and which spec it answered.
One row per solve, and the same columns whoever wrote them: a result writes one, a sweep one per slice keyed by its own key. The only part of an answer the frames themselves cannot carry — a run that left no values writes this and nothing else.
has_primal
instance-attribute
¶
Whether the solve produced values, which the condition alone does not
say: a run stopped at a limit before any incumbent is ok with
nothing to read.
model_digest
class-attribute
instance-attribute
¶
A digest of the model this answered — the spec and its data, where
spec_digest is the document alone. None for an answer written before this column, and
for one whose result was never asked for it.
objective
instance-attribute
¶
What the solve reached, or None where it reached nothing. Null
rather than nan: nan is a number to every aggregate that meets it.
Result.objective is a float and reads it back as nan, having
no null to return.
run
class-attribute
instance-attribute
¶
What the archive holding this answer was called — its file name without
a .zip, so runs/nightly-2026-09-10.zip writes
nightly-2026-09-10 and a directory called case.v2 keeps both
halves of its name. Stamped when the archive is written and null until
then.
solve_status
property
¶
The status this row records — the way back from columns.
The solver's own wording is gone, and status is derived again
rather than read off the row.
solved_at
class-attribute
instance-attribute
¶
When the solver returned, in UTC. None for a solve that carried no
clock — a result built by hand, or read back from a record written
before this column.
spec_digest
instance-attribute
¶
A digest of the spec this answered, or None where the solve
was run off a lowered program and there was no document to digest. Null
on disk, never an empty string.
of
classmethod
¶
The row a solve that terminated this way writes.
status is derived here rather than passed, and an objective is
dropped to null here rather than at each writer.
| PARAMETER | DESCRIPTION |
|---|---|
termination_condition
|
What the solver said.
TYPE:
|
objective
|
What the solve reached. Written only where there are
values to read —
TYPE:
|
has_primal
|
Whether there are values, which the condition alone does not say.
TYPE:
|
spec_digest
|
A digest of the spec answered, or
TYPE:
|
solved_at
|
When the solver returned, in UTC.
TYPE:
|
model_digest
|
The built model's digest, or
TYPE:
|
Metrics ¶
What a build and its solves took, as the row an archive records beside the answer.
Record's sibling — one says how the solve terminated, this is the
measure of what it took — and the same columns whoever writes them, so
rows written by runs that never met concatenate into one table.
The scalars of Diagnostics and none of
its frames: a coefficient range is a table per declaration, which does not
fold into a row beside a count.
Cumulative over the model's life, as every counter it is read off is.
solves says how many solves the clocks cover; it reads 1 for
the archive specsolve.solve writes, that verb building the model it
solves.
attach_seconds
instance-attribute
¶
Wall-clock seconds in each phase a build clocks, in the order they run: the caller's sources onto the plan, the declarations into the model frames, the built model into a solver, the solver's own run, and the built model streamed to an LP or MPS file. A phase that never ran writes zero rather than no column.
So write_seconds reads zero on an archive whose caller never
asked for a file, which is most of them: it is
write's clock rather than the archive's own. What writing the archive cost is
not here and is not anywhere: a caller who wants that number times the
call.
run
class-attribute
instance-attribute
¶
What the archive holding this row was called, as Record.run is
stamped onto the record beside it: the archive's file name without a
.zip. Null until one is written.
solves
instance-attribute
¶
How many solves the row covers, and how many of those loaded the solver from scratch. Read together with the clocks, which are cumulative over exactly these solves.
SliceMetrics ¶
What one slice of a sweep took — Metrics one dimension in.
Not the same columns, and the fold is what separates them. A slice's clocks
are its own share rather than a cumulative total; loaded says whether
the solver took this slice from scratch, where a whole model counts its
loads; and what a sink added, how many solves ran and what a file write
took are facts about a model's life that one slice of a sweep has no share
of.
Written per slice by the spill and read back as one table, so a sweep's every slice concatenates the way a directory of archives does.
attach_seconds
instance-attribute
¶
This slice's own seconds per phase, so a slow sweep says which slice and
which phase of it. A whole model's write has no per-slice meaning —
a sweep writes no file per slice — and there is no column for it.
loaded
instance-attribute
¶
Whether the solver took this slice's model from scratch instead of
having values pushed onto one it already held. Under a serial fold the
first slice does and the rest do not, so a later True is a slice
whose data moved a mask; under an executor every slice loads.
Carry an answer¶
SolveArchive
dataclass
¶
A spec, the data it was solved with, and what one solve of it returned.
sps.solve(archive.spec, archive.sources) asks the question again.
| ATTRIBUTE | DESCRIPTION |
|---|---|
spec |
The spec as written, read back as one
TYPE:
|
sources |
What was attached, keyed as the file declares it: a table
from
TYPE:
|
answer |
What came back.
TYPE:
|
source_digests |
TYPE:
|
metrics |
What reaching the answer took, as one
TYPE:
|
SweepArchive
dataclass
¶
A spec, the data a sweep was solved over, the axis that cut it, and what came back.
sps.solve_over(sweep.spec, sweep.sources, sweep.axis, carry=sweep.carry)
runs it again.
| ATTRIBUTE | DESCRIPTION |
|---|---|
spec |
The spec as written.
TYPE:
|
sources |
What the sweep was given, uncut. A table or a path, as
TYPE:
|
axis |
What cut them.
TYPE:
|
carry |
TYPE:
|
answer |
Every slice's answer, keyed by slice. Held from
TYPE:
|
source_digests |
As
TYPE:
|
load_archive ¶
Read an archive back whole: the sources as tables, the answer's frames in memory.
| PARAMETER | DESCRIPTION |
|---|---|
path
|
The archive, a
TYPE:
|
into
|
Where to unpack a zip, kept afterwards, for a caller who wants the extracted tree as well. Without it a zip unpacks to a scratch directory that is gone when this returns. Refused for a directory archive, which is read where it lies.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
SolveArchive | SweepArchive
|
A |
SolveArchive | SweepArchive
|
|
| RAISES | DESCRIPTION |
|---|---|
LanguageError
|
A |
LayoutError
|
A member outside the layout, an into given for a directory, or an answer whose layout has moved since it was written. |
SpecsolveError
|
An answer that names a different spec than the one beside it. |
BadZipFile
|
A file that is not a zip archive. |
load_result ¶
Read back an answer Result.save wrote — a solve, off disk.
Every reader answers what it answered in the session that solved: the
values, the duals and activities, each named expression, and the reason
behind anything the solve could not produce. A Result is frames and a
few scalars, so none of it needs the build that made it or the solver
that filled it — which is what makes an archived answer comparable with
one solved today.
Two things do not come back, both being facts about a session rather than
about an answer: kept reads
nothing, this result holding no solver, and the solver's verbatim
wording behind a refusal is not recorded — the termination condition is. A
solve that reached no objective wrote null and reads back as nan,
which is what objective has to
return, being a float.
| PARAMETER | DESCRIPTION |
|---|---|
directory
|
Where
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Result
|
The result, read whole: the frames are in memory when this returns, so |
Result
|
it owes directory nothing. |
Result
|
on disk. |
| RAISES | DESCRIPTION |
|---|---|
LayoutError
|
A directory holding no |
load_sweep ¶
Read back a sweep Sweep.save wrote, or one solve_over(spill_to=) spilled.
The sweep comes back held: every slice's frames are in memory when this
returns, so it is the value a sweep solved without spill_to= is —
Sweep.primal, Sweep.to_dataset and Sweep.save all
answer, and it owes directory nothing afterwards. A sweep larger than
memory is scan_sweep instead.
Sweep.record and Sweep.metrics are one row per slice
either way, and original_index works on both, the manifest carrying the
dimension a window sliced.
| PARAMETER | DESCRIPTION |
|---|---|
directory
|
Where the sweep was written.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Sweep
|
The sweep, keyed as it was solved. |
| RAISES | DESCRIPTION |
|---|---|
LayoutError
|
A directory holding no |
scan_archive ¶
Read an archive back off disk: the sources as paths, each frame read at the call that asks for it.
The members have to outlive the value, so into is required for a zip
and kept. The same values and the same errors as load_archive,
and LayoutError for a zip with no into.
scan_result ¶
The answer under directory, read as its readers are called rather than now.
load_result's other half, and the same value: every reader answers
what that one's does. What differs is when the bytes move — each frame is
a polars.scan_parquet of the file it lies in, so an answer far
larger than memory is readable a name at a time, and one whose names go
unread costs nothing to open.
The files stay where they are, so they have to outlive the result: a name read after the directory is gone raises where the scan is collected, and a file rewritten underneath it comes back changed.
| PARAMETER | DESCRIPTION |
|---|---|
directory
|
As
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
LayoutError
|
As |
scan_sweep ¶
The sweep under directory, its frames left where they lie.
load_sweep's other half, and the value a sweep solved with
spill_to= already is: nothing but the record is read, and
Sweep.scan reads a name back as a polars.LazyFrame when one
is asked for. That is the reader for a sweep too large to hold, and it
costs the frame readers: Sweep.primal and its siblings refuse,
naming Sweep.scan.
directory has to outlive the sweep, the frames being read off it as they are asked for.
| PARAMETER | DESCRIPTION |
|---|---|
directory
|
As
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
LayoutError
|
As |
Errors and warnings¶
Every error is one tree, rooted at SpecsolveError. A spec the language
accepts and specsolve cannot build raises SpecsolveError itself, and its
message names the rewrite. LanguageError, with SchemaError and
DimensionError, is a fault in the spec, and is the language's own:
which error you get.
SpecsolveError
module-attribute
¶
The root, under the name callers catch it by. An alias and not a subclass:
except sps.SpecsolveError has to catch a LanguageError.
LanguageError ¶
The spec is not sayable in the language, or does not obey its rules.
SchemaError ¶
What a load refuses: an unknown key, a bad dtype, a duplicate YAML key, an unparseable or unresolvable expression.
DimensionError ¶
A dim-set rule was violated. Raised at load time, before any data.
The rest are specsolve's:
DataError ¶
Data attached to a valid spec is missing or the wrong shape.
LayoutError ¶
What is on disk is not a layout this package reads.
The target is a directory or archive that save wrote, or did not. The
fix is which path was named, or re-solving a model whose layout has moved
since it was written. The layout is the one
save stamps.
NoSolutionError ¶
The solve returned no values to read — infeasible, unbounded, errored.
A scenario sweep catches this and records the outcome; a
LanguageError instead means the file needs editing.
SpecsolveWarning ¶
Advice from check: the spec loads and solves, and reads wrong.
Raised for a spec that is still part-written, where an expression has not yet reached what it declares.
Rules across the verbs¶
What no single entry above holds, because every verb keeps it.
Names that differ only by case¶
Two declarations of one namespace whose names differ only by case are
refused, whichever verb lowers the spec. Every declaration is written to
disk as a file named after it, and a case-insensitive filesystem, which a
stock macOS or Windows volume is, folds p and P into one file.
variable 'P' and variable 'p' differ only by case, and one answer on disk
cannot hold both: ... Tell them apart by a suffix rather than a capital:
'p_rated' beside 'p'.
The namespaces are the language's own: one flat namespace holding dimensions,
relations, parameters, variables and named expressions, and constraints beside
it. A constraint may carry a variable's name already, so a constraint P
beside a variable p is accepted. The two are written under dual/ and
primal/, which nothing folds together.
What each sink takes¶
check(spec, sink=...) asks whether a sink takes a spec, and solve and
write read the same table, so a refusal comes whether or not it was asked
for. Where a spec can land is
a separate question
from whether it is sayable. The four quadratic rows, and the two sections
HiGHS writes but will not read back, are probed against the shipped solvers by
tests/test_sink_capability_probes.py and
tests/test_gurobi_capability_probes.py. The rest are read off the APIs.
lp_file |
mps_file |
HiGHS direct | Gurobi direct | Xpress direct | |
|---|---|---|---|---|---|
| affine rows, COO, integrality | text | text, MARKER |
native | native | native |
| semi-continuous | text | not written — no SC bound |
kSemiContinuous |
native | native |
| SOS1 / SOS2 | text section | SOS section |
no concept — refused, naming Spec.expand() |
addSOS |
native |
| indicator | text section | not written | no concept | addGenConstrIndicator |
native |
| convex quadratic objective | text section | not written | passHessian |
setMObjective |
no path here |
| nonconvex quadratic objective | text section | not written | refused | native, at default parameters | no path here |
| quadratic objective and integrality | text section | not written | refused | native (MIQP) | no path here |
| quadratic constraint | text section, unreadable | not written | no concept | addQConstr |
no path here |
- HiGHS excludes quadratic twice: by convexity, and by conjunction with integrality.
- The
lp_filecolumn says what can be written, not what reads back. The same HiGHS parser takes the quadratic-objective section and refuses thesosand quadratic-constraint sections. - "No path here" describes this package, not Xpress. The Optimizer takes
a Hessian; the sink in
solvers/xpress.pynever hands it one.