- """
- Four useful classes are provided in this module.
- ConstraintsMixin
- Converts equality constraints to penalties. Depends on the value provided
- for penalty_multiplier.
- InequalitiesMixin
- Allows inequality constraints, converting to equality constraints before
- penalties by adding slack variables.
- ConstraintModel
- An example implementation of the ConstraintsMixin.
- InequalityConstraintModel
- An example implementation of the InequalitiesMixin.
- >>> lhs = np.array([[1, 1],
- ... [2, 2]])
- >>> rhs = np.array([1, 1])
- >>> senses = ["LE", "GE"]
- >>> model = InequalityConstraintModel()
- >>> model.constraints = lhs, rhs
- >>> model.senses = senses
- >>> A, b = model.constraints
- >>> A
- array([[ 1., 1., 1., 0.],
- [ 2., 2., 0., -1.]])
- >>> model.penalty_multiplier = 1.0
- >>> model.checkPenalty(np.array([1, 0, 0, 1]))
- 0.0
- >>> model.checkPenalty(np.array([1, 1, 0, 0]))
- 10.0
- """
- from typing import (List, Tuple)
- import numpy as np
- from eqc_models.base.base import EqcModel
- class ConstraintsMixIn:
- """
- This mixin class contains methods and attributes which transform
- linear constraints into penalties.
-
- """
- lhs = None
- rhs = None
- alpha = 1
- linear_objective = None
- quad_objective = None
- @property
- def penalties(self) -> Tuple[np.ndarray, np.ndarray]:
- """ Returns two numpy arrays, one linear and one quadratic pieces of an operator """
- lhs, rhs = self.constraints
- if lhs is None or rhs is None:
- raise ValueError("Constraints lhs and/or rhs are undefined. " +
- "Both must be instantiated numpy arrays.")
- Pq = lhs.T @ lhs
- Pl = -2 * rhs.T @ lhs
- return Pl.T, Pq
-
- @property
- def penalty_multiplier(self) -> float:
- return self.alpha
-
- @penalty_multiplier.setter
- def penalty_multiplier(self, value: float):
- self.alpha = value
- @property
- def constraints(self):
- return self.lhs, self.rhs
-
- @constraints.setter
- def constraints(self, value: Tuple[np.ndarray, np.ndarray]):
- self.lhs, self.rhs = value
- @property
- def offset(self) -> float:
- """ Calculate the offset due to the conversion of constraints to penalties """
- lhs, rhs = self.constraints
- return np.squeeze(rhs.T@rhs)
- def evaluate(self, solution : np.ndarray, alpha : float = None, includeoffset:bool=False):
- """
- Compute the objective value plus penalties for the given solution. Including
- the offset will ensure the penalty contribution is non-negative.
- Parameters
- ----------
- solution : np.array
- The solution vector for the problem.
- alpha : float
- Penalty multiplier, optional. This can be used to test different
- multipliers for determination of sufficiently large values.
- """
- if alpha is None:
- alpha = self.penalty_multiplier
- penalty = self.evaluatePenalties(solution)
- penalty *= alpha
- if includeoffset:
- penalty += alpha * self.offset
- return penalty + self.evaluateObjective(solution)
- def evaluatePenalties(self, solution) -> float:
- """
- Evaluate penalty function without alpha or offset
- Parameters
- ----------
- solution : np.array
- The solution vector for the problem.
- """
- Pl, Pq = self.penalties
- qpart = solution.T@Pq@solution
- lpart = Pl.T@solution
- ttlpart = qpart + lpart
- return ttlpart
- def checkPenalty(self, solution : np.ndarray):
- """
- Get the penalty of the solution.
- Parameters
- ----------
- solution : np.array
- The solution vector for the problem.
- """
- penalty = self.evaluatePenalties(solution)
- penalty += self.penalty_multiplier * self.offset
- assert penalty >= 0, "Inconsistent model, penalty cannot be less than 0."
- return penalty
- class InequalitiesMixin:
- """
- This mixin enables inequality constraints by automatically
- generating slack variables for each inequality
-
- This mixin adds a `senses` attribute which has a value for each
- constraint. The values are one of 'EQ', 'LE' or 'GE' for equal
- to, less than or equal to or greater than or equal to. The effect
- of the value is to control whether a slack is added and what
- the sign of the slack variable in the constraint is. Negative
- is used for GE, positive is used for LE and all slack variables
- get a coefficient magnitude of 1.
- The constraints are modified on demand, so the class members,
- `lhs` and `rhs` remain unmodified.
- """
- _senses = None
- @property
- def senses(self) -> List[str]:
- """ Comparison operator by constraint """
- return self._senses
-
- @senses.setter
- def senses(self, value : List[str]):
- self._senses = value
- @property
- def num_slacks(self) -> int:
- """
- The number of slack variables. Will match the number of inequality
- constraints.
- Returns
- -------
- number : int
- """
- G = self.lhs
- m = G.shape[0]
- senses = self.senses
- num_slacks = sum([0 if senses[i] == "EQ" else 1 for i in range(m)])
- return num_slacks
-
- @property
- def constraints(self) -> Tuple[np.ndarray, np.ndarray]:
- """
- Get the general form of the constraints, add slacks where needed
- and return a standard, equality constraint form.
-
- """
- G = self.lhs
- h = self.rhs
- senses = self.senses
- m = G.shape[0]
- n = G.shape[1]
- num_slacks = self.num_slacks
-
- slack_vars = np.zeros((m, num_slacks))
- ii = 0
- for i in range(m):
- rule = senses[i]
- if rule == "LE":
-
- slack_vars[i, ii] = 1
- ii += 1
- elif rule == "GE":
-
- slack_vars[i, ii] = -1
- ii += 1
- A = np.hstack((G, slack_vars))
- b = h
- return A, b
-
- @constraints.setter
- def constraints(self, value : Tuple[np.ndarray, np.ndarray]):
- if len(value) != 2:
- raise ValueError("Constraints must be specified as a 2-tuple")
- self.lhs, self.rhs = value
- class ConstraintModel(ConstraintsMixIn, EqcModel):
- """
- Abstract class for representing linear constrained optimization problems as
- EQC models.
-
- """
- class InequalityConstraintModel(InequalitiesMixin, ConstraintModel):
- """
- Abstract class for a linear constrained optimization model with inequality constraints
- """