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. Three modules hold them:
specsolveholds what you call;specsolve.typesholds what a call hands back;specsolve.errorsholds what a call raises or warns.
Any other name under specsolve. is internal, and any release can change it.
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.
Every other verb reads the spec through this, so what this refuses they
refuse too. Whether a sink takes the model is a fact about the build, and
Model.check answers it with no solve.
| PARAMETER | DESCRIPTION |
|---|---|
spec
|
A YAML path, a mapping, or a
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Program
|
The lowered program, for reading the plan; typeset it or read its |
Program
|
declarations through |
Program
|
|
| RAISES | DESCRIPTION |
|---|---|
LanguageError
|
A construct outside the streaming language, a
|
SpecsolveError
|
Two declarations whose names differ only by case, or a
name that starts with |
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 shapes a value may take, and what attaching refuses, are the data contract.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Model
|
The built model. |
| RAISES | DESCRIPTION |
|---|---|
LanguageError
|
A construct outside the streaming language. |
DataError
|
A source that is missing, unreadable, or the wrong shape. |
solve ¶
solve(spec, sources, solver_name='highs', *, solver_options=None, record_options=None, archive=None)
Build spec and solve it in one call.
To solve the same spec again with new numbers, use build and
Model.update. There is no keep: the solve is the first of the
model's life, so kept is
always nothing.
| PARAMETER | DESCRIPTION |
|---|---|
spec
|
As
TYPE:
|
sources
|
As
TYPE:
|
solver_name
|
As
TYPE:
|
solver_options
|
As
TYPE:
|
record_options
|
As
TYPE:
|
archive
|
As
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Result
|
The solution. It owns its frames; the model and the solver are |
Result
|
released before this returns. |
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
A solver name nothing serves — checked before the build. |
write ¶
Build spec and stream it to out, in the format its 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, read off the built model, naming the sinks that take it. |
evaluate ¶
The value of expression over a spec with no variables — arithmetic, no solver.
A spec with only dimensions, parameters, relations and expressions:
is a calculation: each expression reads only the attached data. This
attaches sources and values one expression, the way
evaluate does at a
solution. The spec is read as solve reads it; one that declares
variables belongs there.
| PARAMETER | DESCRIPTION |
|---|---|
spec
|
As
TYPE:
|
sources
|
As
TYPE:
|
expression
|
What one
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DataFrame
|
The value, |
DataFrame
|
this expression is compiled. |
| 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. |
DataError
|
A source that is missing, unreadable, or the wrong shape, or a divisor with no value where the expression divides. |
tidy ¶
The tables a solve of spec attaches from sources, one per name the spec declares.
Every source is read and checked as build does. The tables are what
an archive holds under sources/, less its specsolve_run column, so
a returned table goes back in as a source unchanged.
| PARAMETER | DESCRIPTION |
|---|---|
spec
|
As
TYPE:
|
sources
|
As
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
dict[str, DataFrame]
|
Each dimension as |
dict[str, DataFrame]
|
and the |
dict[str, DataFrame]
|
parameter as |
dict[str, DataFrame]
|
declares. |
| RAISES | DESCRIPTION |
|---|---|
LanguageError
|
A construct outside the streaming language. |
DataError
|
As |
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, record_options=None, solver_name='highs', keep='solver', spill_to=None, archive=None, keep_windows=False)
Solve spec once per slice of axis and fold the answers together.
The rules 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:
|
record_options
|
As
TYPE:
|
solver_name
|
As
TYPE:
|
keep
|
As
TYPE:
|
spill_to
|
A directory each slice's frames are written to as the fold
goes, so the sweep holds one slice in memory however many there
are;
TYPE:
|
archive
|
Where to write the model, the sources the sweep was cut from,
the axis that cut them and every slice's answer, so that
TYPE:
|
keep_windows
|
Also archive an
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Sweep
|
The sweep, which reads its answer. |
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
Before a slice is taken: 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; keep_windows without archive or on an axis that does not cut windows — each answerable from the declarations alone; keys of more than one type, or two keys of one text. |
DataError
|
No source carries the axis, an index of another dimension carries it, 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.
Parameters and relations carrying dim are filtered to one coordinate and
the column dropped, so the model never mentions it; a dim the spec
declares is refused, and so is an index that carries it. Every other
source passes through untouched. Slices run in sorted coordinate order,
which is the order a carry chains them in.
EachWindow
dataclass
¶
One slice per window of consecutive coordinates of dim.
Each window keeps steps coordinates and sees lookahead beyond them,
so a lookahead above zero is overlap. An int keeps the same number
every window; a sequence keeps those numbers in order, for a telescoping
horizon or a month at a time. Both count coordinates, not values, so dim
need only be orderable — datetimes, strings and gapped integers all work.
dim is re-indexed into a dense 0..n-1 column named into, 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]).
The pairs carry no ownership: solved as a list, the slices need
key_name=, the answer is keyed by slice rather than stitched, and a
carry cannot collapse a dimension.
What comes back¶
Model ¶
A spec with your data attached to it — what build returns.
check → Program (the math) → build → Model (the math with
your data) → solve → Result (one answer).
One build feeds any number of solve and write calls;
update puts new numbers on it without re-reading the YAML, and
diagnostics says what it did. Nothing has to be released;
close hands a large model back early.
check ¶
Refuse the built model where sink cannot take it; no solve, no file.
::
sps.build('dispatch.yaml', sources).check('highs')
The answer is read off the built model, not the file: a square the
data prices at zero, an integer variable with no built column or a set
with no members asks for nothing. solve and write refuse
exactly what this refuses, with the same message.
| PARAMETER | DESCRIPTION |
|---|---|
sink
|
A solver name (
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
A construct the sink cannot take, naming it and the sinks that do; or a name belonging to no sink. |
diagnostics ¶
What this build and its solves did that the answer does not show.
Answerable after close, and after a build that raised. A raise
leaves the sizes at zero, since they are taken once a model is whole,
and everything measured before it stands.
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. It reads the
built model and needs no solve: a term whose variable was absent
is missing, and a row a where masked out is not there at all. 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 a dimension may be
called
TYPE:
|
coordinate
|
One label per dim of that constraint, all of them.
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 ¶
solve(solver_name='highs', *, solver_options=None, record_options=None, keep='solver', archive=None)
Hand the built model to a solver and solve it.
A solver that can stay loaded is kept between calls, so an updated
model pushes only its numbers. How much this solve 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:
|
record_options
|
More option names whose value the result records, in any letter case. Name no credential here: an archive goes to shared storage.
TYPE:
|
keep
|
TYPE:
|
archive
|
Where to write 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, a keep other than those three, or a bare string as record_options. |
LayoutError
|
An archive directory that already holds something, refused before the solve. |
update ¶
Put new numbers on the same model, in place.
::
model.update({'cap_hat': capacity}).solve()
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 rather than pushed onto a loaded solver;
loads says which ran.
Results taken before the update keep reading their own frames, and
keep their build's label frames alive until dropped or until
close is called. A sweep, a
rolling horizon or a myopic pathway is
solve_over, which runs this loop.
| PARAMETER | DESCRIPTION |
|---|---|
sources
|
Only what changed; the rest keeps what
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Model
|
This object. |
| RAISES | DESCRIPTION |
|---|---|
DataError
|
A name the spec does not declare, refused before anything changes. Data the build refuses 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, as
|
ConstraintRow
dataclass
¶
One built constraint row, spelled back out — what row returns.
The row at one coordinate as the model built it, read off the built model,
so it needs no solve. where masking, absent variables and coefficients
the data made exactly zero have already removed their terms, so it can be
shorter than the file suggests.
Printed, it is one line of math in linopy's format; a row wider than
display_terms prints each variable's term count and coefficient
span instead. terms is the same content as a frame.
| 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, _provenance=NO_PROVENANCE)
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.
A result owns its values, so it outlives an update, another solve or
model.close(). It keeps the label frames of the build it answered
alive until close, which is optional.
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: a first solve or a structure that
moved keeps nothing whatever keep= requested, so a driver that
asked for progress and reads nothing is being told its labels
moved. Advisory, like Diagnostics: no answer depends on it.
provenance
property
¶
The solver, its options and the package versions that produced this answer.
Read back as it was written, so an answer loaded from an archive names the environment that solved it rather than the one reading it.
record
property
¶
How this solve terminated, as the one row save writes for it.
A sweep keeps the same row per slice in record.
objective is None rather than nan where there are no
values. Asking computes model_digest, as a save does.
solved_at
property
¶
When the solver returned, in UTC — None where the solve carried no clock.
spec_digest
property
¶
Which spec this answered — a digest of the file, not its name.
Two answers with one digest answered the same document, perhaps over
other data. None where the solve ran off a lowered program.
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.
The solver's own number, not a recomputation, and readable whenever there is a solution, a mixed-integer one included.
| RAISES | DESCRIPTION |
|---|---|
NoSolutionError
|
The solve left no values to read. |
SpecsolveError
|
This result was closed. |
KeyError
|
No constraint is called name. |
close ¶
Release this result's frames, and its hold on the build's label frames, early. Optional.
Frames already read stay valid. The model and the solver are the
Model's to close.
dual ¶
Shadow prices of constraint name — (dims…, value), primal's shape and order.
Each is the rate at which the optimal objective rises as the row's
right side rises: of lhs <= rhs, the rate in d of
lhs <= rhs + d. That is mathspec's dual(c) for every
comparator, sense and sink, so which side a term is written on decides
the sign.
Duals exist only where a solver ran here, not for a model written to a file and solved elsewhere. 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 only reader that answers on an infeasible solve, where
primal, dual and activity all raise. Weight every row
by its value here and add them, and the combined row demands more than
the columns can deliver inside their bounds: the proof that nothing
satisfies all of them at once, and what a Benders feasibility cut is
built from.
dual's shape and order, and the row's own sign on every sink.
Where every column is held only by a lower bound of zero, the proof is
Σ weight * right-hand side > 0.
highs always produces a certificate; 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; the solve was not infeasible; or the sink produced no ray, in which case the message names the solver option that would have. |
KeyError
|
No constraint is called name. |
evaluate ¶
The value of expression at this solution — (dims…, value), primal's shape and order.
expression is what one expressions: entry takes: a name the file
declares, an expression string, or the mapping carrying cases:
with dims: and otherwise:. The value is aggregated to the
expression's own dims, in declaration order.
Anything but a declared name lowers the spec as written, which costs
what check costs. save writes, and a sweep spills, only the
expressions declared under expressions:.
| RAISES | DESCRIPTION |
|---|---|
NoSolutionError
|
The solve left no values to read. |
SpecsolveError
|
This result was closed; an undeclared expression
on a model built from a 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.
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, with a null
rather than nan objective where none was reached. Beside it are
primal/<name>.parquet per variable, dual/<name>.parquet per
constraint where the duals are defined, activity/<name>.parquet
per constraint, and expression/<name>.parquet per named expression
this data can evaluate. reasons.parquet holds
(kind, name, reason) for whatever is deliberately left out — one
row per failed expression, one with an empty name for the duals —
and is absent when nothing is. A solve that left no values writes the
record alone. The same model and data write the same bytes.
format.json stamps the layout and the specsolve that wrote it:
{"layout": 3, "specsolve": "…"}. Every reader refuses another
layout with a LayoutError that says
to solve the model again and save it.
Whatever a previous save left in directory is removed first; files outside 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; every name of that kind where none is given.
Each arrives dense over its own dims, all at once — on a large model name the few you need.
to_pandas ¶
One name's values as a tidy pandas.DataFrame.
name is read through the reader kind names: primal, dual
or expression. Needs pandas, which specsolve does not install; the
xarray bridges need xarray too.
Sweep
dataclass
¶
Sweep(key_name, record, metrics, _slices=dict(), _no_duals=None, _absent=dict(), _stitch=None, _spill=None, _answer=None, _windows=True, _evaluate=None)
What a fold returned: the answer over the model's own coordinates, and a record per slice.
Result's readers, same names and
shapes, and each returns the answer by default. An EachWindow
sweep is read over the dimension it sliced: each coordinate comes from
the window that owns it, and the lookahead rows are dropped. An
EachCoordinate or hand-built sweep is keyed by slice, the key
prepended, each slice being a whole answer. Nothing is combined across
slices.
per_window=True reads an EachWindow sweep one window at a time:
keyed by where each window started, over the index inside it, lookahead
rows included.
metrics
instance-attribute
¶
One Metrics per slice, keyed as
record is and in slice order — diagnostics
one dimension wider, its counts and clocks only. Each row is the slice's
own share: solves is 1, and loads is 1 where the solver
took the model from scratch. Under a serial fold the first slice does
and the rest are pushed values, so a later 1 is a slice whose data
moved a mask; under an executor every slice builds alone and every one
loads. So a slow sweep says which slice, and which phase of it.
record
instance-attribute
¶
One Record per slice, in slice
order — how every slice terminated, whether or not it produced an
answer. The key column comes first, as its own type, so the table joins
to the frames; slice_axis and slice name the slice again as
text, and are what a saved sweep or an archive writes in its place. 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.
primal's shape and arguments. A slice whose model had an
integer variable contributes no duals. In the answer of an EachWindow
sweep 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 — or as |
evaluate ¶
The value of expression at every slice's solution, as an answer.
evaluate over the
sweep, with primal's shape. A declared name is read from what
the sweep holds, live or off disk. Anything else is valued at each
slice's own solution with no re-solve, which needs the spec, sources
and axis the sweep load_archive
hands back carries; a Sweep a live solve returned retains no model. An
expression over a parameter the sweep carried is refused, that
value being a previous slice's answer rather than stored data.
In an EachWindow sweep's answer each coordinate carries the value of the window that owns it, so a sum does not double-count the lookahead.
| PARAMETER | DESCRIPTION |
|---|---|
expression
|
A declared name, an expression string, or the
TYPE:
|
per_window
|
Read an EachWindow sweep one window at a time instead of its answer.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
No slice produced a declared expression (an
evaluation that failed on every slice carries its own reason);
an undeclared expression on a Sweep with no model behind it, or
one that reads a parameter the sweep carried; a quantity
reduced over an EachWindow sweep's windowed dimension, which
has an answer only per window; or |
LanguageError
|
A construct outside the language, or a name the spec does not declare. |
primal ¶
One variable's answer.
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:
|
per_window
|
Read an EachWindow sweep one window at a time instead of its answer.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
No slice of the sweep produced name; a variable
that is not over an EachWindow sweep's windowed dimension,
which has an answer only per window; |
save ¶
Everything the sweep holds, per slice, in the layout spill_to= writes.
<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. scan reads it, and
the call that made this sweep, pointed at it with spill_to=, reads
it back without solving a slice. A sweep whose every slice terminated
without values writes each slice's record and no frames.
| RETURNS | DESCRIPTION |
|---|---|
Path
|
The directory. |
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
The sweep was read off an archive written without its windows. |
scan ¶
One name's answer as a polars.LazyFrame: primal, dual or evaluate, not collected.
On a sweep whose frames lie on disk — solved with spill_to=, or
read by scan_sweep or
scan_archive — nothing is read
until the frame is collected, so a filter or a select runs before the
bytes move.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
A variable, a constraint or a named expression the spec declares, as kind says.
TYPE:
|
kind
|
TYPE:
|
per_window
|
Read an EachWindow sweep one window at a time instead of its answer.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
No slice produced name, a kind that names no
reader, or |
to_dataarray ¶
One name's answer as a xarray.DataArray; to_pandas's arguments.
An EachWindow sweep's answer is indexed by the dimension it sliced.
Any other sweep adds the slice key as a dimension named by the axis,
(scenario, …), and a read per window adds <dim>_start; there a
slice that reached no solution comes back NaN, as a masked coordinate
does from Result.
to_dataset ¶
The named answers of one kind as one xarray.Dataset; all of that kind by default.
| PARAMETER | DESCRIPTION |
|---|---|
names
|
What to include; none means every name of kind the
sweep has an answer for. A name an EachWindow sweep cannot
stitch is left out, as an archive leaves out its file; named,
or read
TYPE:
|
kind
|
TYPE:
|
per_window
|
Read an EachWindow sweep one window at a time instead of its answer.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
SpecsolveError
|
The sweep has no answer of kind at all — the
message names each name it left out, and why — or as
|
ResultArchive
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, as
TYPE:
|
result |
What the solve returned.
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(archive.spec, archive.sources, archive.axis, carry=archive.carry)
runs it again.
| ATTRIBUTE | DESCRIPTION |
|---|---|
spec |
The spec as written.
TYPE:
|
sources |
What the sweep was given, uncut, as
TYPE:
|
axis |
What cut them.
TYPE:
|
carry |
TYPE:
|
sweep |
The archived answer, in memory from
TYPE:
|
source_digests |
As
TYPE:
|
The rows and frames those hand back: how a solve terminated, what produced it, 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.
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.
Zero and infinity are excluded. A model can be clean on
coefficient_range and still have bounds the solver asks to have
scaled. 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. largest / smallest over the frame is the
conditioning to compare against the solver's.
columns
instance-attribute
¶
The shape the build produced: columns, rows, and matrix entries.
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: one emptied of all its terms, and one a propagated absence
deleted. Empty where every declared row was built. A recurrence's first
coordinate counts, so a shift against the horizon's edge reports
here, as the boundary rather than a fault.
rhs_range
instance-attribute
¶
(constraint, smallest, largest) — the same for each block's
right-hand sides, over the rows that survived.
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.
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. loads == solves on an iterating driver means the
model masks on a parameter that varies, unless the driver asked for
keep='nothing'. loads ticks on exactly the solves that report
Result.kept of nothing.
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.
Empty where every one is complete. An entry is a report, not a fault:
absence is how a model masks. A parameter over no dims is never here.
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, which slice_axis and
slice name. 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 that
never held one.
objective
instance-attribute
¶
What the solve reached, or None where it reached nothing — null
rather than nan, which every aggregate reads as a number.
Result.objective is a float and reads it back as nan.
slice_axis
class-attribute
instance-attribute
¶
What the sweep that solved this called its slices — scenario,
snapshot_start, draw — and which slice this is, as text. Both
null for a single solve. Fixed names rather than a column named for the
axis, so every table written here has the same columns.
solved_at
class-attribute
instance-attribute
¶
When the solver returned, in UTC, or None for a solve that carried
no clock, such as a result built by hand.
spec_digest
instance-attribute
¶
A digest of the spec this answered, or None where the solve was run
off a lowered program. Null on disk, never an empty string.
specsolve_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. Null until the archive is written. Every other
table the archive holds carries the same column, specsolve_run.
of
classmethod
¶
of(termination_condition, objective, *, has_primal, spec_digest, solved_at, model_digest=None, provenance=NO_PROVENANCE)
The row a solve that terminated this way writes; each argument fills the column of its name.
status is derived from termination_condition, objective is
kept only where has_primal says there are values, and provenance
fills its own fields' columns.
Provenance ¶
What produced an answer: the solver, the options it ran with, and the packages that built the model.
Enough to install the same environment again and ask the same question.
Every field is None for an answer no solve wrote, such as one built by
hand.
solver
class-attribute
instance-attribute
¶
The solver's name, as solver_name takes it.
solver_options
class-attribute
instance-attribute
¶
The options the solver ran with, as one JSON object with sorted keys:
{} where none were passed. An option that changes the answer, such
as a time limit or a gap, keeps its value, and an infinite or nan
one is the string "inf", "-inf" or "nan"; any other has the
value <not recorded>, so a licence credential never reaches an archive.
solver_version
class-attribute
instance-attribute
¶
The installed version of the solver's Python package.
Metrics ¶
What a build and its solves took, as the row an archive records beside the answer.
The scalars of Diagnostics,
with the same columns whoever writes them, so rows from unrelated runs
concatenate into one table.
Cumulative over the solves it counts, which solves says: 1
for the archive specsolve.solve writes and on each slice's row of a
sweep, whose clocks are that slice's own share. There loads is 1
where the solver took the slice from scratch and 0 where values were
pushed onto the model it held, and write_seconds is zero, a sweep
writing no model file.
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. write_seconds is
write's clock, not the archive's: what
writing the archive cost is recorded nowhere.
slice_axis
class-attribute
instance-attribute
¶
Which sweep slice this row is, as Record.slice_axis and
Record.slice say it; null for a single solve.
solves
instance-attribute
¶
How many solves the row covers, and how many of those loaded the solver from scratch. The clocks are cumulative over exactly these solves.
specsolve_run
class-attribute
instance-attribute
¶
What the archive holding this row was called, as
Record.specsolve_run: its file name without a .zip. Null
until one is written.
since ¶
This row less earlier's counts and clocks: the share of the solves between the two.
Read an answer back¶
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. Without it a zip unpacks to a scratch directory removed before this returns. Refused for a directory archive, which is read where it lies.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ResultArchive | SweepArchive
|
A |
ResultArchive | 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. No build or solver is needed.
kept reads nothing, and
the solver's verbatim wording behind a refusal is not recorded — the
termination condition is. A solve that reached no objective reads back as
nan.
| PARAMETER | DESCRIPTION |
|---|---|
directory
|
Where
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Result
|
The result, read into memory, so it owes directory nothing. |
Result
|
|
| RAISES | DESCRIPTION |
|---|---|
LayoutError
|
A directory holding no |
load_sweep ¶
Read back a sweep Sweep.save wrote, or one solve_over(spill_to=) spilled.
Every slice's frames are in memory when this returns, so the sweep owes
directory nothing afterwards; a sweep larger than memory is
scan_sweep instead. The readers return the answer, the manifest
carrying what each window owns.
| RETURNS | DESCRIPTION |
|---|---|
Sweep
|
The sweep, keyed as it was solved. |
| RAISES | DESCRIPTION |
|---|---|
LayoutError
|
directory holds no |
scan_archive ¶
Read an archive back off disk: the sources as paths, each frame read at the call that asks for it.
As load_archive, except that into is required for a zip, and kept,
since the members have to outlive the value; a zip with no into raises
LayoutError.
scan_result ¶
The answer under directory, read as its readers are called rather than now.
The same answer as load_result, but each frame is a
polars.scan_parquet of its file, so an answer larger than memory is
readable a name at a time, and unread names cost nothing.
The files 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. directory is as
load_result takes it.
| RAISES | DESCRIPTION |
|---|---|
LayoutError
|
As |
scan_sweep ¶
The sweep under directory, its frames left where they lie; directory has to outlive it.
load_sweep's other half, and what a sweep solved with spill_to=
already is: only the record is read until a reader asks for a name.
Sweep.primal and its siblings read that name into memory;
Sweep.scan hands it back as a polars.LazyFrame, for a name too
large to hold.
| 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 ¶
Base class for every error this package raises on purpose.
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 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.
Names that start with specsolve_¶
A declared name that starts with specsolve_ is refused, in any letter
case, whichever verb lowers the spec. The prefix is reserved for the columns
specsolve adds, such as specsolve_run on every table an archive holds, so a
declared name cannot collide with one. Case does not tell two columns apart:
SQL, DuckDB and Power BI read Specsolve_run as specsolve_run. The rule
covers dimensions, relations and their columns, parameters, variables,
constraints, named expressions, sos: sets and assumptions. A key_name=
with the prefix is refused too.
variable 'Specsolve_p' starts with 'specsolve_', which is reserved in any
letter case for the columns specsolve adds, so a declared name cannot collide
with one. Rename it.
What each sink takes¶
Model.check(sink) refuses a built model the sink cannot ingest, naming the
sinks that do, and solve and write refuse the same. The answer is read off
the model the build produced, not the file: a square the data prices at zero,
an integer variable no column is built for or a set with no members asks for
nothing. Where a model 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.