Skip to content

Feature: Merge Expr and GenExpr #1074

@Zeroto521

Description

@Zeroto521

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

The relationships between Variable, Term, Expr and GenExpr are complex.

  • Variable: Variable is the element of vars in Term, that's logical. But Variable is the subclass of Expr, that's counter-intuitive. Although it's used to get operation method like +.
  • Expr: Two of classes to represent expression, Expr and GenExpr. Expr is made for the simple expression of Term. GenExpr is made for the expresssion of Expr.
  • __repr__: The results are different for Expr and GenExpr.
  • Expr.terms and GenExpr.children: One is dict and another is list.
Image

Describe the solution you'd like
A clear and concise description of what you want to happen.

Redefine the class

  • Variable: Container for scip variable. Any operatoin to itself, it will return a Expr.
  • Term: It's the same as old.
  • Expr: the base expression class.
  • SumExpr: The function is the same. But it's also a base class made for + operation.
  • PolynomialExpr: It's equal to the old Expr.
  • MonomialExpr: Its function is above the old VarExpr. VarExpr is a single Variable. MonomialExpr is a single Term.
  • ConstExpr: It's equal to the old Const. But it has a new name to keep the same style.
  • FuncExpr: It's the base class to contain any functional expression.
  • Expr.children: Merge old .terms and .children to one. Use dict as the base container.
Image
    x1 = Variable()
    x2 = Variable()
    print((1 / x1) + 2 * (1 / x1))
    # ProdExpr({(Expr({Term(): 1}), PowExpr((Expr({Term(x_1): 1.0}),), -1.0)): 3.0})
    print((x1 + x2 + 1) ** 2)
    # Expr({Term(x_1, x_1): 1.0, Term(x_1, x_2): 2.0, Term(x_1): 2.0, Term(x_2, x_2): 1.0, Term(x_2): 2.0, Term(): 1.0})
A demo solution
from itertools import count

id_counter = count(start=1)


def _get_var():
    return f"x_{next(id_counter)}"


def _is_number(x) -> bool:
    """Check if x is a number."""

    try:
        float(x)
        return True
    except ValueError:
        return False
    except TypeError:
        return False


class Variable:
    # def __init__(self, scip_var):
    def __init__(self):
        # self.scip_var = scip_var
        self.scip_var = _get_var()

    # property name:
    #     def __get__(self):
    #         return bytes(SCIPvarGetName(self.scip_var)).decode("utf-8")

    def ptr(self):
        # return <size_t>(self.scip_var)
        return self.scip_var

    def __repr__(self):
        return str(self.scip_var)
        # return self.name

    def __add__(self, other):
        return self.to_expr().__add__(other)

    def __iadd__(self, other):
        self = self.__add__(other)
        return self

    def __radd__(self, other):
        return self.to_expr().__radd__(other)

    def __mul__(self, other):
        return self.to_expr().__mul__(other)

    def __rmul__(self, other):
        return self.to_expr().__rmul__(other)

    def __truediv__(self, other):
        return self.to_expr().__truediv__(other)

    def __rtruediv__(self, other):
        return self.to_expr().__rtruediv__(other)

    def __pow__(self, other):
        return self.to_expr().__pow__(other)

    def __neg__(self):
        return self.to_expr().__neg__()

    def __sub__(self, other):
        return self.to_expr().__sub__(other)

    def __rsub__(self, other):
        return self.to_expr().__rsub__(other)

    def __richcmp__(self, other, op: int):
        return self.to_expr().__richcmp__(other, op)

    def to_expr(self):
        return MonomialExpr.from_var(self)


class Term:
    """A monomial term consisting of one or more variables."""

    __slots__ = ("vars", "ptrs")

    def __init__(self, *vars):
        self.vars = tuple(sorted(vars, key=lambda v: v.ptr()))
        self.ptrs = tuple(v.ptr() for v in self.vars)

    def __getitem__(self, idx):
        return self.vars[idx]

    def __hash__(self):
        return self.ptrs.__hash__()

    def __eq__(self, other):
        return self.ptrs == other.ptrs

    def __len__(self):
        return len(self.vars)

    def __mul__(self, other):
        if not isinstance(other, Term):
            raise TypeError(
                f"unsupported operand type(s) for *: 'Term' and '{type(other)}'"
            )
        return Term(*self.vars, *other.vars)

    def __repr__(self):
        return f"Term({', '.join(map(str, self.vars))})"


CONST = Term()


class Expr:
    """Expression representing for `expression1 + expression2 + constant`."""

    # cdef public children

    def __init__(self, children: dict = None):
        self.children = children or {}

    def __hash__(self):
        return frozenset(self.children.items()).__hash__()

    def __getitem__(self, key):
        return self.children.get(key, 0.0)

    def __iter__(self):
        return iter(self.children)

    def __next__(self):
        try:
            return next(self.children)
        except:
            raise StopIteration

    def __add__(self, other):
        other = Expr.to_const_or_var(other)
        if isinstance(other, Expr):
            return SumExpr({self: 1.0, other: 1.0})
        # elif isinstance(other, MatrixExpr):
        #     return other.__add__(self)
        raise TypeError(
            f"unsupported operand type(s) for +: 'Expr' and '{type(other)}'"
        )

    def __mul__(self, other):
        other = Expr.to_const_or_var(other)
        if isinstance(other, Expr):
            return ProdExpr(self, other)
        # elif isinstance(other, MatrixExpr):
        #     return other.__mul__(self)
        raise TypeError(
            f"unsupported operand type(s) for *: 'Expr' and '{type(other)}'"
        )

    def __truediv__(self, other):
        other = Expr.to_const_or_var(other)
        if isinstance(other, ConstExpr) and other[CONST] == 0:
            raise ZeroDivisionError("division by zero")
        if hash(self) == hash(other):
            return ConstExpr(1.0)
        return self.__mul__(other.__pow__(-1.0))

    def __rtruediv__(self, other):
        return Expr.to_const_or_var(other).__truediv__(self)

    def __pow__(self, other):
        other = Expr.to_const_or_var(other)
        if not isinstance(other, ConstExpr):
            raise TypeError("exponent must be a number")

        if other[CONST] == 0:
            return ConstExpr(1.0)
        return PowExpr(self, other[CONST])

    def __sub__(self, other):
        return self.__add__(-other)

    def __neg__(self):
        return self.__mul__(-1.0)

    def __iadd__(self, other):
        self = self.__add__(other)
        return self

    def __radd__(self, other):
        return self.__add__(other)

    def __rmul__(self, other):
        return self.__mul__(other)

    def __rsub__(self, other):
        return self.__neg__().__add__(other)

    def __richcmp__(self, other, op: int):
        other = Expr.to_const_or_var(other)

        if op == 1:  # <=
            if isinstance(other, Expr):
                if isinstance(other, ConstExpr):
                    return ExprCons(self, rhs=other[CONST])
                return (self - other) <= 0
            # elif isinstance(other, MatrixExpr):
            #     return self.__richcmp__(other, 5)
            raise TypeError(f"Unsupported type {type(other)}")

        elif op == 5:  # >=
            if isinstance(other, Expr):
                if isinstance(other, ConstExpr):
                    return ExprCons(self, lhs=other[CONST])
                return (self - other) >= 0
            # elif isinstance(other, MatrixExpr):
            #     return self.__richcmp__(other, 1)
            raise TypeError(f"Unsupported type {type(other)}")

        elif op == 2:  # ==
            if isinstance(other, Expr):
                if isinstance(other, ConstExpr):
                    return ExprCons(self, lhs=other[CONST], rhs=other[CONST])
                return (self - other) == 0
            # elif isinstance(other, MatrixExpr):
            #     return self.__richcmp__(other, 2)
            raise TypeError(f"Unsupported type {type(other)}")

        raise NotImplementedError(
            "Can only support constraints with '<=', '>=', or '=='."
        )

    def __repr__(self):
        return f"Expr({self.children})"

    def degree(self):
        """Computes the highest degree of children"""

        return max(map(len, self.children)) if self.children else 0

    @staticmethod
    def to_const_or_var(x):
        """Convert a number or variable to an expression."""

        if _is_number(x):
            return PolynomialExpr.to_subclass({CONST: x})
        elif isinstance(x, Variable):
            return PolynomialExpr.to_subclass({Term(x): 1.0})
        return x

    def to_dict(self, other: dict = None) -> dict:
        """Merge two dictionaries by summing values of common keys"""
        other = other or {}
        if not isinstance(other, dict):
            raise TypeError("other must be a dict")

        res = self.children.copy()
        for child, coef in other.items():
            res[child] = res.get(child, 0.0) + coef

        return res


class SumExpr(Expr):
    def __add__(self, other):
        other = Expr.to_const_or_var(other)
        if isinstance(other, SumExpr):
            return SumExpr(self.to_dict(other.children))
        return super().__add__(other)

    def __mul__(self, other):
        other = Expr.to_const_or_var(other)
        if isinstance(other, ConstExpr):
            if other[CONST] == 0:
                return ConstExpr(0.0)
            return SumExpr({i: self[i] * other[CONST] for i in self if self[i] != 0})
        return super().__mul__(other)


class PolynomialExpr(SumExpr):
    """Expression representing for `2*x**3 + 4*x*y + constant`."""

    def __init__(self, children: dict = None):
        if children and not all(isinstance(t, Term) for t in children):
            raise TypeError("All keys must be Term instances")

        super().__init__(children)

    def __add__(self, other):
        other = Expr.to_const_or_var(other)
        if isinstance(other, PolynomialExpr):
            return PolynomialExpr.to_subclass(self.to_dict(other.children))
        return super().__add__(other)

    def __mul__(self, other):
        other = Expr.to_const_or_var(other)
        if isinstance(other, PolynomialExpr):
            children = {}
            for i in self:
                for j in other:
                    child = i * j
                    children[child] = children.get(child, 0.0) + self[i] * other[j]
            return PolynomialExpr.to_subclass(children)
        return super().__mul__(other)

    def __truediv__(self, other):
        other = Expr.to_const_or_var(other)
        if isinstance(other, ConstExpr):
            return self.__mul__(1.0 / other[CONST])
        return super().__truediv__(other)

    def __pow__(self, other):
        other = Expr.to_const_or_var(other)
        if (
            isinstance(other, Expr)
            and isinstance(other, ConstExpr)
            and other[CONST].is_integer()
            and other[CONST] > 0
        ):
            res = 1
            for _ in range(int(other[CONST])):
                res *= self
            return res
        return super().__pow__(other)

    @classmethod
    def to_subclass(cls, children: dict = None):
        if len(children) == 0:
            return ConstExpr(0.0)
        elif len(children) == 1:
            if CONST in children:
                return ConstExpr(children[CONST])
            return MonomialExpr(children)
        return cls(children)


class ConstExpr(PolynomialExpr):
    """Expression representing for `constant`."""

    def __init__(self, constant: float = 0):
        super().__init__({CONST: constant})

    def __pow__(self, other, modulo):
        other = Expr.to_const_or_var(other)
        if isinstance(other, ConstExpr):
            return ConstExpr(self[CONST] ** other[CONST])
        return super().__pow__(other, modulo)


class MonomialExpr(PolynomialExpr):
    """Expression representing for `x**3`."""

    def __init__(self, children: dict = None):
        if children and len(children) != 1:
            raise ValueError("MonomialExpr must have exactly one child")

        super().__init__(children)

    @staticmethod
    def from_var(var: Variable, coef: float = 1.0):
        return MonomialExpr({Term(var): coef})


class FuncExpr(Expr):
    def degree(self):
        return float("inf")


class ProdExpr(FuncExpr):
    """Expression representing for `coefficient * expression`."""

    def __init__(self, *children, coef: float = 1.0):
        super().__init__({i: 1.0 for i in children})
        self.coef = coef

    def __hash__(self):
        return (frozenset(self), self.coef).__hash__()

    def __add__(self, other):
        other = Expr.to_const_or_var(other)
        if isinstance(other, ProdExpr) and hash(frozenset(self)) == hash(
            frozenset(other)
        ):
            return ProdExpr(*self, coef=self.coef + other.coef)
        return super().__add__(other)

    def __mul__(self, other):
        other = Expr.to_const_or_var(other)
        if isinstance(other, ConstExpr):
            if other[CONST] == 0:
                return ConstExpr(0.0)
            return ProdExpr(*self, coef=self.coef * other[CONST])
        return super().__mul__(other)

    def __repr__(self):
        return f"ProdExpr({{{tuple(self)}: {self.coef}}})"

    def _normalize(self):
        if self.coef == 0:
            return ConstExpr(0.0)
        return self


class PowExpr(FuncExpr):
    """Expression representing for `pow(expression, exponent)`."""

    def __init__(self, base, expo: float = 1.0):
        super().__init__({base: 1.0})
        self.expo = expo

    def __hash__(self):
        return (frozenset(self), self.expo).__hash__()

    def __repr__(self):
        return f"PowExpr({tuple(self)}, {self.expo})"

    def _normalize(self):
        if self.expo == 0:
            self = ConstExpr(1.0)
        elif self.expo == 1:
            self = tuple(self)[0]


class UnaryExpr(FuncExpr):
    def __init__(self, expr):
        super().__init__((expr,), (1.0,))

    def __hash__(self):
        return frozenset(self).__hash__()

    def __repr__(self):
        return f"{type(self).__name__}({tuple(self)[0]})"


class ExpExpr(UnaryExpr):
    """Expression representing for `exp(expression)`."""


class LogExpr(UnaryExpr):
    """Expression representing for `log(expression)`."""


class SqrtExpr(UnaryExpr):
    """Expression representing for `sqrt(expression)`."""


class SinExpr(UnaryExpr):
    """Expression representing for `sin(expression)`."""


class CosExpr(UnaryExpr):
    """Expression representing for `cos(expression)`."""


# cdef class ExprCons:
class ExprCons:
    """Constraints with a polynomial expressions and lower/upper bounds."""

    # cdef public expr
    # cdef public _lhs
    # cdef public _rhs

    def __init__(self, expr, lhs=None, rhs=None):
        self.expr = expr
        self._lhs = lhs
        self._rhs = rhs
        assert not (lhs is None and rhs is None)
        self.normalize()

    def normalize(self):
        """move constant children in expression to bounds"""

        if isinstance(self.expr, Expr):
            c = self.expr[CONST]
            self.expr -= c
            assert self.expr[CONST] == 0.0
            self.expr.normalize()
        else:
            return

        if self._lhs is not None:
            self._lhs -= c
        if self._rhs is not None:
            self._rhs -= c

    def __richcmp__(self, other, op):
        if op == 1:  # <=
            if self._rhs is not None:
                raise TypeError("ExprCons already has upper bound")
            assert self._lhs is not None

            if not _is_number(other):
                raise TypeError("Ranged ExprCons is not well defined!")

            return ExprCons(self.expr, lhs=self._lhs, rhs=float(other))
        elif op == 5:  # >=
            if self._lhs is not None:
                raise TypeError("ExprCons already has lower bound")

            assert self._lhs is None
            assert self._rhs is not None

            if not _is_number(other):
                raise TypeError("Ranged ExprCons is not well defined!")

            return ExprCons(self.expr, lhs=float(other), rhs=self._rhs)
        else:
            raise NotImplementedError(
                "Ranged ExprCons can only support with '<=' or '>='."
            )

    def __repr__(self):
        return "ExprCons(%s, %s, %s)" % (self.expr, self._lhs, self._rhs)

    def __bool__(self):
        """Make sure that equality of expressions is not asserted with =="""

        msg = """Can't evaluate constraints as booleans.

If you want to add a ranged constraint of the form:
    lhs <= expression <= rhs
you have to use parenthesis to break the Python syntax for chained comparisons:
    lhs <= (expression <= rhs)
"""
        raise TypeError(msg)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions