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

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions