discopt.transformations#
Named model transformations: apply_to / create_using (#1479).
discopt’s reformulations are module functions the solver calls at fixed points
of solve_model() (the GDP lowering, the exact integer and
binary product linearizations, the MPEC complementarity lowerings). Each one
was reachable only through a whole Model.solve() selected by a keyword or a
DISCOPT_* flag, so the only way to look at what a reformulation does was
a whole-solve A/B. This module gives each of them a name and a uniform entry
point, so a transformation can be run on its own, on a copy, and diffed:
import discopt.transformations as dt
dt.available() # registered names
hull = dt.create_using("gdp.hull", m) # m is untouched
dt.apply_to("mpec.sos1", m, pairs=m._complementarities) # m is changed
report = dt.check("integer.bilinear", m) # copy + diff, no solve
report.diff.added_variables
What this module is not. It changes no transformation: every
registered entry resolves, at call time, to the function it names on its module
(so a test’s monkeypatch of that module attribute still takes effect). The
solver’s call sites – every GDP lowering including the AMP route’s
respect_disjunction_methods=False call, the binary/integer product passes,
the MPEC and bilevel complementarity lowerings – dispatch through
Transformation.apply(), which is the bare function call plus a check of
its return type. The solver still decides when each runs and whether to adopt
the result; the registry decides nothing. That dispatch was verified
bound-neutral per CLAUDE.md §5 (identical status, objective, bound and node
count against main on the #1479 panel). One entry is not a solver pass:
core.relax_integrality (#1620, discopt.modeling._relax_integrality),
a user-facing continuous relaxation that no solve path calls.
Two function contracts, one API. The wrapped functions come in two shapes:
functional –
f(model, **options) -> Model: returns a new model (ormodelitself when there is nothing to do) and leaves the input alone. The new model shares the input’sVariableobjects, which still reportvar.model is input.in-place –
f(model, **options) -> None: appends rows and variables tomodel.
apply_to gives both an in-place meaning and create_using gives both a
copy meaning. For a functional transformation apply_to adopts the
returned model’s state into the caller’s object and re-points the new
auxiliaries’ .model at it, so afterwards every variable of the model
reports that model as its owner – which the raw functional result does not.
The adoption refuses (before touching anything) if the returned model is
referenced by anything it does not know how to re-point.
After a transformation that changed the model, two records that describe the
pre-transformation model are cleared: _source_nl_path (the solver would
otherwise hand POUNCE the original .nl for a model that no longer matches
it) and _last_solve_result.
Attributes#
Classes#
One named transformation. |
|
What a transformation changed, by name and expression text. |
|
The outcome of |
Functions#
|
A hashable structural snapshot of |
|
Structural difference between two models (typically a model and its transform). |
|
Apply one transformation to a copy of |
|
Register a transformation under |
|
The registered transformation |
|
Sorted names of every registered transformation. |
|
|
|
|
Module Contents#
- class discopt.transformations.Transformation#
One named transformation.
- Variables:
name (str) – Registry key, dotted by family (
"gdp.hull","mpec.sos1").target (str or callable) – The implementing function, or
"module:attr"resolved on first use (so importing this module imports none of the reformulation layers).style ({"functional", "in_place"}) – The implementing function’s contract; see the module docstring.
summary (str) – One line for
available()listings.exact (bool) –
Truewhen the transformation preserves the feasible set and optimum (an exact reformulation, possibly with auxiliary variables);Falsefor a relaxation or regularization (Scholtes), whose optimum is only an approximation of the source model’s.fixed_options (mapping) – Options this entry always passes (
"gdp.hull"is"gdp"withmethod="hull"). Passing one of them again is refused.
- name: str#
- target: str | Callable[..., Any]#
- style: Style#
- summary: str#
- exact: bool = True#
- fixed_options: Mapping[str, Any]#
- property function: Callable[..., Any]#
The implementing function (the object the solver itself calls).
- apply(model: discopt.modeling.core.Model, **options: Any) discopt.modeling.core.Model#
Run the function with its own contract; return the model it produced.
Functional: the function’s return value (possibly
modelitself). In-place:model, after the function mutated it. This is exactly the call a solver call site makes – no adoption, no invalidation.
- apply_to(model: discopt.modeling.core.Model, **options: Any) discopt.modeling.core.Model#
Transform
modelin place and return it.
- create_using(model: discopt.modeling.core.Model, **options: Any) discopt.modeling.core.Model#
Transform a copy of
modeland return the copy;modelis untouched.The model and the options are deep-copied together, so an option that names parts of the model (
pairs=relations, at=parameter) is mapped onto the copy’s objects rather than left pointing at the original.
- discopt.transformations.model_fingerprint(model: discopt.modeling.core.Model) tuple#
A hashable structural snapshot of
model.Covers the variables (name, type, shape, bounds), parameters (name, value), every constraint (type, name, exact expression text), the objective, the builder-resident linear rows and objective, and the complementarity records. Two models with equal fingerprints state the same problem in the same order; a fingerprint that changes across a call means the call mutated the model. Expressions are rendered by
_canon, not the displayrepr: every float at full precision and every array constant by its full contents (or a SHA-256 of its bytes when large), so a change in the last digit of a coefficient or deep inside a large constant changes the fingerprint (#1497).
- class discopt.transformations.ModelDiff#
What a transformation changed, by name and expression text.
Constraints are compared as multisets of their exact canonical text (see
_canon: full-precision floats, full array contents), so a constraint that moved position is not reported, and one that was replaced by a textually identical copy (as every rebuilding pass does) is not either.- added_variables: tuple[str, ...]#
- removed_variables: tuple[str, ...]#
- rebounded_variables: tuple[str, ...]#
- added_constraints: tuple[str, ...]#
- removed_constraints: tuple[str, ...]#
- objective_changed: bool#
- property unchanged: bool#
- discopt.transformations.diff_models(before: discopt.modeling.core.Model, after: discopt.modeling.core.Model) ModelDiff#
Structural difference between two models (typically a model and its transform).
- class discopt.transformations.TransformationReport#
The outcome of
check().- name: str#
- result: discopt.modeling.core.Model#
- discopt.transformations.check(name: str, model: discopt.modeling.core.Model, **options: Any) TransformationReport#
Apply one transformation to a copy of
modeland diff it, without a solve.Raises
AssertionErrorif the input model changed – the one propertycreate_usingmust never violate. The returned report carries the transformed copy (for a feasibility or bound experiment) and theModelDiffagainst the input. This is the unit of work CLAUDE.md §5 verifies a reformulation with: apply it to a copy and compare, rather than A/B a whole solve behind a flag.
- discopt.transformations.register(name: str, target: str | Callable[..., Any], *, style: Style, summary: str, exact: bool = True, fixed_options: Mapping[str, Any] | None = None) Transformation#
Register a transformation under
name; a duplicate name is refused.
- discopt.transformations.get(name: str) Transformation#
The registered transformation
name, or aKeyErrorlisting the names.
- discopt.transformations.TransformationFactory#
- discopt.transformations.available() list[str]#
Sorted names of every registered transformation.
- discopt.transformations.apply_to(name: str, model: discopt.modeling.core.Model, **options: Any) discopt.modeling.core.Model#
get(name).apply_to(model, **options): transformmodelin place.
- discopt.transformations.create_using(name: str, model: discopt.modeling.core.Model, **options: Any) discopt.modeling.core.Model#
get(name).create_using(model, **options): transform a copy.