diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d8fb8097..c147b70bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Return NotImplemented for `Expr` and `GenExpr` operators if they can't handle input types in the calculation - Speed up `constant * Expr` via C-level API - Speed up `Term.__eq__` via the C-level API +- Speed up `Expr.__add__` and `Expr.__iadd__` via the C-level API ### Removed - Removed outdated warning about Make build system incompatibility - Removed `Term.ptrtuple` to optimize `Term` memory usage diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index cdbed25cd..0ba912d35 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -55,6 +55,7 @@ from cpython.object cimport Py_LE, Py_EQ, Py_GE, Py_TYPE from cpython.ref cimport PyObject from cpython.tuple cimport PyTuple_GET_ITEM +cimport numpy as cnp from pyscipopt.scip cimport Variable, Solution @@ -291,29 +292,22 @@ cdef class Expr(ExprLike): if not _is_expr_compatible(other): return NotImplemented - left = self - right = other - terms = left.terms.copy() + if _is_number(other): + terms = self.terms.copy() + terms[CONST] = terms.get(CONST, 0.0) + other + return Expr(terms) - if isinstance(right, Expr): - # merge the terms by component-wise addition - for v,c in right.terms.items(): - terms[v] = terms.get(v, 0.0) + c - elif _is_number(right): - c = float(right) - terms[CONST] = terms.get(CONST, 0.0) + c - return Expr(terms) + return Expr(_to_dict(self, other, copy=True)) def __iadd__(self, other): if not _is_expr_compatible(other): return NotImplemented - if isinstance(other, Expr): - for v,c in other.terms.items(): - self.terms[v] = self.terms.get(v, 0.0) + c - elif _is_number(other): - c = float(other) - self.terms[CONST] = self.terms.get(CONST, 0.0) + c + if _is_number(other): + self.terms[CONST] = self.terms.get(CONST, 0.0) + other + else: + _to_dict(self, other, copy=False) + return self def __mul__(self, other): @@ -1010,6 +1004,28 @@ cdef inline object _ensure_matrix(object arg): matrix = MatrixExpr if isinstance(arg, Expr) else MatrixGenExpr return np.array(arg, dtype=object).view(matrix) +cdef dict _to_dict(Expr expr, Expr other, bool copy = True): + cdef dict children = expr.terms.copy() if copy else expr.terms + cdef Py_ssize_t pos = 0 + cdef PyObject* k_ptr = NULL + cdef PyObject* v_ptr = NULL + cdef PyObject* old_v_ptr = NULL + cdef double other_v + cdef object k_obj + + while PyDict_Next(other.terms, &pos, &k_ptr, &v_ptr): + if (other_v := (v_ptr)) == 0: + continue + + k_obj = k_ptr + old_v_ptr = PyDict_GetItem(children, k_obj) + if old_v_ptr != NULL: + children[k_obj] = (old_v_ptr) + other_v + else: + children[k_obj] = other_v + + return children + def expr_to_nodes(expr): '''transforms tree to an array of nodes. each node is an operator and the position of the diff --git a/tests/test_expr.py b/tests/test_expr.py index 8b8e0ae08..2152f5aed 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -494,3 +494,28 @@ def test_term_eq(): assert t3 != t4 # same length, but different term assert t1 != t3 # different length assert t1 != "not a term" # different type + + +def test_Expr_add_Expr(): + m = Model() + x = m.addVar(name="x") + y = m.addVar(name="y") + + e1 = -x + 1 + e2 = y - 1 + e3 = e1 + e2 + assert str(e1) == "Expr({Term(x): -1.0, Term(): 1.0})" + assert str(e2) == "Expr({Term(y): 1.0, Term(): -1.0})" + assert str(e3) == "Expr({Term(x): -1.0, Term(): 0.0, Term(y): 1.0})" + + +def test_Expr_iadd_Expr(): + m = Model() + x = m.addVar(name="x") + y = m.addVar(name="y") + + e1 = -x + 1 + e2 = y - 1 + e1 += e2 + assert str(e1) == "Expr({Term(x): -1.0, Term(): 0.0, Term(y): 1.0})" + assert str(e2) == "Expr({Term(y): 1.0, Term(): -1.0})"