From f1816a6de960bca33a5171ab00f8c8b8e604c6d1 Mon Sep 17 00:00:00 2001 From: Tias Guns Date: Tue, 31 Mar 2026 20:11:11 +0200 Subject: [PATCH 1/3] exprs with *args constructors, avoid flatlist --- cpmpy/expressions/core.py | 2 +- cpmpy/expressions/globalconstraints.py | 136 ++++++++++++++++++++++--- 2 files changed, 125 insertions(+), 13 deletions(-) diff --git a/cpmpy/expressions/core.py b/cpmpy/expressions/core.py index aaf9ea866..b524d5210 100644 --- a/cpmpy/expressions/core.py +++ b/cpmpy/expressions/core.py @@ -115,7 +115,7 @@ class Expression(object): - any ``__op__`` python operator overloading """ - def __init__(self, name: str, arg_list: tuple[Any, ...]): + def __init__(self, name: str, arg_list: tuple[Any, ...], has_subexpr: Optional[bool] = None): """ Constructor of the Expression class diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index 3d1bc123e..be99f432d 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -132,7 +132,6 @@ def my_circuit_decomp(self): DirectConstraint """ -import copy import warnings from typing import cast, Literal, Optional, Iterable, Any, TYPE_CHECKING import numpy as np @@ -140,7 +139,7 @@ def my_circuit_decomp(self): import cpmpy as cp from .core import Expression, BoolVal, ExprLike, ListLike -from .variables import cpm_array, intvar, boolvar, _BoolVarImpl +from .variables import cpm_array, intvar, boolvar, _BoolVarImpl, NDVarArray from .utils import all_pairs, is_int, is_bool, STAR, get_bounds, argvals, is_any_list, flatlist, is_num, is_boolexpr, implies from .globalfunctions import * # XXX make this file backwards compatible @@ -216,13 +215,28 @@ class AllDifferent(GlobalConstraint): """ Enforces that all arguments have a different (distinct) value """ + _args: tuple[ExprLike, ...] def __init__(self, *args: ExprLike|ListLike[ExprLike]): """ Arguments: args (ExprLike|ListLike[ExprLike]): List of expressions or constants to be different from each other """ - super().__init__("alldifferent", tuple(flatlist(args))) + # shortcut + if len(args) == 1 and isinstance(args[0], NDVarArray): + arr = args[0] + super().__init__("alldifferent", tuple(arr.flat), has_subexpr=arr.has_subexpr()) + return + + flat: list[ExprLike] = [] + for a in args: + if isinstance(a, np.ndarray): + flat.extend(a.flat) + elif isinstance(a, (list, tuple)): + flat.extend(a) + else: + flat.append(a) + super().__init__("alldifferent", tuple(flat)) def decompose(self) -> tuple[list[Expression], list[Expression]]: """ @@ -314,7 +328,20 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): Arguments: args (ListLike[ExprLike]): List of expressions or constants to be different from each other, except those equal to 0 """ - super().__init__(flatlist(args), 0) + # shortcut + if len(args) == 1 and isinstance(args[0], NDVarArray): + super().__init__(args[0], 0) + return + + flat: list[ExprLike] = [] + for a in args: + if isinstance(a, np.ndarray): + flat.extend(a.flat) + elif isinstance(a, (list, tuple)): + flat.extend(a) + else: + flat.append(a) + super().__init__(flat, 0) def allequal(args): @@ -336,7 +363,21 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): Arguments: args (ListLike[ExprLike]): List of expressions or constants to have the same value """ - super().__init__("allequal", tuple(flatlist(args))) + # shortcut + if len(args) == 1 and isinstance(args[0], NDVarArray): + arr = args[0] + super().__init__("allequal", tuple(arr.flat), has_subexpr=arr.has_subexpr()) + return + + flat: list[ExprLike] = [] + for a in args: + if isinstance(a, np.ndarray): + flat.extend(a.flat) + elif isinstance(a, (list, tuple)): + flat.extend(a) + else: + flat.append(a) + super().__init__("allequal", tuple(flat)) def decompose(self) -> tuple[list[Expression], list[Expression]]: """ @@ -422,10 +463,25 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): Arguments: args (ListLike[ExprLike]): List of expressions or constants representing the successors of the nodes to form the circuit """ - flatargs = flatlist(args) - if len(flatargs) < 2: + has_subexpr = None + # shortcut + if len(args) == 1 and isinstance(args[0], NDVarArray): + arr = args[0] + newargs = tuple(arr.flat) + has_subexpr = arr.has_subexpr() + else: + flatargs: list[ExprLike] = [] + for a in args: + if isinstance(a, np.ndarray): + flatargs.extend(a.flat) + elif isinstance(a, (list, tuple)): + flatargs.extend(a) + else: + flatargs.append(a) + newargs = tuple(flatargs) + if len(newargs) < 2: raise ValueError('Circuit constraint must be given a minimum of 2 variables') - super().__init__("circuit", tuple(flatargs)) + super().__init__("circuit", newargs, has_subexpr=has_subexpr) def decompose(self) -> tuple[list[Expression], list[Expression]]: """ @@ -1664,7 +1720,21 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): Arguments: args (ListLike[ExprLike]): List of expressions or constants to be assigned to increasing values """ - super().__init__("increasing", tuple(flatlist(args))) + # shortcut + if len(args) == 1 and isinstance(args[0], NDVarArray): + arr = args[0] + super().__init__("increasing", tuple(arr.flat), has_subexpr=arr.has_subexpr()) + return + + flat: list[ExprLike] = [] + for a in args: + if isinstance(a, np.ndarray): + flat.extend(a.flat) + elif isinstance(a, (list, tuple)): + flat.extend(a) + else: + flat.append(a) + super().__init__("increasing", tuple(flat)) def decompose(self) -> tuple[list[Expression], list[Expression]]: """ @@ -1697,7 +1767,21 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): Arguments: args (ListLike[ExprLike]): List of expressions or constants to be assigned to decreasing values """ - super().__init__("decreasing", tuple(flatlist(args))) + # shortcut + if len(args) == 1 and isinstance(args[0], NDVarArray): + arr = args[0] + super().__init__("decreasing", tuple(arr.flat), has_subexpr=arr.has_subexpr()) + return + + flat: list[ExprLike] = [] + for a in args: + if isinstance(a, np.ndarray): + flat.extend(a.flat) + elif isinstance(a, (list, tuple)): + flat.extend(a) + else: + flat.append(a) + super().__init__("decreasing", tuple(flat)) def decompose(self) -> tuple[list[Expression], list[Expression]]: """ @@ -1730,7 +1814,21 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): Arguments: args (ListLike[ExprLike]): List of expressions or constants to be assigned to strictly increasing values """ - super().__init__("strictly_increasing", tuple(flatlist(args))) + # shortcut + if len(args) == 1 and isinstance(args[0], NDVarArray): + arr = args[0] + super().__init__("strictly_increasing", tuple(arr.flat), has_subexpr=arr.has_subexpr()) + return + + flat: list[ExprLike] = [] + for a in args: + if isinstance(a, np.ndarray): + flat.extend(a.flat) + elif isinstance(a, (list, tuple)): + flat.extend(a) + else: + flat.append(a) + super().__init__("strictly_increasing", tuple(flat)) def decompose(self) -> tuple[list[Expression], list[Expression]]: """ @@ -1764,7 +1862,21 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): Arguments: args (ListLike[ExprLike]): List of expressions or constants to be assigned to strictly decreasing values """ - super().__init__("strictly_decreasing", tuple(flatlist(args))) + # shortcut + if len(args) == 1 and isinstance(args[0], NDVarArray): + arr = args[0] + super().__init__("strictly_decreasing", tuple(arr.flat), has_subexpr=arr.has_subexpr()) + return + + flat: list[ExprLike] = [] + for a in args: + if isinstance(a, np.ndarray): + flat.extend(a.flat) + elif isinstance(a, (list, tuple)): + flat.extend(a) + else: + flat.append(a) + super().__init__("strictly_decreasing", tuple(flat)) def decompose(self) -> tuple[list[Expression], list[Expression]]: """ From e78424a012945ea20bebebe22b2d573f672cb8e4 Mon Sep 17 00:00:00 2001 From: Tias Guns Date: Fri, 3 Apr 2026 09:33:52 +0200 Subject: [PATCH 2/3] adding args properties --- cpmpy/expressions/core.py | 4 --- cpmpy/expressions/globalconstraints.py | 43 +++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/cpmpy/expressions/core.py b/cpmpy/expressions/core.py index b524d5210..a484d2a18 100644 --- a/cpmpy/expressions/core.py +++ b/cpmpy/expressions/core.py @@ -137,10 +137,6 @@ def __init__(self, name: str, arg_list: tuple[Any, ...], has_subexpr: Optional[b def args(self) -> tuple[Any, ...]: return self._args - @args.setter - def args(self, args: Iterable[Any]) -> None: - raise AttributeError("Cannot modify read-only attribute 'args', use 'update_args()'") - def update_args(self, args: Iterable[Any]) -> None: """ Allows in-place update of the expression's arguments. Resets all cached computations which depend on the expression tree. diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index be99f432d..1db6a3611 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -215,7 +215,6 @@ class AllDifferent(GlobalConstraint): """ Enforces that all arguments have a different (distinct) value """ - _args: tuple[ExprLike, ...] def __init__(self, *args: ExprLike|ListLike[ExprLike]): """ @@ -236,8 +235,14 @@ def __init__(self, *args: ExprLike|ListLike[ExprLike]): flat.extend(a) else: flat.append(a) + # args: tuple[ExprLike, ...] super().__init__("alldifferent", tuple(flat)) + @property + def args(self) -> tuple[ExprLike, ...]: + """ READ-ONLY, well-tuped arguments of this global constraint """ + return self._args + def decompose(self) -> tuple[list[Expression], list[Expression]]: """ Decomposition of the AllDifferent global constraint using pairwise disequality constraints. @@ -377,8 +382,14 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): flat.extend(a) else: flat.append(a) + # args: tuple[ExprLike, ...] super().__init__("allequal", tuple(flat)) + @property + def args(self) -> tuple[ExprLike, ...]: + """ READ-ONLY, well-tuped arguments of this global constraint """ + return self._args + def decompose(self) -> tuple[list[Expression], list[Expression]]: """ Decomposition of the AllEqual global constraint using cascaded equality constraints. @@ -481,8 +492,14 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): newargs = tuple(flatargs) if len(newargs) < 2: raise ValueError('Circuit constraint must be given a minimum of 2 variables') + # args: tuple[ExprLike, ...] super().__init__("circuit", newargs, has_subexpr=has_subexpr) + @property + def args(self) -> tuple[ExprLike, ...]: + """ READ-ONLY, well-tuped arguments of this global constraint """ + return self._args + def decompose(self) -> tuple[list[Expression], list[Expression]]: """ Decomposition of the Circuit global constraint using auxiliary variables to reprsent the order in which we visit all the nodes. @@ -1734,8 +1751,14 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): flat.extend(a) else: flat.append(a) + # args: tuple[ExprLike, ...] super().__init__("increasing", tuple(flat)) + @property + def args(self) -> tuple[ExprLike, ...]: + """ READ-ONLY, well-tuped arguments of this global constraint """ + return self._args + def decompose(self) -> tuple[list[Expression], list[Expression]]: """ Decomposition of the Increasing constraint. @@ -1781,8 +1804,14 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): flat.extend(a) else: flat.append(a) + # args: tuple[ExprLike, ...] super().__init__("decreasing", tuple(flat)) + @property + def args(self) -> tuple[ExprLike, ...]: + """ READ-ONLY, well-tuped arguments of this global constraint """ + return self._args + def decompose(self) -> tuple[list[Expression], list[Expression]]: """ Decomposition of the Decreasing constraint. @@ -1828,8 +1857,14 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): flat.extend(a) else: flat.append(a) + # args: tuple[ExprLike, ...] super().__init__("strictly_increasing", tuple(flat)) + @property + def args(self) -> tuple[ExprLike, ...]: + """ READ-ONLY, well-tuped arguments of this global constraint """ + return self._args + def decompose(self) -> tuple[list[Expression], list[Expression]]: """ Decomposition of the IncreasingStrict constraint. @@ -1876,8 +1911,14 @@ def __init__(self, *args: ExprLike | ListLike[ExprLike]): flat.extend(a) else: flat.append(a) + # args: tuple[ExprLike, ...] super().__init__("strictly_decreasing", tuple(flat)) + @property + def args(self) -> tuple[ExprLike, ...]: + """ READ-ONLY, well-tuped arguments of this global constraint """ + return self._args + def decompose(self) -> tuple[list[Expression], list[Expression]]: """ Decomposition of the DecreasingStrict constraint. From 51626c66c2132a5d00b5e6b739e7c0a41178ff8b Mon Sep 17 00:00:00 2001 From: Tias Guns Date: Fri, 3 Apr 2026 11:26:39 +0200 Subject: [PATCH 3/3] type args of some, add helpers, its a mess --- cpmpy/expressions/core.py | 6 +- cpmpy/expressions/globalconstraints.py | 57 ++++++++++--------- cpmpy/expressions/utils.py | 76 +++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 31 deletions(-) diff --git a/cpmpy/expressions/core.py b/cpmpy/expressions/core.py index a3a720809..714bf3c97 100644 --- a/cpmpy/expressions/core.py +++ b/cpmpy/expressions/core.py @@ -222,10 +222,10 @@ def is_bool(self): """ return True - def value(self): - return None # default + def value(self) -> Optional[int]: + return None # default - def get_bounds(self): + def get_bounds(self) -> tuple[int, int]: if self.is_bool(): return 0, 1 #default for boolean expressions raise NotImplementedError(f"`get_bounds` is not implemented for type {self}") diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index ca0ad833a..71c9f4cf5 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -141,7 +141,7 @@ def my_circuit_decomp(self): from ..exceptions import TypeError from .core import Expression, BoolVal, ExprLike, ListLike from .variables import cpm_array, intvar, boolvar, _BoolVarImpl, NDVarArray -from .utils import all_pairs, is_int, is_bool, STAR, get_bounds, argvals, is_any_list, flatlist, is_num, is_boolexpr, implies +from .utils import all_pairs, is_int, is_bool, STAR, get_bounds, argvals, is_any_list, flatlist, is_num, is_boolexpr, implies, get_minimax_bounds_listlike, argvals_listlike, clean_bool if TYPE_CHECKING: from cpmpy.solvers.solver_interface import SolverInterface @@ -216,7 +216,7 @@ class AllDifferent(GlobalConstraint): Enforces that all arguments have a different (distinct) value """ - def __init__(self, *args: ExprLike|ListLike[ExprLike]): + def __init__(self, *args: ExprLike | ListLike[ExprLike]): """ Arguments: args (ExprLike|ListLike[ExprLike]): List of expressions or constants to be different from each other @@ -259,8 +259,7 @@ def decompose_linear(self) -> tuple[list[Expression], list[Expression]]: For use with integer linear programming and pb/sat solvers. """ - lbs, ubs = get_bounds(self.args) - lb, ub = min(lbs), max(ubs) + lb, ub = get_minimax_bounds_listlike(self.args) return [cp.sum((arg_i == val) for arg_i in self.args) <= 1 for val in range(lb, ub + 1)], [] def value(self) -> Optional[bool]: @@ -268,10 +267,10 @@ def value(self) -> Optional[bool]: Returns: Optional[bool]: True if the global constraint is satisfied, False otherwise, or None if any argument is not assigned """ - vals = argvals(self.args) - if any(v is None for v in vals): + vals = argvals_listlike(self.args) + if vals is None: return None - return len(set(vals)) == len(self.args) + return len(set(vals)) == len(vals) class AllDifferentExceptN(GlobalConstraint): @@ -279,7 +278,7 @@ class AllDifferentExceptN(GlobalConstraint): Enforces that all arguments, except those equal to a value in n, have a different (distinct) value. Arguments: - arr (Sequence[Expression]): List of expressions to be different from each other, except those equal to a value in n + arr (ListLike[ExprLike]): List of expressions to be different from each other, except those equal to a value in n n (int or list[int]): Value or list of values that are excluded from satisfying the alldifferent condition """ @@ -398,15 +397,16 @@ def decompose(self) -> tuple[list[Expression], list[Expression]]: tuple[list[Expression], list[Expression]]: A tuple containing the constraints representing the constraint value and the defining constraints """ # arg0 == arg1, arg1 == arg2, arg2 == arg3... no need to post n^2 equalities - return [x == y for x, y in zip(self.args[:-1], self.args[1:])], [] + decomp = clean_bool([x == y for x, y in zip(self.args[:-1], self.args[1:])]) + return decomp, [] def value(self) -> Optional[bool]: """ Returns: Optional[bool]: True if the global constraint is satisfied, False otherwise, or None if any argument is not assigned """ - vals = argvals(self.args) - if any(v is None for v in vals): + vals = argvals_listlike(self.args) + if vals is None: return None return len(set(vals)) == 1 @@ -511,7 +511,13 @@ def decompose(self) -> tuple[list[Expression], list[Expression]]: # construct the chain of neighbors succ = cp.cpm_array(self.args) - order = [succ[0]] + tmp: ExprLike = succ[0] + if isinstance(tmp, Expression): + succ_0: Expression = tmp + else: + # type incompatible side-case, succ[0] should not be a constant + succ_0 = intvar(int(tmp), int(tmp)) + order: list[Expression] = [succ_0] for i in range(1, len(succ)): order.append(succ[order[i - 1]]) @@ -1538,7 +1544,7 @@ def decompose(self) -> tuple[list[Expression], list[Expression]]: Decomposition of the NoOverlap constraint, using pairwise no-overlap constraints. Returns: - tuple[Sequence[Expression], Sequence[Expression]]: A tuple containing the constraints representing the constraint value and the defining constraints + tuple[list[Expression], list[Expression]]: A tuple containing the constraints representing the constraint value and the defining constraints """ start, dur, end, is_present = self.args cons = [implies(p, d >= 0) for d, p in zip(dur, is_present)] @@ -1735,15 +1741,15 @@ def decompose(self) -> tuple[list[Expression], list[Expression]]: tuple[list[Expression], list[Expression]]: A tuple containing the constraints representing the constraint value and the defining constraints """ args = self.args - return [args[i] <= args[i+1] for i in range(len(args)-1)], [] + return clean_bool([args[i] <= args[i+1] for i in range(len(args)-1)]), [] def value(self) -> Optional[bool]: """ Returns: Optional[bool]: True if the global constraint is satisfied, False otherwise, or None if any argument is not assigned """ - args = argvals(self.args) - if any(x is None for x in args): + args = argvals_listlike(self.args) + if args is None: return None return all(args[i] <= args[i+1] for i in range(len(args)-1)) @@ -1788,15 +1794,15 @@ def decompose(self) -> tuple[list[Expression], list[Expression]]: tuple[list[Expression], list[Expression]]: A tuple containing the constraints representing the constraint value and the defining constraints """ args = self.args - return [args[i] >= args[i+1] for i in range(len(args)-1)], [] + return clean_bool([args[i] >= args[i+1] for i in range(len(args)-1)]), [] def value(self) -> Optional[bool]: """ Returns: Optional[bool]: True if the global constraint is satisfied, False otherwise, or None if any argument is not assigned """ - args = argvals(self.args) - if any(x is None for x in args): + args = argvals_listlike(self.args) + if args is None: return None return all(args[i] >= args[i+1] for i in range(len(args)-1)) @@ -1841,15 +1847,15 @@ def decompose(self) -> tuple[list[Expression], list[Expression]]: tuple[list[Expression], list[Expression]]: A tuple containing the constraints representing the constraint value and the defining constraints """ args = self.args - return [args[i] < args[i+1] for i in range(len(args)-1)], [] + return clean_bool([args[i] < args[i+1] for i in range(len(args)-1)]), [] def value(self) -> Optional[bool]: """ Returns: Optional[bool]: True if the global constraint is satisfied, False otherwise, or None if any argument is not assigned """ - args = argvals(self.args) - if any(x is None for x in args): + args = argvals_listlike(self.args) + if args is None: return None args = argvals(self.args) return all(args[i] < args[i+1] for i in range(len(args)-1)) @@ -1895,17 +1901,16 @@ def decompose(self) -> tuple[list[Expression], list[Expression]]: tuple[list[Expression], list[Expression]]: A tuple containing the constraints representing the constraint value and the defining constraints """ args = self.args - return [(args[i] > args[i+1]) for i in range(len(args)-1)], [] + return clean_bool([args[i] > args[i+1] for i in range(len(args)-1)]), [] def value(self) -> Optional[bool]: """ Returns: Optional[bool]: True if the global constraint is satisfied, False otherwise, or None if any argument is not assigned """ - args = argvals(self.args) - if any(x is None for x in args): + args = argvals_listlike(self.args) + if args is None: return None - args = argvals(self.args) return all(args[i] > args[i+1] for i in range(len(args)-1)) diff --git a/cpmpy/expressions/utils.py b/cpmpy/expressions/utils.py index 6a4890a1b..3df467968 100644 --- a/cpmpy/expressions/utils.py +++ b/cpmpy/expressions/utils.py @@ -35,12 +35,12 @@ import math from collections.abc import Iterable # for flatten from itertools import combinations -from typing import TYPE_CHECKING, TypeGuard, Union, Optional +from typing import TYPE_CHECKING, TypeGuard, Optional, cast from cpmpy.exceptions import IncompleteFunctionError if TYPE_CHECKING: # only import for type checking - from cpmpy.expressions.core import ListLike, ExprLike + from cpmpy.expressions.core import Expression, ListLike, ExprLike def is_bool(arg): @@ -153,6 +153,23 @@ def argvals(arr): return argval(arr) +def argvals_listlike(lst: ListLike[ExprLike]) -> Optional[list[int]]: + """ The well-typed way to get the values of a list of ExprLike's, or None if any expression is not assigned """ + _Expr = cp.expressions.core.Expression + + vals: list[int] = [] + for e in lst: + if isinstance(e, _Expr): + v = e.value() + if v is None: + return None + vals.append(v) + elif isinstance(e, int): + vals.append(e) + else: # only np.integer is still in ExprLike + vals.append(int(e)) + return vals + def eval_comparison(str_op, lhs, rhs): """ Internal function: evaluates the textual `str_op` comparison operator @@ -187,6 +204,46 @@ def eval_comparison(str_op, lhs, rhs): return lhs <= rhs else: raise Exception("Not a known comparison:", str_op) + +def get_bounds_listlike(lst: ListLike[ExprLike]) -> tuple[list[int], list[int]]: + """ The well-typed way to get the bounds of a list of ExprLike's """ + _Expr = cp.expressions.core.Expression + + lbs: list[int] = [] + ubs: list[int] = [] + for e in lst: + if isinstance(e, _Expr): + (lb, ub) = e.get_bounds() + lbs.append(lb) + ubs.append(ub) + elif isinstance(e, int): + lbs.append(e) + ubs.append(e) + else: # only np.integer is still in ExprLike + lbs.append(int(e)) + ubs.append(int(e)) + return lbs, ubs + +def get_minimax_bounds_listlike(lst: ListLike[ExprLike]) -> tuple[int, int]: + """ The well-typed way to get the minimax bounds of a list of ExprLike's """ + _Expr = cp.expressions.core.Expression + + glb: Optional[int] = None + gub: Optional[int] = None + for e in lst: + if isinstance(e, _Expr): + (lb, ub) = e.get_bounds() + glb = min(glb, lb) if glb is not None else lb + gub = max(gub, ub) if gub is not None else ub + elif isinstance(e, int): + glb = min(glb, e) if glb is not None else e + gub = max(gub, e) if gub is not None else e + else: # only np.integer is still in ExprLike + glb = min(glb, int(e)) if glb is not None else int(e) + gub = max(gub, int(e)) if gub is not None else int(e) + + assert glb is not None and gub is not None, f"No bounds found, empty list? `{lst}`" + return glb, gub def get_bounds(expr): """ return the bounds of the expression @@ -255,3 +312,18 @@ def is_star(arg): Check if arg is star as used in the ShortTable global constraint """ return isinstance(arg, type(STAR)) and arg == STAR + +def clean_bool(exprs: list[ExprLike]) -> list[Expression]: + """ Clean up a list of Boolean expressions representing a conjunction of Booleans + + It only cleans up Python 'bool' instances + it returns [BoolVal(False)] if it contains a 'False' + """ + new_exprs: list[Expression] = [] + for e in exprs: + if isinstance(e, bool): + if e is False: + return [cp.BoolVal(False)] + else: + new_exprs.append(cast(Expression, e)) # we silently assume its Expression + return new_exprs \ No newline at end of file