Source code for monee.model.formulation.registry
"""Formulation registry and solve-time attachment.
Formulations are a property of the *solve*, not of the network data: the
solver backends call :func:`attach_formulations` on their internal network
copy right before model assembly. The registry maps short string keys to the
built-in bundles so a solve can be parameterised as
``solve(net, formulation="convex_miqcqp")`` without imports.
Resolution order per component (most specific wins):
1. a per-component formulation pinned via the ``formulation=`` keyword of the
:class:`~monee.model.network.Network` builder methods,
2. the ``formulation`` argument handed to the solver,
3. the network-level choice recorded by
:meth:`~monee.model.network.Network.apply_formulation`,
4. :data:`~monee.model.formulation.bundles.DEFAULT_SIMULATION_FORMULATION`.
"""
from collections.abc import Sequence
from .bundles import (
CONVEX_MIQCQP_FORMULATION,
DEFAULT_SIMULATION_FORMULATION,
EL_MISOCP_FORMULATION,
EL_NLP_FORMULATION,
EL_NONCONVEX_MIQCQP_FORMULATION,
GAS_CONVEX_MIQCQP_FORMULATION,
GAS_NLP_FORMULATION,
GAS_NONCONVEX_MIQCQP_FORMULATION,
HEAT_CONVEX_MILP_FORMULATION,
HEAT_NLP_FORMULATION,
HEAT_NONCONVEX_MIQCQP_FORMULATION,
NONCONVEX_MIQCQP_FORMULATION,
SMOOTH_NLP_FORMULATION,
combine,
make_gas_milp_pwl_formulation,
make_heat_nonconvex_pwl_formulation,
)
from .core import NetworkFormulation
# str key -> NetworkFormulation, or a zero-arg factory returning one.
FORMULATIONS: dict = {
"default": DEFAULT_SIMULATION_FORMULATION,
# bundles
"smooth_nlp": SMOOTH_NLP_FORMULATION,
"convex_miqcqp": CONVEX_MIQCQP_FORMULATION,
"nonconvex_miqcqp": NONCONVEX_MIQCQP_FORMULATION,
# electricity
"el_nlp": EL_NLP_FORMULATION,
"el_misocp": EL_MISOCP_FORMULATION,
"el_nonconvex_miqcqp": EL_NONCONVEX_MIQCQP_FORMULATION,
# gas
"gas_nlp": GAS_NLP_FORMULATION,
"gas_convex_miqcqp": GAS_CONVEX_MIQCQP_FORMULATION,
"gas_nonconvex_miqcqp": GAS_NONCONVEX_MIQCQP_FORMULATION,
"gas_milp_pwl": make_gas_milp_pwl_formulation,
# water / heat
"heat_nlp": HEAT_NLP_FORMULATION,
"heat_convex_milp": HEAT_CONVEX_MILP_FORMULATION,
"heat_nonconvex_miqcqp": HEAT_NONCONVEX_MIQCQP_FORMULATION,
"heat_nonconvex_pwl": make_heat_nonconvex_pwl_formulation,
}
[docs]
def register_formulation(key: str, formulation) -> None:
"""Register *formulation* (a :class:`NetworkFormulation` or a zero-arg
factory returning one) under *key* for use as a solve-time shortcut."""
FORMULATIONS[key] = formulation
[docs]
def resolve_formulation(spec) -> NetworkFormulation | None:
"""Resolve a formulation spec into a single :class:`NetworkFormulation`.
Accepts ``None`` (no solver-level formulation), a registry key string, a
:class:`NetworkFormulation`, or a sequence of either - sequences are
merged left to right via :func:`~monee.model.formulation.bundles.combine`
(later entries win on type collisions), e.g.
``("smooth_nlp", EL_MISOCP_FORMULATION)``.
"""
if spec is None:
return None
if isinstance(spec, NetworkFormulation):
return spec
if isinstance(spec, str):
try:
entry = FORMULATIONS[spec]
except KeyError:
raise KeyError(
f"Unknown formulation key {spec!r}. Registered keys: "
f"{sorted(FORMULATIONS)}"
) from None
return entry() if callable(entry) else entry
if isinstance(spec, Sequence):
return combine(*[resolve_formulation(item) for item in spec])
raise TypeError(
"formulation must be None, a registry key, a NetworkFormulation or a "
f"sequence of those, got {type(spec).__name__}"
)
[docs]
def attach_formulations(network, formulation=None, simulation: bool = False) -> None:
"""Attach the effective formulation to every component of *network* and
declare its variables (``ensure_var``).
Called by the solver backends on their internal network copy, after
extension ``prepare()`` and before problem application / variable
injection. *formulation* is any spec accepted by
:func:`resolve_formulation`; it overrides the network-level
``apply_formulation`` choice but not per-component pinned formulations.
"""
solver_nf = resolve_formulation(formulation)
for component in network.all_components():
effective = None
if getattr(component, "formulation_pinned", False):
effective = component.formulation
if effective is None and solver_nf is not None:
effective = solver_nf.lookup(component.model, component.grid)
if effective is None:
effective = network.lookup_formulation(component.model, component.grid)
if effective is None:
# Manually assigned (component.formulation = ...) or left over from
# a previous attach when re-solving a solver result network.
effective = component.formulation
if effective is None:
effective = DEFAULT_SIMULATION_FORMULATION.lookup(
component.model, component.grid
)
component.formulation = effective
if effective is not None:
effective.ensure_var(
component.model, simulation=simulation, grid=component.grid
)