Source code for jet20.frontend.expression

"""

"""
from numbers import Real
from typing import Union
import numpy as np
from jet20.frontend.const import *


[docs]class Expression(object): """express a mathematical expression or formula in a square matrix form As shown in the following matrix, the [[0. 0. 0. 0. ] [0. 0. 0. 0.5] [0. 0. 0. 0.5] [0. 0.5 0.5 0. ]] The Expression overloads +, -, *, /, -(neg), **, ==, <, <=, >, >= operators for a convenient way to creating expressions. """ def __init__(self, mat: Union[np.ndarray, None]): if mat.ndim != 2: raise TypeError("mat must be a 2 dimension matrix") self._core_mat = mat @property def core_mat(self) -> np.ndarray: return self._core_mat # @op.setter # def op(self, op: str): # self._op = op # @property # def is_constraint(self) -> bool: # return self.op != '' @property def shape(self) -> tuple: """ Returns: The shape of the core matrix """ return self.core_mat.shape @property def dim(self) -> int: """ Returns: The dim of the core matrix(core matrix must be a square matrix) """ return self.shape[0] @property def total_variables(self) -> int: """ Returns: The total numbers of variables contained in Expression """ return self.dim - 1 @property def highest_order(self) -> int: """ Returns: The highest power of Expression """ # if not np.array_equal(self._quadratic_matrix, np.zeros(self._quadratic_matrix.shape)): if not (self._quadratic_matrix == 0.0).all(): return 2 # elif not np.array_equal(self._linear_partial_sum, np.zeros(self._linear_partial_sum.shape)): elif not (self._linear_partial_sum == 0.0).all(): return 1 else: return 0 @property def _quadratic_matrix(self) -> np.ndarray: """ Returns: A partial matrix from the core matrix for the parameters of quadratic expression As shown in the following matrix, the returned matrix is in the brackets annotation [[0. 0. 0.] 0. 0. 0. 0. [0. 0. 0.] 0.5 => 0. 0. 0. [0. 0. 0.]] 0.5 0. 0. 0. 0. 0.5 0.5 0. """ return self.core_mat[:self.shape[0] - 1, :self.shape[1] - 1] @property def _linear_row_partial(self) -> np.ndarray: """ Returns: A vector extracted form the core matrix for row partial of linear expr's parameters. The value in this vector means the (correct parameter)/2. As shown in the following matrix, the returned vector is in the brackets annotation 0. 0. 0. 0. 0. 0. 0. 0.5 => [0., 0.5, 0.5] 0. 0. 0. 0.5 [0. 0.5 0.5] 0. """ return self.core_mat[self.shape[0] - 1, :self.shape[0] - 1] @property def _linear_col_partial(self) -> np.ndarray: """ Returns: A vector extracted form the core matrix for column partial of linear expr's parameters. The value in this vector means the (correct parameter)/2. As shown in the following matrix, the returned vector is in the brackets annotation 0. 0. 0. [0. ] 0. 0. 0. [0.5] => [0., 0.5, 0.5] 0. 0. 0. [0.5] 0. 0.5 0.5 0. """ return self.core_mat[:self.shape[0] - 1, self.shape[0] - 1] @property def _linear_partial_sum(self) -> np.ndarray: """ Returns: A vector for added row and column partial of the the linear variable parameters. As shown in the following matrix, the returned vector is the sum of the two brackets annotated vectors. 0. 0. 0. [0.] 0. 0. 0. [0.5] => [0., 1., 1.] 0. 0. 0. [0.5] [0. 0.5 0.5] 0. """ return self._linear_row_partial + self._linear_col_partial @property def const(self) -> Real: """ Returns: The value of const in the expression. As shown in the following matrix, the returned value is the brackets annotation 0. 0. 0. 0. 0. 0. 0. 0.5 => 0. 0. 0. 0. 0.5 0. 0.5 0.5 [0.] """ return self.core_mat[self.shape[0] - 1, self.shape[1] - 1] @property def linear_complete_vector(self) -> np.ndarray: """'complete' means the returned linear vector contains const value Returns: A vector for complete parameters of the linear variable and const value As shown in the following matrix, the returned vector is the brackets annotation 0. 0. 0. [0.] 0. 0. 0. [0.5] => [0., 1., 1., 0.] 0. 0. 0. [0.5] [0. 0.5 0.5] [0.] """ return np.append(self._linear_partial_sum, self.const) @staticmethod def _expand_vector(vec: np.ndarray, n: int) -> np.ndarray: """Expand a vector to (n,1) shape with zeros Args: n: expand to n*1 dim(n must larger than the length of vec). vec: Vector will be expand. Returns: A new np.ndarray expanded base on the input vec """ if len(vec) > n: raise ValueError("expand to a shorter vector than itself is not allowed") if len(vec) == n: return vec base = np.zeros(n) base[:len(vec)] = vec return base
[docs] def expand_linear_vector(self, n: int) -> np.ndarray: """ Args: n: expand to n*1 dim. Returns: A new linear parameters vector expanded with zeros(compounded with const) """ return np.append(self._expand_vector(self._linear_partial_sum, n - 1), self.const)
[docs] def expand(self, n: int) -> 'Expression': """Get a new Expression which's core matrix has been expanded to n*n dim. In Mathematically speaking, expand means add variables into expression. Args: n: expand to n*n dim Returns: A new core matrix has been expanded to n*n dim """ if n < self.dim: raise ValueError("expand dim less than the current is not allowed") if n == self.dim: return self base = np.zeros((n, n)) _q_mat = self._quadratic_matrix base[:_q_mat.shape[0], :_q_mat.shape[1]] = _q_mat # cover quadratic paras back # cover linear row partial back base[base.shape[0] - 1, :self._linear_row_partial.shape[0]] = self._linear_row_partial # cover linear column partial back base[:self._linear_col_partial.shape[0], base.shape[1] - 1] = self._linear_col_partial base[base.shape[0] - 1, base.shape[1] - 1] = self.const # cover const back return Expression(base)
[docs] def equal(self, other: 'Expression') -> bool: if isinstance(self.core_mat, np.ndarray) and isinstance(other.core_mat, np.ndarray): return np.array_equal(self.core_mat, other.core_mat) return False
def __add__(self, other: Union[Real, 'Expression']) -> 'Expression': if isinstance(other, (int, float)): # Expression + number base = self.core_mat.copy() base[base.shape[0] - 1, base.shape[1] - 1] = other return Expression(base) elif isinstance(other, Expression): # Expression + Expression # align _self = self.core_mat _other = other.core_mat if self.dim < other.dim: _self = self.expand(other.dim).core_mat else: _other = other.expand(self.dim).core_mat return Expression(_self + _other) else: raise NotImplementedError( f"unsupported operand type(s) for +: {type(self).__name__} and {type(other).__name__}") def __radd__(self, other: 'Expression') -> 'Expression': return self + other def __sub__(self, other: 'Expression') -> 'Expression': if not isinstance(other, (Expression, int, float)): raise NotImplementedError( f"unsupported operand type(s) for -: {type(self).__name__} and {type(other).__name__}") return self + -other def __rsub__(self, other: 'Expression'): return -self + other def __mul__(self, other: Union[Real, 'Expression']) -> 'Expression': if isinstance(other, (int, float)): # number * Expression return Expression(self.core_mat * other) elif isinstance(other, Expression): # Expression * Expression if self.highest_order > 1 or other.highest_order > 1: raise NotImplementedError( f"the result will exceed quadratic, which is unsupported now") _self = self.linear_complete_vector _other = other.linear_complete_vector # align two vectors if len(_self) < len(_other): _self = self.expand_linear_vector(len(_other)) else: _other = other.expand_linear_vector(len(_self)) base = np.outer(_self, _other) return Expression((base.transpose() + base) / 2) else: raise NotImplementedError( f"unsupported operand type(s) for *: {type(self).__name__} and {type(other).__name__}") def __rmul__(self, other) -> 'Expression': return self * other # all kinds of div situations are unsupported # def __div__(self, other) -> 'Expression': # pass # 一次幂除以一次幂? # # def __rdiv__(self, other) -> 'Expression': # return other / self def __pow__(self, power: int, modulo=None) -> 'Expression': if self.highest_order > 1: raise NotImplementedError("the result will exceed quadratic, which is unsupported now") _self = self.linear_complete_vector base = np.outer(_self, _self) return Expression((base.transpose() + base) / 2) def __neg__(self) -> 'Expression': return self * -1 def __eq__(self, other) -> 'Constraint': return Constraint(self, other, OP_EQUAL) def __ge__(self, other) -> 'Constraint': return Constraint(self, other, OP_GE) def __gt__(self, other) -> 'Constraint': return Constraint(self, other, OP_GT) def __le__(self, other) -> 'Constraint': return Constraint(self, other, OP_LE) def __lt__(self, other) -> 'Constraint': return Constraint(self, other, OP_LE) def __str__(self): return f"{self.core_mat.__str__()} (mat)" __repr__ = __str__
[docs]class Constraint(object): def __init__(self, lh: Union[Expression, float], rh: Union[Expression, float], op: OP): self._lh: Union[Expression, float] = lh self._rh: Union[Expression, float] = rh self._op: OP = op @property def lh(self) -> Expression: return self._lh @property def rh(self) -> Expression: return self._rh @property def op(self) -> str: return self._op
[docs] def with_op(self, op: str) -> 'Constraint': self._op = op return self
[docs] def canonicalize(self) -> (Expression, OP): expr = self._lh - self._rh # lh > rh => -(lh-rh) < 0 # lh < rh => lh-rh < 0 return (-expr, OP_PAIRS[self.op]) if self.op in OP_PAIRS else (expr, self.op)