Source code for ams.opt.constraint

"""
Module for optimization Constraint.
"""
import logging

from typing import Callable, Optional

import numpy as np

import cvxpy as cp

from ams.utils import pretty_long_message
from ams.shared import _prefix, _max_length

from ams.core.routine_ns import RoutineNS
from ams.opt import OptzBase, ensure_symbols, ensure_mats_and_parsed
from ams.opt.optzbase import _EFormDescriptor
from ams.opt._runtime_eval import eval_e_str, assert_constraint_lhs_zero

logger = logging.getLogger(__name__)


[docs] class Constraint(OptzBase): """ Base class for constraints. Routines are authored with ``e_str`` strings; the codegen at :func:`ams.prep.generate_for_routine` compiles each ``e_str`` into a callable ``e_fn(r)`` taking a :class:`RoutineNS` proxy and returning a CVXPY constraint. The mutex descriptor on ``e_str`` / ``e_fn`` keeps the two forms exclusive. Authors who need to bypass the DSL may pass ``e_fn=`` directly; codegen leaves a manually-set ``e_fn`` alone. Parameters ---------- name : str, optional A user-defined name for the constraint. e_str : str, optional Mathematical expression in canonical CVXPY syntax with the relational operator embedded — ``'<LHS> <= 0'``, ``'<LHS> == 0'``, or ``'<LHS> >= 0'``. Authoring style is LHS-zero: keep all terms on the left so ``.v`` reports slack-from-zero (negative = respected, positive = violated) uniformly across every constraint. CVXPY canonicalizes every inequality to ``lhs - rhs <= 0`` internally regardless of operator direction. Strict ``<`` / ``>`` are forbidden by CVXPY (raises ``NotImplementedError``). e_fn : callable, optional Callable ``e_fn(r) -> cp.Constraint`` taking a :class:`RoutineNS`. Must return a fully-formed ``cp.constraints.Constraint`` (the codegen convention of returning a bare LHS expression is no longer supported — the routine source is now the single source of truth for the relational shape). info : str, optional Additional informational text about the constraint. Attributes ---------- is_disabled : bool Flag indicating if the constraint is disabled, False by default. rtn : ams.routines.Routine The owner routine instance. code : str, optional The code string for the constraint """ e_str = _EFormDescriptor('e_str', 'e_fn') e_fn = _EFormDescriptor('e_fn', 'e_str')
[docs] def __init__(self, name: Optional[str] = None, e_str: Optional[str] = None, e_fn: Optional[Callable] = None, info: Optional[str] = None, ): OptzBase.__init__(self, name=name, info=info) self._e_str = None self._e_fn = None self.e_str = e_str self.e_fn = e_fn self.is_disabled = False self.code = None
[docs] def get_idx(self): raise NotImplementedError
[docs] def get_all_idxes(self): raise NotImplementedError
@ensure_symbols def parse(self): """ Parse the constraint. Validates the LHS-zero authoring shape on every constraint carrying an ``e_str`` (covers both the codegen path — ``e_str`` is preserved on the item — and the eval-fallback path). See :func:`assert_constraint_lhs_zero` for why. Codegen-wired ``e_fn`` items then short-circuit; nothing else to parse here. For the legacy ``e_str`` path, store the source verbatim — symbol rewriting + eval happen together in :func:`ams.opt._runtime_eval.eval_e_str` at evaluate time. ``self.code`` is preserved (as the unrewritten source) so :pyattr:`OptzBase.e` and :meth:`Routine.formulation_summary` still have something to read. """ if self.e_str is not None: assert_constraint_lhs_zero(self, self.e_str) if self.e_fn is not None: return True self.code = self.e_str msg = f" - Constr <{self.name}>: {self.e_str}" logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) return True @ensure_mats_and_parsed def evaluate(self): """ Evaluate the constraint. Both the ``e_fn`` (codegen) and ``e_str`` (eval-fallback) paths must return a fully-formed ``cp.constraints.Constraint`` — the relational operator lives in the source ``e_str`` (or in the body of an author-supplied ``e_fn``), not in any out-of-band flag. ``Constraint.v`` then reads ``self.optz._expr.value`` which CVXPY canonicalizes to the slack-from-zero LHS regardless of whether the author wrote ``<=`` or ``>=``. """ if self.e_fn is not None: try: result = self.e_fn(RoutineNS(self.om.rtn)) except Exception as e: raise Exception(f"Error in evaluating Constraint <{self.name}> " f"via e_fn.\n{e}") else: msg = f" - Constr <{self.name}>: {self.e_str}" logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) result = eval_e_str(self, self.e_str) if not isinstance(result, cp.constraints.Constraint): source = f"e_str={self.e_str!r}" if self.e_str is not None else "e_fn" raise TypeError( f"Constraint <{self.name}>: {source} must produce a " f"cp.constraints.Constraint (embed the relational " f"operator: '<LHS> <= 0', '<LHS> == 0', or '<LHS> >= 0'). " f"Got {type(result).__name__}. Stale " f"~/.ams/pycode/ from before the v1.2.3 is_eq retirement " f"is auto-invalidated by PYCODE_FORMAT_VERSION; if you see " f"this from a freshly-regenerated cache, the underlying " f"e_str is missing its trailing operator." ) self.optz = result return True def __repr__(self): enabled = 'OFF' if self.is_disabled else 'ON' out = f"{self.class_name}: {self.name} [{enabled}]" return out @property def v(self): """ Return the CVXPY constraint LHS value. """ if self.optz is None: return None if self.optz._expr.value is None: # Solver hasn't run / didn't converge; return a zeros array # of the right shape rather than ``None`` so callers doing # post-solve diagnostics get an array of the expected size. try: return np.zeros(self.optz._expr.shape) except AttributeError: return None return self.optz._expr.value @v.setter def v(self, value): raise AttributeError("Cannot set the value of the constraint.")