from jet20.frontend.expression import Expression, Constraint
from jet20.frontend.variable import Variable, Array
from jet20.frontend.const import *
from jet20.frontend.backends import jet20_default_backend_func
from typing import List, Union, Callable
from functools import wraps
import numpy as np
import re
[docs]def assert_power(add_expr):
"""
"""
@wraps(add_expr)
def check(self, *constraints: Constraint):
"""
"""
for c in constraints:
if isinstance(c, Constraint) and c.canonicalize()[0].highest_order > 1:
raise NotImplementedError("power exceed")
return add_expr(self, *constraints)
return check
[docs]class Problem(object):
__default_solvers__ = {
"jet20.backend": jet20_default_backend_func,
}
def __init__(self, name: str = ""):
self.name = name
self._subject: Union[Expression, None] = None
self._constraints: List[Constraint] = []
self._variables: List[Variable] = []
self._solver = {} # 'solver_name': function
self._solver.update(**self.__default_solvers__)
@property
def variables_count(self) -> int:
"""
Returns: The numbers of variables this problem has contained
"""
return len(self._variables)
[docs] def variable(self, name: str, lb: Union[None, float] = None, ub: Union[None, float] = None) -> Variable:
"""Adding single variable with the name annotation.
Args:
name: The name of a variable
Returns:
A new variable
"""
_var = Variable(self.variables_count, name) # len(variables) exactly be equal with next variable's index
self._variables += [_var]
if lb is float:
self._constraints += [Constraint(_var, lb, OP_GE)]
if ub is float:
self._constraints += [Constraint(_var, ub, OP_LE)]
return _var
[docs] def variables(self, symbols: str, lb: Union[None, List[float], float] = None,
ub: Union[None, List[float], float] = None) -> List[Variable]:
"""Adding a batch variables.
Example: variables("x y z")
Args:
symbols: A set of in-order potential variable names, the separator is blank or comma or semicolon[ ,;]
Returns:
Variables, and their name is attached with per symbol
"""
# if type(lb) not in (None, list, float) or type(ub) not in (None, list, float):
# raise TypeError("bounds must be list of floats, float or None")
_var_names = list(filter(None, re.split("[ ,;]", symbols)))
if isinstance(lb, list) and len(_var_names) != len(lb):
raise ValueError("mismatch length of lower bounds vector and variables vector")
if isinstance(ub, list) and len(_var_names) != len(ub):
raise ValueError("mismatch length of upper bounds vector and variables vector")
if lb is None or isinstance(lb, (float, int)):
lb = [lb] * len(_var_names)
if ub is None or isinstance(ub, (float, int)):
ub = [ub] * len(_var_names)
_vars = Array()
for symbol, lb, ub in zip(_var_names, lb, ub):
_var = Variable(self.variables_count, symbol, lb, ub)
_vars.append(_var)
self._variables.append(_var)
return _vars
[docs] def minimize(self, expr: Union[Expression, Array]):
"""Add the object math expression of this problem for minimizing it's value.
Args:
expr: A instance of Expression.
Returns:
"""
if isinstance(expr, Expression):
self._subject = expr
elif isinstance(expr, Array) and expr.len == 1:
self._subject = expr.array[0]
else:
raise TypeError("expr must be a Expression or a Array")
[docs] def maximize(self, expr: Union[Expression, Array]):
"""Add the object math expression of this problem for maximizing it's value.
Args:
expr: A instance of Expression.
Returns:
"""
if isinstance(expr, Expression):
self._subject = -expr
elif isinstance(expr, Array) and expr.len == 1:
self._subject = -expr.array[0]
else:
raise TypeError("expr must be a Expression or a Array")
[docs] @assert_power
def constraints(self, *constraints: Union[Array, Constraint]):
"""Add a constraint.
Args:
constraints: A instance of Constraint, insists of a left value, an operator and a right value.
Returns:
"""
for cons in constraints:
if isinstance(cons, Constraint):
self._constraints += [cons]
elif isinstance(cons, Array):
self._constraints += [con for con in cons.array if isinstance(con, Constraint)]
@property
def canonical(self):
"""Return the canonical form of this problem.
Returns: (object express matrix, list of constraint tuples).
[[1. 0. 0. 0. ]
[0. 2. 0. 0.5] [[5. 4.0 4.3 0. ]
[0. 0. 0. 0.5] [0. 3.2 0. 2.1]
( [0. 0.5 0.5 0. ]], [1. 2. 3. 4. ]], ["<","<=",...], [const1,const2,...])
^ ^ ^ ^
| | | |
object constraits ops consts
"""
for _var in self._variables:
if _var.lb is not None:
self._constraints.append(Constraint(_var, _var.lb, OP_GE))
if _var.ub is not None:
self._constraints.append(Constraint(_var, _var.ub, OP_LE))
_obj = self._subject.core_mat # need cut const off here?
exprs, ops = list(zip(*[con.canonicalize() for con in self._constraints])) # unzip constraints, ops
_constraints = np.stack(
[con.expand_linear_vector(len(self._variables) + 1)[:-1] for con in exprs]) # cut const off
_ops = np.array(ops)
_consts = np.array([-con.const for con in exprs])
return _obj, _constraints, _ops, _consts
[docs] def solve(self, name: str = "jet20.backend", *args, **kwargs):
"""Calling one of the registered solvers to solve the problem., jet20.backend will be used by default.
:param name: One of the registered solvers's name.
:type name: str
:param args: Extra args depends on the solver
:param kwargs: Extra args depends on the solver
:return: solution of the problem depends on the solver
for jet20.backend following args can be used:
:param x: initial solution of the problem
:type x: list,numpy.ndarray
:param opt_u: hyperparameters for interior point method
:type opt_u: float
:param opt_alpha: hyperparameters for line search
:type opt_alpha: float
:param opt_beta: hyperparameters for line search
:type opt_beta: float
:param opt_tolerance: objective value tolerance
:type opt_tolerance: float
:param opt_constraint_tolerance: feasibility tolerance
:type opt_constraint_tolerance: float
:param rouding_precision: rouding precision
:type rouding_precision: int
:param force_rouding: whether force rounding
:type rouding_precision: bool
:return: solution of the problem
:rtype: Solution
"""
return self._solver[name](self, *args, **kwargs)
[docs] def reg_solver(self, name: str, func: Callable):
"""Register a solver, it will be called to solve the problem later.
Args:
name: Name annotation of this solver.
func: A function implemented to solve the problem.
Returns:
"""
self._solver[name] = func