Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 33 additions & 17 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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) + <double>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) + <double>other
else:
_to_dict(self, other, copy=False)

return self

def __mul__(self, other):
Expand Down Expand Up @@ -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 = <Py_ssize_t>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 := <double>(<object>v_ptr)) == 0:
continue

k_obj = <object>k_ptr
old_v_ptr = PyDict_GetItem(children, k_obj)
if old_v_ptr != NULL:
children[k_obj] = <double>(<object>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
Expand Down
25 changes: 25 additions & 0 deletions tests/test_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})"
Loading