Skip to content

Python API

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

specsolve holds what you call, specsolve.types what a call hands back, and specsolve.errors what a call raises or warns. Any other name under specsolve. is internal. The glossary defines the house terms.

Run a spec

check

check(spec)

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 Spec — what mathspec.to_spec takes. A lowered Program is not taken. A piecewise: block is written out first: to_spec(spec).expand('piecewise') keeps every sos: set for a sink that takes one, and to_spec(spec).expand() writes the sets out as binaries every sink takes.

TYPE: Buildable

RETURNS DESCRIPTION
Program

The lowered program, for reading the plan; typeset it or read its

Program

declarations through mathspec. No verb takes it back; keep the

Program

Spec for that.

RAISES DESCRIPTION
LanguageError

A construct outside the streaming language, a piecewise: block still to be written out, or a fragment that reads a name under given: — mathspec.merge composes it.

SpecsolveError

Two declarations whose names differ only by case, or a name that starts with specsolve_ in any letter case, which is reserved.

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

build(spec, sources)

Attach sources to spec and build it — the model with your data on it.

PARAMETER DESCRIPTION
spec

As check takes it.

TYPE: Buildable

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. Each is read once and kept without a copy, so it must not change while the model or its answers are in use; change data through Model.update.

TYPE: Mapping[str, Source]

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, outputs=(), start=None)

Build spec and solve it in one call.

To solve the same spec again with new numbers, use build and Model.update.

PARAMETER DESCRIPTION
spec

As check takes it.

TYPE: Buildable

sources

As build takes them.

TYPE: Mapping[str, Source]

solver_name

As Model.solve takes it.

TYPE: str DEFAULT: 'highs'

solver_options

As Model.solve takes them.

TYPE: Mapping[str, object] | None DEFAULT: None

record_options

As Model.solve takes them.

TYPE: Sequence[str] | None DEFAULT: None

archive

As Model.solve takes it — a .zip, or a directory.

TYPE: str | Path | None DEFAULT: None

outputs

As Model.solve takes them.

TYPE: Iterable[Output] DEFAULT: ()

start

As Model.solve takes it.

TYPE: Result | Start | None DEFAULT: None

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, or outputs that Model.solve refuses — both checked before the build — or as Model.solve raises.

write

write(spec, sources, out)

Build spec and stream it to out, in the format its suffix names.

PARAMETER DESCRIPTION
spec

As check takes it.

TYPE: Buildable

sources

As build takes them.

TYPE: Mapping[str, Source]

out

Where to write; .lp and .mps ship, and name a model's columns and rows the same way.

TYPE: str | Path

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

evaluate(spec, sources, expression)

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 check takes it.

TYPE: Buildable

sources

As build takes them.

TYPE: Mapping[str, Source]

expression

What one expressions: entry takes — a name the spec declares, an expression string, or the mapping carrying cases: with dims: and otherwise:.

TYPE: str | Mapping[str, object]

RETURNS DESCRIPTION
DataFrame

The value, (dims…, value) over the expression's own dims. Only

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

tidy(spec, sources)

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 check takes it.

TYPE: Buildable

sources

As build takes them.

TYPE: Mapping[str, Source]

RETURNS DESCRIPTION
dict[str, DataFrame]

Each dimension as (dim, specsolve_position): its labels once each,

dict[str, DataFrame]

and the Int64 position from 0 that shift counts. Each

dict[str, DataFrame]

parameter as (dims…, value). Each relation as the columns it

dict[str, DataFrame]

declares.

RAISES DESCRIPTION
LanguageError

A construct outside the streaming language.

DataError

As build refuses the sources.

Run it many times

Sweeps says how a sweep is cut and read.

solve_over

solve_over(spec, sources, axis, *, key_name=None, executor=None, workers_share_fs=None, solver_options=None, record_options=None, solver_name='highs', spill_to=None, archive=None, keep_windows=False, outputs=(), start='previous')

Solve spec once per slice of axis and fold the answers together.

The rules are sweeps.

PARAMETER DESCRIPTION
spec

As check takes it. Parsed once, whichever executor runs the slices.

TYPE: Buildable

sources

As build takes them, every shape included; the axis filters the parameters and relations that carry it and passes the rest through.

TYPE: Mapping[str, Source]

axis

EachCoordinate, EachWindow, either as the plain data its to_dict writes, or a list of (key, sources) written by hand. An axis's carry= chains its slices; a hand-built list carries nothing.

TYPE: Axis | Mapping[str, Any] | HandBuilt

key_name

What to call the slice column; a class axis names its own, a hand-built list has to be told.

TYPE: str | None DEFAULT: None

executor

Any concurrent.futures.Executor; None runs the slices in order on one model. A process pool must be spawn or forkserver — a forked worker hangs.

TYPE: Executor | None DEFAULT: None

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: bool | None DEFAULT: None

solver_options

As solve takes them.

TYPE: Mapping[str, object] | None DEFAULT: None

record_options

As solve takes them, reaching every slice.

TYPE: Sequence[str] | None DEFAULT: None

solver_name

As solve takes it.

TYPE: str DEFAULT: 'highs'

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; Sweep.scan reads a name off it lazily. A directory holds one sweep: the same sweep run at it again does not solve the slices already there, which is how an interrupted sweep resumes.

TYPE: str | Path | None DEFAULT: None

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 sps.load_archive gives all four back and the sweep runs again from the file alone. A .zip suffix packs it into one file; anything else is a directory. The answer is one file per name at answer/<kind>/<name>.parquet, as for a single solve; a name with no answer, such as a quantity reduced over an EachWindow sweep's windowed dimension, is left out and answer/reasons.parquet says why. Beside spill_to the archive packs the spill, so a sweep too large to hold is archived without being held; the archive is a second copy of the answers on disk. A sliced source is archived whole, and one number over a window's local index as a table over the axis. Refused for a hand-built axis, which is a set of sources per slice: archive one solve each.

TYPE: str | Path | None DEFAULT: None

keep_windows

Also archive an EachWindow sweep's frames per window, lookahead rows included, under answer/windows/, so that per_window=True reads off the archive. Refused for any other axis, and without archive.

TYPE: bool DEFAULT: False

outputs

As solve takes them, reaching every slice. The sweep, its spill and its archive carry these and nothing else; a spill_to directory solved with others is refused rather than resumed.

TYPE: Iterable[Output] DEFAULT: ()

start

What each slice starts from, as solve takes it: an earlier answer, an earlier sweep, or a Start of tables. Each table is cut by the axis as a source is: one that carries the sliced dimension, or a hand-built axis's key column, gives each slice its own rows, and one without reaches every slice whole. An earlier sweep is its answer, so each slice starts from the earlier slice of its key, and each window from the hours it covers. 'previous', the default, carries each slice on from the one before it on the model updated in place, as solve does, and under an executor starts every slice from nothing. None starts every slice from nothing.

TYPE: Sweep | Result | Start | Literal['previous'] | None DEFAULT: 'previous'

RETURNS DESCRIPTION
Sweep

The sweep, which reads its answer.

RAISES DESCRIPTION
SpecsolveError

Before a slice is taken: an axis given as a mapping whose keys name no axis; 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; outputs that solve refuses; a start word other than 'previous' — each answerable from the declarations alone; keys of more than one type, or two keys of one text; a start table over an EachWindow sweep's local index alone, or one solve refuses.

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 — a position the model counts, which every window restarts, or a start that leaves a slice no row, which starts that slice from nothing.

EachCoordinate dataclass

EachCoordinate(dim, *, carry=dict())

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.

carry={parameter: variable} chains them in that order: each slice's answer is copied into the next slice's data, and the first slice takes the parameter from the sources as its seed.

carry class-attribute instance-attribute
carry = field(default_factory=dict, kw_only=True, hash=False)

{parameter: variable} handed from each slice to the next; empty for slices that do not depend on one another.

dim instance-attribute
dim
slices
slices(sources)

The (key, sources) list this axis would run — what axis= takes hand-built.

For building one slice alone: sps.build(spec, axis.slices(sources)[3][1]).

to_dict
to_dict()

This axis as plain data, which solve_over takes in its place.

json.dumps writes it, and an archive's axis.json is this. It holds a carry key only where the axis carries something.

EachWindow dataclass

EachWindow(dim, *, steps, lookahead, into, carry=dict())

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.

carry={parameter: variable} chains the windows in order: each window's answer is copied into the next window's data. Where the two are over different dimensions, the dropped one is into, and the last coordinate the window owns is handed on. The first window takes the parameter from the sources as its seed.

carry class-attribute instance-attribute
carry = field(default_factory=dict, kw_only=True, hash=False)

{parameter: variable} handed from each window to the next; empty for windows that do not depend on one another.

dim instance-attribute
dim
into class-attribute instance-attribute
into = field(kw_only=True)
lookahead class-attribute instance-attribute
lookahead = field(kw_only=True)
steps class-attribute instance-attribute
steps = field(kw_only=True)
slices
slices(sources)

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 nothing is carried between them.

to_dict
to_dict()

This axis as plain data, which solve_over takes in its place.

json.dumps writes it, and an archive's axis.json is this. It holds a carry key only where the axis carries something.

What comes back

Model

Model(spec, sources)

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
check(sink)

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 (highs, gurobi, xpress) or an output suffix (.lp, .mps).

TYPE: str

RAISES DESCRIPTION
SpecsolveError

A construct the sink cannot take, naming it and the sinks that do; or a name belonging to no sink.

close
close()

Release the built model, and any solver still holding it.

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

infeasible_subsystem
infeasible_subsystem()

The rows and bounds that make the last solve infeasible, by declaration and coordinate.

The verb for this model has no solution and I do not know why. It asks the solver that ran the last solve for an IIS (irreducible infeasible subsystem). The search runs only on this call, since it can cost more than the solve, and under the solve's solver_options, so a solver's IIS settings and time limit go there. Read a member row's terms with row.

Every sink finds one for a linear model. For a discrete model, every sink leaves out the integrality that makes the rows and bounds conflict, and highs can find none where integrality causes the conflict.

RAISES DESCRIPTION
SpecsolveError

The model has not been solved since it was built, updated or closed; the last solve was not infeasible; or the solver found no subsystem, or stopped at a limit before it was irreducible.

Example

model.solve().termination_condition # doctest: +SKIP 'infeasible' print(model.infeasible_subsystem()) # doctest: +SKIP balance[snapshot=1] == 200 p[snapshot=1, tech=gas] <= 100 (upper bound) p[snapshot=1, tech=wind] <= 50 (upper bound)

row
row(name, /, **coordinate)

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

TYPE: str

coordinate

One label per dim of that constraint, all of them.

TYPE: Label DEFAULT: {}

RETURNS DESCRIPTION
ConstraintRow

The terms as (variable, coordinate, coefficient), beside the

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[snapshot=1, tech=wind] +50 p[snapshot=1, tech=gas] >= 60

solve
solve(solver_name='highs', *, solver_options=None, record_options=None, archive=None, outputs=(), start='previous')

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; diagnostics counts the solves that loaded it again.

PARAMETER DESCRIPTION
solver_name

highs, which ships with the package; gurobi, which needs the [gurobi] extra; or xpress, which needs the [xpress] extra.

TYPE: str DEFAULT: 'highs'

solver_options

Forwarded to the solver verbatim, in its own vocabulary, so a time limit is time_limit, TimeLimit or timelimit. Gurobi's are applied when its environment is created, so ComputeServer, TokenServer and WLSAccessID reach it too. The result's provenance records the value of an option that changes the answer, such as a time limit or a gap, and the name alone of any other.

TYPE: Mapping[str, object] | None DEFAULT: None

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: Sequence[str] | None DEFAULT: None

archive

Where to write the spec, the data attached to it now, and this answer, so that load_archive gives all three back and the model solves again from the file alone. A .zip suffix packs it into one file; anything else is a directory. What the build and its solves spent goes in beside the answer, as Metrics. Each source goes in as the table tidy returns, with specsolve_run added, and members are stored uncompressed.

TYPE: str | Path | None DEFAULT: None

outputs

Each Output the answer carries beside the primal, the duals and the declared expressions: activity for each constraint's left-hand side, reduced_cost for each variable's reduced cost, slack for each constraint's distance to binding, and basis for the basis status each variable and constraint ended on, read with variable_basis and constraint_basis. The result, its save and its archive carry these and nothing else, and the reader of one not asked for refuses.

TYPE: Iterable[Output] DEFAULT: ()

start

What the solve begins from; it changes how the solver gets to the optimum, never which one. 'previous', the default, carries on from the last solve while the solver stays loaded, and begins from nothing after an update that loads it again. None begins from nothing. An earlier answer, live, loaded with load_result or from an archive, or a Start of tables keyed by reader, is matched by coordinate, so one from another build of the spec, with rows or columns gained or lost, starts it too. An LP starts from a basis where one is given, which an answer carries when solved with outputs={'basis'}: a coordinate it leaves out starts at a bound if it is a variable's, and not binding if it is a constraint's. Otherwise an LP, and a mixed-integer model always, starts from values, which the solver completes and repairs. Where a solver takes values for an LP and no gain from them is known, the solve warns.

TYPE: Result | Start | Literal['previous'] | None DEFAULT: 'previous'

RETURNS DESCRIPTION
Result

The solution, holding this model.

RAISES DESCRIPTION
SpecsolveError

A solver name nothing serves, one this environment cannot run, a bare string as record_options or outputs, a name in outputs that is not an output, a start word other than 'previous', or a start this model or solver cannot start from: a key that names no reader, a table naming no declaration or lacking its dims, a basis status outside the five words, a basis alone for a mixed-integer model, a start that lands on no coordinate, or values for an LP that the solver cannot take.

LayoutError

An archive directory that already holds something, refused before the solve.

update
update(sources)

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 build attached. A dimension's labels count too, which is how a coordinate set grows.

TYPE: Mapping[str, Source]

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
write(path)

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 check refuses it.

ConstraintRow dataclass

ConstraintRow(name, coordinate, terms, sense, rhs)

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, each term at its named coordinate; 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: str

coordinate

Where in that declaration it sits.

TYPE: Mapping[str, object]

terms

(variable, coordinate, coefficient), one row per term, in the solver's own column order. coordinate is the term's coordinate as one string, t=1, g=gas, in its variable's dim order.

TYPE: DataFrame

sense

<=, >= or ==.

TYPE: str

rhs

What the left-hand side is compared against.

TYPE: float

coordinate instance-attribute
coordinate
display_terms class-attribute instance-attribute
display_terms = 12

How many terms a line spells out before it summarises instead.

name instance-attribute
name
rhs instance-attribute
rhs
sense instance-attribute
sense
terms instance-attribute
terms

Result dataclass

Result(_status, _objective, _primals, _duals, _outputs, _expressions=None, _evaluate=None, _no_duals=None, _dual_rays=None, _no_dual_ray=None, _spec=None, _solved_at=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
has_primal

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.

is_ok property
is_ok

The linopy rollup: not an error, an abort or a refusal.

objective property
objective

The objective value, or nan when there is no solution.

provenance property
provenance

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
record

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.

solved_at property
solved_at

When the solver returned, in UTC — None where the solve carried no clock.

spec property
spec

The spec this answered, as written; None where the solve ran off a lowered program.

Two answers whose specs compare equal answered the same document, perhaps over other data.

status property
status

Coarse outcome: ok / warning / error / aborted / unknown.

termination_condition property
termination_condition

What the solver said — optimal, infeasible, time_limit and so on.

activity
activity(name)

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. Carried only where the solve was asked for it with outputs={'activity'}.

RAISES DESCRIPTION
NoSolutionError

The solve left no values to read.

SpecsolveError

This result was closed, or the solve was not asked for its activity.

KeyError

No constraint is called name.

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

constraint_basis
constraint_basis(name)

The basis status of each row of constraint name where the solve ended — (dims…, value), dual's shape and order.

In variable_basis's words, with the right-hand side as the row's bound: a binding <= row is at_upper, a binding >= row at_lower, a binding == row fixed, and a row that is not binding basic. Carried only where the solve was asked for it with outputs={'basis'}, and only an LP that ended on a basis has one.

RAISES DESCRIPTION
NoSolutionError

The solve left no values to read.

SpecsolveError

This result was closed, the solve was not asked for its basis, or it ended on none.

KeyError

No constraint is called name.

dual
dual(name)

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.

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 sos: set that Spec.expand() wrote out as binaries. gurobi and xpress branch on a set itself and keep them.

KeyError

No constraint is called name.

dual_ray
dual_ray(name)

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
evaluate(expression)

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 Program or read back off disk, which has nothing to lower it against; an archive whose sources build another model than the one this answered; or a divisor with no value.

LanguageError

A construct outside the language, or a name the spec does not declare — a new parameter is a build, not a read.

primal
primal(name)

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.

reduced_cost
reduced_cost(name)

Reduced costs of variable name — (dims…, value), primal's shape and order.

Each is the rate at which the optimal objective rises as the variable's binding bound rises, in the sign convention of dual for every sense and sink: of x >= l, the rate in d of x >= l + d. A variable strictly between its bounds has zero.

It is computed from the duals as the objective's gradient less each row's gradient times that row's dual, so it exists wherever dual does and nowhere else. Carried only where the solve was asked for it with outputs={'reduced_cost'}.

RAISES DESCRIPTION
NoSolutionError

The solve left no values at all.

SpecsolveError

This result was closed; the solve was not asked for reduced costs; or it left primals but no duals, as dual raises.

KeyError

No variable is called name.

save
save(directory)

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, and spec.yaml the spec it answered. Beside them are primal/<name>.parquet per variable, dual/<name>.parquet per constraint where the duals are defined, expression/<name>.parquet per named expression this data can evaluate, and <output>/<name>.parquet for each Output the solve was asked for, such as activity/ per constraint. 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 and the spec alone. The same model and data write the same bytes.

format.json stamps the layout, the specsolve that wrote it and the outputs the answer carries: {"layout": 4, "specsolve": "…", "outputs": ["activity"]}. 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.

slack
slack(name)

How far constraint name is from binding at the solution — (dims…, value), dual's shape and order.

Non-negative wherever the row holds, whichever side a term is written on: rhs - lhs for <=, lhs - rhs for >=, and -|rhs - lhs| for ==, which holds only at zero. A binding row reads zero, and a violated one, such as at an incumbent within the solver's tolerance, reads below zero by how much. Computed from activity, so readable wherever it is, and carried only where the solve was asked for it with outputs={'slack'}.

RAISES DESCRIPTION
NoSolutionError

The solve left no values to read.

SpecsolveError

This result was closed, or the solve was not asked for its slack.

KeyError

No constraint is called name.

to_dataarray
to_dataarray(name, kind='primal')

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
to_dataset(*names, kind='primal')

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
to_pandas(name, kind='primal')

One name's values as a tidy pandas.DataFrame.

name is read through the reader kind names: primal, dual, expression, or the reader of an Output the solve was asked for. Needs pandas, which specsolve does not install; the xarray bridges need xarray too.

variable_basis
variable_basis(name)

The basis status of each column of variable name where the solve ended — (dims…, value), primal's shape and order.

value is one of basic, at_lower, at_upper, fixed (nonbasic at bounds that are equal) and superbasic (nonbasic between its bounds), in that order as an Enum, the same words for every sink. Carried only where the solve was asked for it with outputs={'basis'}, and only an LP that ended on a basis has one.

RAISES DESCRIPTION
NoSolutionError

The solve left no values to read.

SpecsolveError

This result was closed, the solve was not asked for its basis, or it ended on none.

KeyError

No variable is called name.

InfeasibleSubsystem dataclass

InfeasibleSubsystem(constraints, bounds)

The rows and bounds that cannot hold together — what infeasible_subsystem returns.

Irreducible: drop any one member and the rest can hold. A model can have several, and each solver may find a different one. Printed, it is one line per member, constraints first.

ATTRIBUTE DESCRIPTION
constraints

(dims…, sense, rhs) per constraint with a row in it, in declaration order, its rows in label order.

TYPE: Mapping[str, DataFrame]

bounds

(dims…, bound, value) per variable with a bound in it, in declaration order. bound is lower or upper.

TYPE: Mapping[str, DataFrame]

bounds instance-attribute
bounds
constraints instance-attribute
constraints

Sweep dataclass

Sweep(key_name, record, metrics, spec, _slices=dict(), _no_duals=None, _absent=dict(), _stitch=None, _spill=None, _answer=None, _windows=True, _outputs=frozenset(), _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.

key_name instance-attribute
key_name
keys property
keys
metrics instance-attribute
metrics

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
record

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.

spec instance-attribute
spec

The spec every slice answered, as written; None only for a directory written without one.

activity
activity(name, *, per_window=False)

One constraint's left-hand side at every slice's solution.

primal's shape and arguments, and carried only where the sweep was asked for it with outputs={'activity'}. In the answer of an EachWindow sweep each coordinate carries the window that owns it.

RAISES DESCRIPTION
SpecsolveError

The sweep was not asked for its activity, or as primal raises.

constraint_basis
constraint_basis(name, *, per_window=False)

One constraint's basis status where every slice's solve ended.

primal's shape and arguments, and Result.constraint_basis's words. Carried only where the sweep was asked for it with outputs={'basis'}. A slice that ended on no basis contributes none.

RAISES DESCRIPTION
SpecsolveError

The sweep was not asked for its basis, no slice ended on one, or as primal raises.

dual
dual(name, *, per_window=False)

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 primal raises.

evaluate
evaluate(expression, *, per_window=False)

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 cases: mapping, as one expressions: entry takes.

TYPE: str | Mapping[str, object]

per_window

Read an EachWindow sweep one window at a time instead of its answer.

TYPE: bool DEFAULT: False

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 per_window as primal raises it.

LanguageError

A construct outside the language, or a name the spec does not declare.

primal
primal(name, *, per_window=False)

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: str

per_window

Read an EachWindow sweep one window at a time instead of its answer.

TYPE: bool DEFAULT: False

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; per_window on a sweep that was not cut into windows, or on an archive written without them.

reduced_cost
reduced_cost(name, *, per_window=False)

One variable's reduced costs at every slice's solution.

primal's shape and arguments, and Result.reduced_cost's sign. Carried only where the sweep was asked for it with outputs={'reduced_cost'}. A slice whose model had an integer variable contributes none, as it contributes no duals.

RAISES DESCRIPTION
SpecsolveError

The sweep was not asked for reduced costs, no slice produced them — the message says why — or as primal raises.

save
save(directory)

Everything the sweep holds, per slice, in the layout spill_to= writes.

<kind>/<name>/<position>.parquet for every primal, dual, expression and output it carries, 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
scan(name, kind='primal', *, per_window=False)

One name's answer as a polars.LazyFrame: primal, dual, evaluate or an output, 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: str

kind

primal, dual, expression, or an Output the sweep was asked for — the reader this stands in for.

TYPE: str DEFAULT: 'primal'

per_window

Read an EachWindow sweep one window at a time instead of its answer.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
SpecsolveError

No slice produced name, a kind that names no reader, an output the sweep was not asked for, or per_window where there are no windows to read.

slack
slack(name, *, per_window=False)

One constraint's distance to binding at every slice's solution.

primal's shape and arguments, and Result.slack's sign. Carried only where the sweep was asked for it with outputs={'slack'}.

RAISES DESCRIPTION
SpecsolveError

The sweep was not asked for its slack, or as primal raises.

to_dataarray
to_dataarray(name, kind='primal', *, per_window=False)

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
to_dataset(*names, kind='primal', per_window=False)

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 per_window, it is read as primal reads it.

TYPE: str DEFAULT: ()

kind

As scan takes it.

TYPE: str DEFAULT: 'primal'

per_window

Read an EachWindow sweep one window at a time instead of its answer.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
SpecsolveError

The sweep has no answer of kind at all — the message names each name it left out, and why — or as primal raises.

to_pandas
to_pandas(name, kind='primal', *, per_window=False)

One name's answer as a tidy pandas.DataFrame; scan's arguments.

The name is resolved before pandas is imported, so a sweep that never held name says so on any install.

variable_basis
variable_basis(name, *, per_window=False)

One variable's basis status where every slice's solve ended.

primal's shape and arguments, and Result.variable_basis's words. Carried only where the sweep was asked for it with outputs={'basis'}. A slice that ended on no basis contributes none.

RAISES DESCRIPTION
SpecsolveError

The sweep was not asked for its basis, no slice ended on one, or as primal raises.

ResultArchive dataclass

ResultArchive(spec, sources, result, metrics)

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 Spec whatever went in.

TYPE: Spec

sources

What was attached, keyed as the file declares it, as tidy returns it: a table from load_archive, the path to one from scan_archive, which also holds the specsolve_run column.

TYPE: Mapping[str, Source]

result

What the solve returned.

TYPE: Result

metrics

What reaching the answer took, as one Metrics.

TYPE: Metrics

metrics instance-attribute
metrics
result instance-attribute
result
sources instance-attribute
sources
spec instance-attribute
spec

SweepArchive dataclass

SweepArchive(spec, sources, axis, sweep)

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) runs it again.

ATTRIBUTE DESCRIPTION
spec

The spec as written.

TYPE: Spec

sources

What the sweep was given, uncut, as ResultArchive holds them. A source the axis cuts holds the axis column first; a parameter given as one number over a window's local index is held over the axis, so each slice cuts from it what it attached.

TYPE: Mapping[str, Source]

axis

What cut them, with the carry= it chained the slices with.

TYPE: Axis

sweep

The archived answer, in memory from load_archive, on disk from scan_archive. per_window=True reads an EachWindow sweep's windows where keep_windows=True kept them, and is refused otherwise.

TYPE: Sweep

axis instance-attribute
axis
sources instance-attribute
sources
spec instance-attribute
spec
sweep instance-attribute
sweep

ArchivedInputs dataclass

ArchivedInputs(spec, sources, axis)

A spec and the data it was solved with, read off an archive without its answer.

sps.solve(inputs.spec, inputs.sources) asks the question again, and sps.solve_over(inputs.spec, inputs.sources, inputs.axis) runs a sweep again.

ATTRIBUTE DESCRIPTION
spec

The spec as written.

TYPE: Spec

sources

What was attached, keyed as the file declares it, as the tables tidy returns. A sweep's are uncut, as SweepArchive holds them.

TYPE: Mapping[str, Source]

axis

What cut the sources, with the carry= it chained the slices with, or None for an archive of one solve.

TYPE: Axis | None

axis instance-attribute
axis
sources instance-attribute
sources
spec instance-attribute
spec

The rows and frames those hand back:

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
bound_range

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

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

The shape the build produced: columns, rows, and matrix entries.

loads instance-attribute
loads
nonzeros instance-attribute
nonzeros
objective_range instance-attribute
objective_range

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
omissions

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

(constraint, smallest, largest) — the same for each block's right-hand sides, over the rows that survived.

rows instance-attribute
rows
seconds instance-attribute
seconds

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
solves

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.

sparse_parameters instance-attribute
sparse_parameters

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

metrics
metrics()

The sizes, counters and clocks as one value — the row archive= records beside the answer.

Metrics says what each field means. A phase this build never entered reads zero, and specsolve_run is null.

Record

How a solve terminated, what it reached, and when.

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
has_primal

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.

mathspec_version class-attribute instance-attribute
mathspec_version = None
objective instance-attribute
objective

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.

provenance property
provenance

What produced the answer this row records.

slice class-attribute instance-attribute
slice = None
slice_axis class-attribute instance-attribute
slice_axis = None

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.

solve_status property
solve_status

The status this row records, without the solver's own wording.

solved_at class-attribute instance-attribute
solved_at = None

When the solver returned, in UTC, or None for a solve that carried no clock, such as a result built by hand.

solver class-attribute instance-attribute
solver = None

Provenance's fields, one column each.

solver_options class-attribute instance-attribute
solver_options = None
solver_version class-attribute instance-attribute
solver_version = None
specsolve_run class-attribute instance-attribute
specsolve_run = None

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.

specsolve_version class-attribute instance-attribute
specsolve_version = None
status instance-attribute
status
termination_condition instance-attribute
termination_condition
of classmethod
of(termination_condition, objective, *, has_primal, solved_at, 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.

mathspec_version class-attribute instance-attribute
mathspec_version = None
solver class-attribute instance-attribute
solver = None

The solver's name, as solver_name takes it.

solver_options class-attribute instance-attribute
solver_options = None

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
solver_version = None

The installed version of the solver's Python package.

specsolve_version class-attribute instance-attribute
specsolve_version = None

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
attach_seconds

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.

build_seconds instance-attribute
build_seconds
columns instance-attribute
columns

The shape the build produced, in the solver's own vocabulary.

handoff_seconds instance-attribute
handoff_seconds
loads instance-attribute
loads
nonzeros instance-attribute
nonzeros
rows instance-attribute
rows
slice class-attribute instance-attribute
slice = None
slice_axis class-attribute instance-attribute
slice_axis = None

Which sweep slice this row is, as Record.slice_axis and Record.slice say it; null for a single solve.

solve_seconds instance-attribute
solve_seconds
solves instance-attribute
solves

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
specsolve_run = None

What the archive holding this row was called, as Record.specsolve_run: its file name without a .zip. Null until one is written.

write_seconds instance-attribute
write_seconds
since
since(earlier)

This row less earlier's counts and clocks: the share of the solves between the two.

Output module-attribute

Output = Literal['activity', 'reduced_cost', 'slack', 'basis']

What an answer carries only where the solve asked for it with outputs=, beside the primal, the duals and the declared expressions it always carries. activity is each constraint's left-hand side at the solution, reduced_cost each variable's reduced cost, slack each constraint's distance to binding, and basis the basis status the solve ended on, of each variable and each constraint.

Start

What a solve starts from, given as tables: under the name of each reader, a table per declaration in that reader's shape.

Each table is (dims…, value) and is matched by coordinate, so it may leave out declarations and coordinates. primal gives values, and variable_basis and constraint_basis give a basis status in Result.variable_basis's words, as a string or as that Enum. An earlier Result is the same, from the tables it carries. Given to solve_over, a table that carries the sliced dimension is cut per slice, as a source is.

Example::

start: Start = {
    'primal': {'on': on},
    'variable_basis': {'p': p_status},
    'constraint_basis': {'balance': balance_status},
}
constraint_basis instance-attribute
constraint_basis
primal instance-attribute
primal
variable_basis instance-attribute
variable_basis

Read an answer back

load_archive

load_archive(path, into=None)

Read an archive back whole: the sources as tables, the answer's frames in memory.

PARAMETER DESCRIPTION
path

The archive, a .zip or the directory one was written to.

TYPE: str | Path

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: str | Path | None DEFAULT: None

RETURNS DESCRIPTION
ResultArchive | SweepArchive

A SweepArchive where the archive carries an axis, a

ResultArchive | SweepArchive

ResultArchive where it does not.

RAISES DESCRIPTION
LanguageError

A spec.yaml the language does not accept.

LayoutError

A member outside the layout, an into given for a directory, or a spec, sources or answer whose layout has moved since it was written. Where only the answer's has, load_inputs still reads the spec and the sources.

SpecsolveError

An answer that names a different spec than the one beside it.

BadZipFile

A file that is not a zip archive.

load_inputs

load_inputs(path, into=None)

Read the spec and the data an archive holds, without its answer.

The spec and the sources have a layout of their own, so this reads an archive whose answer load_archive refuses because its layout has moved.

PARAMETER DESCRIPTION
path

The archive, a .zip or the directory one was written to.

TYPE: str | Path

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: str | Path | None DEFAULT: None

RAISES DESCRIPTION
LanguageError

A spec.yaml the language does not accept.

LayoutError

A member outside the layout, an into given for a directory, or a spec and sources whose layout has moved since they were written.

BadZipFile

A file that is not a zip archive.

load_result

load_result(directory)

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, each named expression, the outputs the solve was asked for, and the reason behind anything the solve could not produce. No build or solver is needed.

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 save wrote it. One inside an archive is load_archive's to find.

TYPE: str | Path

RETURNS DESCRIPTION
Result

The result, read into memory, so it owes directory nothing.

Result

scan_result is the same answer left on disk.

RAISES DESCRIPTION
LayoutError

A directory holding no record.parquet, or one whose layout has moved since it was written.

load_sweep

load_sweep(directory)

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 sweep.json, misses a record every fold writes, or is in a layout that has moved since it was written.

scan_archive

scan_archive(path, into=None)

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

scan_result(directory)

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 load_result raises it.

scan_sweep

scan_sweep(directory)

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 load_sweep raises it.

Errors and warnings

Every error is rooted at SpecsolveError. A spec the language accepts and specsolve cannot build raises SpecsolveError itself. LanguageError, SchemaError and DimensionError are faults in the spec, and 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 functions

Names that differ only by case

Two declarations of one namespace whose names differ only by case are refused by every function. A case-insensitive filesystem folds p and P into one file on disk.

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

Constraints are a namespace of their own, so a constraint P beside a variable p is accepted.

Names that start with specsolve_

A declared name that starts with specsolve_, in any letter case, is refused by every function. The prefix is reserved for the columns specsolve adds, such 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, and so is a dimension called value.

variable 'Specsolve_p' starts with 'specsolve_', which is reserved in any
letter case for the columns specsolve adds, so it could 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. solve and write refuse the same. The check reads the built model, so a square the data prices at zero, an integer variable with no column or an empty set asks for nothing. Where a model can land is a separate question from whether it is sayable.

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 refuses a nonconvex quadratic, and a quadratic with integrality.
  • The lp_file column says what is written, not what reads back. HiGHS reads the quadratic-objective section back and refuses the sos and quadratic-constraint sections.
  • "No path here" describes this package, not Xpress. The Xpress sink never hands the Optimizer a Hessian.