import abc
[docs]class Expression(abc.ABC):
"""Allows us to build expressions for :class:`Constraints <.Constraint>` using common operators.
The following operators are supported:
* addition: `x + y`
* subtraction: `x - y`
* multiplication: `x * y`
* division: `x / y`
* floor division: `x // y`
* remainder: `x % y`
* exponentiation: `x ** y`
* equality: `x == y`
* inequality: `x != y`
* greater than: `x > y`
* greater than or equal: `x >= y`
* less than: `x < y`
* less than or equal: `x <= y`
* and: `x & y`
* or: `x | y`
Example:
`((param1 + param2 <= param3) & param4.is_present()) | param5 == 'abc'`
Note the use of brackets with the `&` and `|` operators - this is because they don't have the same precedence as
`and` and `or` so you need to be explicit in order to generate the correct expression.
See `this page <../constraints.ipynb>`_ for more examples.
Refer to the OPTaaS swagger page for more details on constraint syntax.
"""
def __add__(self, other) -> 'Expression':
return BinaryPredicate(self, '+', other)
def __radd__(self, other) -> 'Expression':
return BinaryPredicate(other, '+', self)
def __sub__(self, other) -> 'Expression':
return BinaryPredicate(self, '-', other)
def __rsub__(self, other) -> 'Expression':
return BinaryPredicate(other, '-', self)
def __mul__(self, other) -> 'Expression':
return BinaryPredicate(self, '*', other)
def __rmul__(self, other) -> 'Expression':
return BinaryPredicate(other, '*', self)
def __truediv__(self, other) -> 'Expression':
return BinaryPredicate(self, '/', other)
def __rtruediv__(self, other) -> 'Expression':
return BinaryPredicate(other, '/', self)
def __floordiv__(self, other) -> 'Expression':
return BinaryPredicate(self, '//', other)
def __rfloordiv__(self, other) -> 'Expression':
return BinaryPredicate(other, '//', self)
def __mod__(self, other) -> 'Expression':
return BinaryPredicate(self, '%', other)
def __rmod__(self, other) -> 'Expression':
return BinaryPredicate(other, '%', self)
def __pow__(self, other) -> 'Expression':
return BinaryPredicate(self, '**', other)
def __rpow__(self, other) -> 'Expression':
return BinaryPredicate(other, '**', self)
def __eq__(self, other) -> 'Expression': # type: ignore
return BinaryPredicate(self, '==', other)
def __ne__(self, other) -> 'Expression': # type: ignore
return BinaryPredicate(self, '!=', other)
def __gt__(self, other) -> 'Expression':
return BinaryPredicate(self, '>', other)
def __lt__(self, other) -> 'Expression':
return BinaryPredicate(self, '<', other)
def __ge__(self, other) -> 'Expression':
return BinaryPredicate(self, '>=', other)
def __le__(self, other) -> 'Expression':
return BinaryPredicate(self, '<=', other)
def __and__(self, other) -> 'Expression':
return BinaryPredicate(self, '&&', other)
def __or__(self, other) -> 'Expression':
return BinaryPredicate(self, '||', other)
[docs] @abc.abstractmethod
def to_optaas_expression(self) -> str:
pass
[docs]class Predicate(Expression):
"""An Expression that evaluates to a boolean value that can be used in a :class:`.Constraint`"""
def __init__(self, optaas_expression: str) -> None:
self._optaas_expression = optaas_expression
[docs] def to_optaas_expression(self) -> str:
"""A string representation of this expression in a format which can be parsed by OPTaaS"""
return self._optaas_expression
[docs]class BinaryPredicate(Predicate):
"""A Predicate with 2 operands"""
def __init__(self, left_operand, operator: str, right_operand) -> None:
super().__init__(f'{_format(left_operand)} {operator} {_format(right_operand)}')
[docs]class UnaryPredicate(Predicate):
"""A Predicate with a single operand"""
def __init__(self, operand, operator: str) -> None:
super().__init__(f'{_format(operand)} {operator}')
[docs]class WhenThenExpression(abc.ABC):
"""
Defines a when expression, then expression clause:
The "when" part is optional (if omitted, the then part applies).
NOTE: At the MFOpt level, WhenThenExpressions get transformed to either constraints or prior mean functions, which
means the "then" expressions have to conform to some formats: they have to be boolean for constraints, and not
boolean for prior means.
"""
def __init__(self, then: Expression, when: Expression = None) -> None:
self.then = then
self.when = when
[docs] def to_optaas_expression(self) -> str:
return (self.then.to_optaas_expression() if self.when is None
else f'if {self.when.to_optaas_expression()} then {self.then.to_optaas_expression()}')
def __eq__(self, other):
return isinstance(other, WhenThenExpression) and self.to_optaas_expression() == other.to_optaas_expression()
def __repr__(self):
return self.to_optaas_expression()
[docs]class Constraint(WhenThenExpression):
"""
This is an alias for the WhenThenExpression class which defines a when expression, then expression clause:
The "when" part is optional (if omitted, the then part applies).
"""
def __eq__(self, other):
return isinstance(other, Constraint) and self.to_optaas_expression() == other.to_optaas_expression()
[docs]class PriorMeanExpression(WhenThenExpression):
"""
This is an alias for the WhenThenExpression class which defines a when expression, then expression clause:
The "when" part is optional (if omitted, the then part applies).
"""
def __eq__(self, other):
return isinstance(other, PriorMeanExpression) and self.to_optaas_expression() == other.to_optaas_expression()
def _format(operand):
from mindfoundry.optaas.client.parameter import Parameter # pylint: disable=import-outside-toplevel
if operand is True:
operand = 'true'
elif operand is False:
operand = 'false'
elif isinstance(operand, Parameter):
operand = operand.to_optaas_expression()
elif isinstance(operand, str):
operand = f"'{operand}'"
elif isinstance(operand, BinaryPredicate):
operand = '( ' + operand.to_optaas_expression() + ' )'
elif isinstance(operand, UnaryPredicate):
operand = operand.to_optaas_expression()
return operand