From e7adfe2a627ae9d27c06d10b4b660694b42217cc Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 11 Jun 2026 23:16:11 +0800 Subject: [PATCH 01/10] `Expr` and `GenExpr` support `__pos__` now --- src/pyscipopt/expr.pxi | 45 +++++++++++++++++++++++++++++++++--------- src/pyscipopt/scip.pyi | 1 + 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 62f0c880d..4bab08ffc 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -278,6 +278,9 @@ cdef class ExprLike: def __neg__(self, /) -> Union[Expr, GenExpr]: return self * -1.0 + def __pos__(self, /) -> Union[Expr, GenExpr]: + return self.copy() + def __abs__(self) -> GenExpr: return UnaryExpr(Operator.fabs, buildGenExprObj(self)) @@ -296,6 +299,9 @@ cdef class ExprLike: def cos(self) -> GenExpr: return UnaryExpr(Operator.cos, buildGenExprObj(self)) + cdef ExprLike copy(self, bint copy=True): + raise NotImplementedError("copy() must be implemented in subclasses") + ##@details Polynomial expressions of variables with operator overloading. \n #See also the @ref ExprDetails "description" in the expr.pxi. @@ -435,6 +441,12 @@ cdef class Expr(ExprLike): res += coef * term._evaluate(sol) return res + cdef ExprLike copy(self, bint copy=True): + cdef object cls = Py_TYPE(self) + cdef ExprLike res = cls.__new__(cls) + res.terms = self.terms.copy() if copy else self.terms + return res + cdef class ExprCons: '''Constraints with a polynomial expressions and lower/upper bounds.''' @@ -703,18 +715,11 @@ cdef class GenExpr(ExprLike): '''returns operator of GenExpr''' return self._op - cdef GenExpr copy(self, bool copy = True): + cdef ExprLike copy(self, bint copy=True): cdef object cls = Py_TYPE(self) - cdef GenExpr res = cls.__new__(cls) + cdef ExprLike res = cls.__new__(cls) res._op = self._op res.children = self.children.copy() if copy else self.children - if cls is SumExpr: - (res).constant = (self).constant - (res).coefs = (self).coefs.copy() if copy else (self).coefs - if cls is ProdExpr: - (res).constant = (self).constant - elif cls is PowExpr: - (res).expo = (self).expo return res @@ -741,6 +746,14 @@ cdef class SumExpr(GenExpr): res += coefs[i] * (children[i])._evaluate(sol) return res + cdef ExprLike copy(self, bint copy=True): + cdef SumExpr res = SumExpr.__new__(SumExpr) + res._op = self._op + res.children = self.children.copy() if copy else self.children + res.constant = self.constant + res.coefs = self.coefs.copy() if copy else self.coefs + return res + # Prod Expressions cdef class ProdExpr(GenExpr): @@ -765,6 +778,13 @@ cdef class ProdExpr(GenExpr): return 0.0 return res + cdef ExprLike copy(self, bint copy=True): + cdef ProdExpr res = ProdExpr.__new__(ProdExpr) + res._op = self._op + res.children = self.children.copy() if copy else self.children + res.constant = self.constant + return res + # Var Expressions cdef class VarExpr(GenExpr): @@ -798,6 +818,13 @@ cdef class PowExpr(GenExpr): cpdef double _evaluate(self, Solution sol) except *: return (self.children[0])._evaluate(sol) ** self.expo + cdef ExprLike copy(self, bint copy=True): + cdef PowExpr res = PowExpr.__new__(PowExpr) + res._op = self._op + res.children = self.children.copy() if copy else self.children + res.expo = self.expo + return res + # Exp, Log, Sqrt, Sin, Cos Expressions cdef class UnaryExpr(GenExpr): diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 86196cfc1..dae7a3ef8 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -338,6 +338,7 @@ class ExprLike: def __rmul__(self, other: object, /) -> Incomplete: ... def __rtruediv__(self, other: object, /) -> GenExpr: ... def __neg__(self, /) -> Union[Expr, GenExpr]: ... + def __pos__(self, /) -> Union[Expr, GenExpr]: ... def __abs__(self) -> GenExpr: ... def exp(self) -> GenExpr: ... def log(self) -> GenExpr: ... From f5a364757b08d1ad96de0110746d21a73d315fe2 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 11 Jun 2026 23:17:30 +0800 Subject: [PATCH 02/10] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80bea8d07..1d81d54bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Added - Added `addConsCumulative()` for SCIP cumulative constraints (#1222) +- `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr` ### Fixed ### Changed - Move magic methods (`__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__`) to `ExprLike` base class (#1204) From 193cb1e7f46b6413d772550b821d42fba1b084bc Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Jun 2026 11:47:43 +0800 Subject: [PATCH 03/10] Add copy method to .pxd file --- src/pyscipopt/expr.pxi | 4 +++- src/pyscipopt/scip.pxd | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 4bab08ffc..bd765bd02 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -300,7 +300,9 @@ cdef class ExprLike: return UnaryExpr(Operator.cos, buildGenExprObj(self)) cdef ExprLike copy(self, bint copy=True): - raise NotImplementedError("copy() must be implemented in subclasses") + raise NotImplementedError( + f"{self.__class__.__name__!s} need to implement copy() method" + ) ##@details Polynomial expressions of variables with operator overloading. \n diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 9ff2979e7..6db5be281 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2151,7 +2151,8 @@ cdef extern from "tpi/tpi.h": int SCIPtpiGetNumThreads() cdef class ExprLike: - pass + + cdef ExprLike copy(self, bint copy=*) cdef class Expr(ExprLike): cdef public terms From 950b1ec46013594c5afbf820374a112768d694ca Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Jun 2026 12:04:17 +0800 Subject: [PATCH 04/10] Variable support copy method --- src/pyscipopt/scip.pxi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 45f0ccf9e..b9431cdbe 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1556,6 +1556,9 @@ cdef class Variable(Expr): Expr.__init__(var, {Term(var) : 1.0}) return var + cdef ExprLike copy(self, bint copy=True): + return self + property name: def __get__(self): if self.scip_var == NULL: From de079ce2806c79ef2c5e081e3ab53c13097defa4 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Jun 2026 12:05:26 +0800 Subject: [PATCH 05/10] Add test cases for `__pos__` --- tests/test_expr.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/test_expr.py b/tests/test_expr.py index f35096f73..68aa8d3ef 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -547,3 +547,43 @@ def test_Expr_iadd_Expr(): 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})" + + +def test_pos(): + m = Model() + x = m.addVar(name="x") + + # test Variable + res = +x + assert str(res) == "x" + assert res is x + + # test Expr + e = x + 1 + res = +(x + 1) + assert str(res) == "Expr({Term(x): 1.0, Term(): 1.0})" + assert e is not res + + # test SumExpr + e = sqrt(x) + 1 + res = +e + assert str(res) == str(e) + assert e is not res + + # test UnaryExpr + e = cos(x) + res = +e + assert str(res) == str(e) + assert e is not res + + # test ProdExpr + e = x * sin(x) + res = +e + assert str(res) == str(e) + assert e is not res + + # test PowExpr + e = log(x)**2 + res = +e + assert str(res) == str(e) + assert e is not res From b8b657de909ff1fec343d7cc3cd8f6e2f6f0a3b2 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Jun 2026 12:06:52 +0800 Subject: [PATCH 06/10] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d81d54bd..5218cf918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Changed - Move magic methods (`__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__`) to `ExprLike` base class (#1204) - Speed up `Expr.__add__` and `Expr.__iadd__` via the C-level API +- Support `__pos__` magic method for `Expr` and `GenExpr` ### Removed ## 6.2.1 - 2026.05.16 From f6baa1f7318b20b83d8c5126829b3fd8c615a843 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Jun 2026 12:10:39 +0800 Subject: [PATCH 07/10] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5218cf918..1d81d54bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ ### Changed - Move magic methods (`__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__`) to `ExprLike` base class (#1204) - Speed up `Expr.__add__` and `Expr.__iadd__` via the C-level API -- Support `__pos__` magic method for `Expr` and `GenExpr` ### Removed ## 6.2.1 - 2026.05.16 From 9ac69b9dcd9f890fd4f8e7d17d8673e81392e258 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Jun 2026 12:11:59 +0800 Subject: [PATCH 08/10] add @disjoint_base for ExprLike --- src/pyscipopt/scip.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index dae7a3ef8..aa1cff2a5 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -324,6 +324,7 @@ class Eventhdlr: def eventinit(self) -> Incomplete: ... def eventinitsol(self) -> Incomplete: ... +@disjoint_base class ExprLike: def __array_ufunc__( self, From bb7b2ec37188b1f42e0ac146586332f0e1114251 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 14 Jun 2026 11:26:23 +0800 Subject: [PATCH 09/10] add better type annotations for `copy` --- src/pyscipopt/expr.pxi | 14 +++++++------- src/pyscipopt/scip.pxi | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index bd765bd02..a8158dd25 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -443,9 +443,9 @@ cdef class Expr(ExprLike): res += coef * term._evaluate(sol) return res - cdef ExprLike copy(self, bint copy=True): + cdef Expr copy(self, bint copy=True): cdef object cls = Py_TYPE(self) - cdef ExprLike res = cls.__new__(cls) + cdef Expr res = cls.__new__(cls) res.terms = self.terms.copy() if copy else self.terms return res @@ -717,9 +717,9 @@ cdef class GenExpr(ExprLike): '''returns operator of GenExpr''' return self._op - cdef ExprLike copy(self, bint copy=True): + cdef GenExpr copy(self, bint copy=True): cdef object cls = Py_TYPE(self) - cdef ExprLike res = cls.__new__(cls) + cdef GenExpr res = cls.__new__(cls) res._op = self._op res.children = self.children.copy() if copy else self.children return res @@ -748,7 +748,7 @@ cdef class SumExpr(GenExpr): res += coefs[i] * (children[i])._evaluate(sol) return res - cdef ExprLike copy(self, bint copy=True): + cdef SumExpr copy(self, bint copy=True): cdef SumExpr res = SumExpr.__new__(SumExpr) res._op = self._op res.children = self.children.copy() if copy else self.children @@ -780,7 +780,7 @@ cdef class ProdExpr(GenExpr): return 0.0 return res - cdef ExprLike copy(self, bint copy=True): + cdef ProdExpr copy(self, bint copy=True): cdef ProdExpr res = ProdExpr.__new__(ProdExpr) res._op = self._op res.children = self.children.copy() if copy else self.children @@ -820,7 +820,7 @@ cdef class PowExpr(GenExpr): cpdef double _evaluate(self, Solution sol) except *: return (self.children[0])._evaluate(sol) ** self.expo - cdef ExprLike copy(self, bint copy=True): + cdef PowExpr copy(self, bint copy=True): cdef PowExpr res = PowExpr.__new__(PowExpr) res._op = self._op res.children = self.children.copy() if copy else self.children diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index b9431cdbe..8259a8ab0 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1556,9 +1556,6 @@ cdef class Variable(Expr): Expr.__init__(var, {Term(var) : 1.0}) return var - cdef ExprLike copy(self, bint copy=True): - return self - property name: def __get__(self): if self.scip_var == NULL: @@ -1566,6 +1563,9 @@ cdef class Variable(Expr): cname = bytes( SCIPvarGetName(self.scip_var) ) return cname.decode('utf-8') + cdef Variable copy(self, bint copy=True): + return self + def ptr(self): return (self.scip_var) From 65fab37116fac308189cc2affd576024c4aa8f8d Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Jun 2026 22:20:43 +0800 Subject: [PATCH 10/10] Add `copy` method for `Constant` --- src/pyscipopt/expr.pxi | 7 +++++++ tests/test_expr.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index a8158dd25..3b232fea2 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -861,6 +861,13 @@ cdef class Constant(GenExpr): cpdef double _evaluate(self, Solution sol) except *: return self.number + cdef Constant copy(self, bint copy=True): + # The copy parameter doesn't work; this is for compatibility. + cdef Constant res = Constant.__new__(Constant) + res._op = self._op + res.number = self.number + return res + def exp(x): """ diff --git a/tests/test_expr.py b/tests/test_expr.py index 68aa8d3ef..1b51e4f2a 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -587,3 +587,10 @@ def test_pos(): res = +e assert str(res) == str(e) assert e is not res + + # test Constant + c = sqrt(1).children[0] + assert type(c) is not int + e = +c + assert str(e) == str(c) + assert e is not c