Skip to content

Conversation

@Zeroto521
Copy link
Contributor

Close to #1074. This is a breaking change. We can release the 5.7.0 version first.

Changed the type of _lhs and _rhs attributes in the ExprCons class from unspecified to 'object' to clarify their expected type and improve type safety.
Replaces the __init__ method with __cinit__ in the Variable class and updates the argument type to SCIP_VAR*.
Updated references from 'terms' to 'children' for Expr objects throughout Model methods to reflect changes in the Expr API. This ensures compatibility with the updated data structure and avoids errors when accessing expression terms.
Introduces _to_nodes methods for Expr, PolynomialExpr, and UnaryExpr to convert expressions into node lists for SCIP construction. Refactors Model's constraint creation to use the new node format, simplifying and clarifying the mapping from expression trees to SCIP nonlinear constraints.
Changed Expr from a Cython cdef class to a standard Python class for improved compatibility and maintainability. Removed cdef public dict children, as attribute is now managed in Python.
Converted SumExpr, ProdExpr, and PowExpr from cdef classes to regular Python classes for improved compatibility and maintainability.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR merges the Expr and GenExpr classes into a unified expression system. The refactoring replaces the previous dual-system (polynomial Expr and general GenExpr) with a single hierarchy based on Expr, using specialized subclasses like PolynomialExpr, SumExpr, ProdExpr, and various function expressions (ExpExpr, LogExpr, etc.).

Key changes:

  • Unified expression representation with children replacing terms
  • Variable class no longer inherits from Expr
  • Simplified expression tree structure with improved type system
  • Refactored constraint creation methods to use the new expression system

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/pyscipopt/scip.pyi Updated Variable class to remove inheritance from Expr
src/pyscipopt/scip.pxi Modified Variable class structure and added operator overloading methods to delegate to expression system; refactored constraint creation methods to use children instead of terms
src/pyscipopt/scip.pxd Updated Variable class declaration to remove Expr inheritance
src/pyscipopt/propagator.pxi Simplified variable creation by removing unnecessary temporary variable
src/pyscipopt/expr.pxi Complete rewrite of expression system replacing Expr/GenExpr duality with unified class hierarchy

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Zeroto521 and others added 3 commits November 18, 2025 18:35
Co-authored-by: Copilot <[email protected]>
Deleted the definition of the Expr class, which was not used in the code. This helps clean up the codebase and improves maintainability.
Changed ExprCons from a cdef class to a standard Python class and added type hints to the constructor parameters. This improves code readability and compatibility with Python tooling.
Type hints were added to methods in Term, Expr, PolynomialExpr, ConstExpr, ProdExpr, PowExpr, UnaryExpr, and ExprCons classes to improve code clarity and static analysis. Minor refactoring was performed for consistency in variable naming and method signatures.
Replaces the _to_unary_expr helper with a static method UnaryExpr.from_expr for creating unary expressions. Updates all relevant methods to use the new static method for consistency and improved encapsulation.
Changed the expected exception from NotImplementedError to TypeError when powering an expression with sqrt(2) in test_expr_op_expr. This reflects the actual exception raised by the code.
Changed quicksum and quickprod to use ConstExpr for initialization and updated parameter names and types for clarity. This improves performance and code readability by avoiding unnecessary intermediate data structures.
Refines the __add__ methods in Expr and SumExpr to better handle addition with SumExpr instances, ensuring correct merging of terms and consistent behavior when combining expressions.
Moved and unified the degree() method implementations for Term, Expr, PolynomialExpr, and FuncExpr classes. The degree calculation now consistently uses the degree() method of child elements, improving maintainability and correctness.
Corrects the logic for building node lists in Term and Expr classes to ensure proper handling of coefficients and child nodes. This improves the robustness of SCIP expression construction.
Updates the Expr.degree() method to return infinity when there are no children, instead of defaulting to zero. This ensures correct behavior for empty expressions.
Adds type checking for the expr argument and ensures at least one of lhs or rhs is provided in ExprCons. Moves validation logic from _normalize to __init__ for earlier error detection.
Initializes Expr children with {CONST: 0.0} by default instead of an empty dict. This ensures that expressions always have a constant term, which may help avoid issues with missing constants in further computations.
Updates the test to expect degree 0 for an empty Expr instead of infinity, aligning with the intended behavior of the Expr class.
Changed assertions in test_equation to expect lhs to be 1 instead of 0.0, reflecting updated behavior or requirements for equation evaluation.
Deleted the test_degree function from test_expr.py as it is no longer needed or used in the test suite.
Refines the __mul__ method in Expr to handle multiplication with zero and constant expressions more robustly. Also updates ConstExpr constructor to use float for default constant value.
Changed expected values in test_inequality to reflect updated behavior of GenExprs, asserting 1 instead of 0.0 for _lhs and _rhs.
Introduced a shared _remove_zero() method in SumExpr to eliminate zero-valued children, and updated normalization methods in SumExpr and PolynomialExpr to use this helper for improved code reuse and clarity.
Refactored __add__ in Expr to return 'other' when 'self' has no children, ensuring correct addition semantics and simplifying logic.
Changed the expected degree of an empty Expr from 0 to float('inf') in test_degree to reflect updated behavior.
Improves multiplication handling in Expr and ProdExpr classes. Expr now multiplies by a constant using dictionary comprehension, and ProdExpr prevents duplicate children and simplifies multiplication logic for constants.
Merged SumExpr functionality into Expr, simplifying sum expression logic and normalization. Updated PolynomialExpr to inherit directly from Expr. Adjusted Model class to handle Expr instead of SumExpr for sum expressions. This refactor streamlines expression management and reduces class complexity.
Moved Cython and C imports below standard library imports for better organization. Reformatted the addMatrixVar method signature and the addCons loop for improved readability and consistency.
Implements the __iadd__ method for PolynomialExpr, allowing in-place addition of polynomial expressions by updating child coefficients. Falls back to superclass behavior for non-polynomial operands.
@Zeroto521 Zeroto521 marked this pull request as ready for review November 26, 2025 13:26
@Zeroto521
Copy link
Contributor Author

Zeroto521 commented Nov 26, 2025

Finish the base feature and function. And something needs to be done:

  • add test cases
  • add documentation
  • adapt .pyi file
  • cythonize methods
  • update changelog

@Zeroto521 Zeroto521 requested a review from Copilot November 26, 2025 13:28
Copilot finished reviewing on behalf of Zeroto521 November 26, 2025 13:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

raise TypeError("expr must be an Expr instance")
if lhs is None and rhs is None:
raise ValueError(
"Ranged ExprCons (with both lhs and rhs) doesn't supported"
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message is grammatically incorrect. It should be "Ranged ExprCons (with both lhs and rhs) isn't supported" or "Ranged ExprCons (with both lhs and rhs) is not supported" instead of "doesn't supported".

Suggested change
"Ranged ExprCons (with both lhs and rhs) doesn't supported"
"Ranged ExprCons (with both lhs and rhs) is not supported"

Copilot uses AI. Check for mistakes.
def __next__(self) -> Union[Term, Expr]:
try:
return next(self.children)
except:
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a bare except: clause is considered bad practice in Python. It should catch a specific exception type, such as except StopIteration:. A bare except: will catch all exceptions including SystemExit, KeyboardInterrupt, and others that should generally not be caught.

Suggested change
except:
except StopIteration:

Copilot uses AI. Check for mistakes.
Comment on lines +1532 to +1533
self = self.__add__(other)
return self
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The __iadd__ implementation is incorrect. In Cython/Python, when self = self.__add__(other) is executed, the reassignment only affects the local variable self within the method, not the object itself. This means the in-place addition doesn't actually modify the Variable. While Variables are now immutable value objects (since they don't inherit from Expr), the __iadd__ should simply return the result of __add__ without the misleading assignment.

Suggested change
self = self.__add__(other)
return self
return self.__add__(other)

Copilot uses AI. Check for mistakes.
Comment on lines 1057 to 1064
def _evaluate(self, term):
self._checkStage("SCIPgetSolVal")
result = 1
cdef _VarArray wrapper
wrapper = _VarArray(term.vartuple)
for i in range(len(term.vartuple)):
wrapper = _VarArray(term.vars)
for i in range(len(term.vars)):
result *= SCIPgetSolVal(self.scip, self.sol, wrapper.ptr[i])
return result
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _evaluate method assumes that all children in expr.children are Term objects with a .vars attribute (line 1061). However, for non-polynomial expressions (like Expr({SinExpr(x): 1.0})), the children can be other Expr subclasses that don't have a .vars attribute, which will cause an AttributeError. The code should either check the type and handle non-Term children appropriately, or document that solution evaluation only works for polynomial expressions.

Copilot uses AI. Check for mistakes.
m, x, y, z = model
expr = Expr()
assert expr.degree() == 0
assert expr.degree() == float("inf")
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The degree of an empty Expr() should be 0 (representing a zero constant), not float("inf"). An empty expression has no variables and no terms, which is effectively a constant zero. The float("inf") degree is reserved for non-polynomial expressions like exp(), log(), etc.

Suggested change
assert expr.degree() == float("inf")
assert expr.degree() == 0

Copilot uses AI. Check for mistakes.
return Expr(self._remove_zero())

def degree(self) -> float:
return max((i.degree() for i in self)) if self.children else float("inf")
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The degree() method returns float("inf") for an empty expression, but this is inconsistent with the expected behavior. An empty Expr() should represent a zero constant and have degree 0, not infinity. The infinity degree should only apply to non-polynomial expressions (like those involving exp, log, sin, etc.). This affects the base implementation in line 244.

Suggested change
return max((i.degree() for i in self)) if self.children else float("inf")
return max((i.degree() for i in self)) if self.children else 0

Copilot uses AI. Check for mistakes.
return ExprCons(self, lhs=other[CONST], rhs=other[CONST])
return (self - other).__eq__(0)
elif isinstance(other, MatrixExpr):
return other.__ge__(self)
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The __eq__ method in line 206 calls other.__ge__(self) when other is a MatrixExpr, but this should call other.__eq__(self) to maintain the equality constraint semantics. Using __ge__ would incorrectly create a greater-than-or-equal constraint instead of an equality constraint.

Suggested change
return other.__ge__(self)
return other.__eq__(self)

Copilot uses AI. Check for mistakes.


class ExprCons:
"""Constraints with a polynomial expressions and lower/upper bounds."""
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says "Constraints with a polynomial expressions" but should say "Constraints with polynomial expressions" (without "a" before "polynomial"). Also note that ExprCons can now handle non-polynomial expressions too (like exp, log, etc.), so the docstring is inaccurate and should be updated to reflect that it handles general expressions.

Suggested change
"""Constraints with a polynomial expressions and lower/upper bounds."""
"""Constraints with general expressions and lower/upper bounds."""

Copilot uses AI. Check for mistakes.
relevant_value = relevant_value[:-1] # removing %

if _is_number(relevant_value):
if isinstance(relevant_value, Number):
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check isinstance(relevant_value, Number) will always be False because relevant_value is a string extracted from the file (see line 12083 and 12117). Strings are never instances of Number. This appears to be a logic error introduced when replacing the old _is_number() helper function. The code should use a try/except block or recreate the _is_number() function to check if the string can be converted to a number.

Copilot uses AI. Check for mistakes.
@Joao-Dionisio
Copy link
Member

Thanks for your work @Zeroto521 !! This is something that should be very very well tested before merging. We're also in the middle of a SCIP meeting, so I can bring this up with the guys and we can quickly take a high level look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants