Skip to content

floatsum to still enable floats in the objective#957

Open
tias wants to merge 39 commits into
masterfrom
floatsum
Open

floatsum to still enable floats in the objective#957
tias wants to merge 39 commits into
masterfrom
floatsum

Conversation

@tias

@tias tias commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator

also deprecates the use of floats in mul

if this looks good, we can apply it to the other solvers that support floats before merging (scip, highs, gurobi, cplex; maybe rc2, z3, choco, exact, hexaly?)

also deprecates the use of floats in mul
@tias tias mentioned this pull request Apr 24, 2026
18 tasks
tias added 6 commits April 24, 2026 23:50
so list[Expression] was not a ListLike[ExprLike]...

because list is mutable, and mutable collections are type 'invariant';
what we want is 'covariant' behaviour which would treat Expression as
a subclass of ExprLike
https://mypy.readthedocs.io/en/stable/common_issues.html#invariance-vs-covariance

The Sequence type covers tuple and list (and others, like iterators I think)
and it is covariant, so that resolves that (and accepts other things we
also accept, because we just use generic sequence (iterate, select
element) functions on it

@IgnaceBleukx IgnaceBleukx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall seems fine; it's very different from our expressions, but that is the point, of course.

There is quite some duplication of handling the FloatSums in the solver interfaces; maybe it's better to move it to the _make_numexpr helper functions?

Main thing for me is some missing docs: e.g., in the template, and in the main modeling docs, making it clear FloatSum and DirectConstraint are the ONLY things where you are allowed to use floats.

A follow-up PR should probably go through all of the is_num checks and replace them with is_int instead?

Comment thread cpmpy/expressions/globalfunctions.py Outdated
Comment thread cpmpy/expressions/globalfunctions.py Outdated
Comment thread cpmpy/solvers/cplex.py
Comment thread cpmpy/solvers/z3.py Outdated
if z3.is_int_value(obj_val):
self.objective_value_ = obj_val.as_long()
elif z3.is_rational_value(obj_val):
self.objective_value_ = obj_val.numerator_as_long() / obj_val.denominator_as_long()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Document this?

@tias tias mentioned this pull request May 21, 2026
32 tasks
@tias

tias commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

removing the is_num's, yes, but its expansive... some are these are in the constructors that we are still cleaning up anyway, so I think after properly typing the constructors is best

@tias

tias commented May 31, 2026

Copy link
Copy Markdown
Collaborator Author

I refactored it in parallel...

It was failing on negboolviews for some solvers, and there was also the open thing of constants.

Its still a bit a non-well-typed mess: in the current form we can let the solverobject overwrite objective() to also accept FloatSum (I would not do this in Model... so the tests directly need to call the solverobject, we would have to document this). This is allowed by the type mechanism.

but then, the current code lets objective_value() return int|float, but that is not allowed by the type mechanism... so we would have to convert it to int, and we would have to document that you need to read the objective straight from the FloatSum? (for finite-precision/comparison reasons, this might actually be OK)

(both are not actually visible yet because our typing has not advanced that much yet)

@tias tias added this to the v0.20 milestone Jun 8, 2026
@tias

tias commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author
  • refactored to have fewer duplicate code (and also store a constant, and be able to not return negboolviews)
  • typed model.minimize/maximize/objective_value and friends as int
  • floatsum only in specific solver interfaces
  • added docs in multiple places, including FLOBJ as new capability in capability table

please re-review

@tias tias requested a review from IgnaceBleukx June 9, 2026 20:58

@IgnaceBleukx IgnaceBleukx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good, almost there I think.

The main question mark for me is the objective value return.
I.e., are we sure that we will always have a float as objective value if and only if we have a FloatSum objective?

I tweaked some tests, but overall ok; docs also look fine.

Comment thread cpmpy/expressions/globalfunctions.py Outdated
:class:`FloatSum` is **not** an Expression nor a GlobalFunction.
It is only supported by some solvers (ortools, gurobi, cplex, scip, z3, minizinc, highs, hexaly),
and must be passed directly to that solver's :meth:`~cpmpy.solvers.solver_interface.SolverInterface.minimize`
/ :meth:`~cpmpy.solvers.solver_interface.SolverInterface.maximize`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For consistency: I think elsewhere in the code base we use :func: instead of :meth:?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Had to look up/ask Sphinx conventions: solver minimize/maximize/objective_value are class methods of the SolverInterface class, so :meth: with full qualified paths. :func: kept only for module-level functions (decompose_in_tree, boolvar, etc.). So kept meth here (with a qualified path update)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

also update the docs/developers.md (not on readthedocs public yet)

vars: NDVarArray
const: float

def __init__(self, coeffs: ListLike[float|np.floating], vars: ListLike[_NumVarImpl], const: float|np.floating = 0.0):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

docstring for the intit function?
(I suppose just the past paragraph in the docstring of the class , but witht the "Arguments:" and "Return" thingies?)

Comment thread cpmpy/expressions/globalfunctions.py Outdated
raise TypeError("FloatSum(coeffs, terms) expects at least one term")

def __repr__(self) -> str:
if self.const:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

At first I thought this was checking for None, but its checking if it's 0, maybe add a comment?

Comment thread cpmpy/expressions/globalfunctions.py Outdated
return None
return float(np.dot(self.coeffs, vals) + self.const)

def components(self, negbool=False) -> tuple[np.ndarray, NDVarArray, float]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can/should we type the NDVarArray that is returned here with _NumVarImpl?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Also allow_negbool would be more intuitive

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

allow_negbool: agreed

I don't know if there is a way to type the NDVarArray as containing _NumVarImpls? it contains objects from dtype perspective... not opening that can of worms : )

Comment thread cpmpy/expressions/globalfunctions.py Outdated
def __mul__(self, other: object) -> NoReturn: self._raise_objective_only()
def __rmul__(self, other: object) -> NoReturn: self._raise_objective_only()
def __neg__(self) -> NoReturn: self._raise_objective_only()
def __abs__(self) -> NoReturn: self._raise_objective_only()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Missing a couple here: pow, truediv, floordiv, mod
https://www.pythonmorsels.com/every-dunder-method/

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

sure

Comment thread cpmpy/expressions/globalfunctions.py Outdated
return np.asarray(ws, dtype=float), cpm_array(vs), const

def _raise_objective_only(self) -> NoReturn:
raise TypeError("FloatSum is objective-only. Use it directly in solver minimize()/maximize().")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Its more specific than that, maybe: FloatSum cannot be used as a subexpression or something?

(Btw: we probably want something similar implemented for DirectConstraint)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'm leaving the fun of doing it for DirectConstraint to others : )

Comment thread cpmpy/solvers/cplex.py Outdated
self.objective_value_ = round(obj_val)
else: # can happen with DirectVar or when using floats as coefficients
self.objective_value_ = float(obj_val)
else: # FloatSum objective, must be read through FloatSum.value()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Are we sure of this? Is there no other way the solver can return a float as objective value? (e.g., due to precision issues?
I think it's safer to explicitely test if we posted a FloatSum

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

right... that might happen; so we actually have to store the cpmpy object like we do for exact... refactoring

Comment thread cpmpy/solvers/cplex.py
if isinstance(expr, FloatSum):
ws, vs, const = expr.components()
self.user_vars.update(vs) # save user variables
self._obj_offset = const

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We don't do anything with this offset in the case of floatsum, right? Its computed through the FloatSum.value function.

BUt actually, for the non-floatsum case, we should add this to the objective value after solving...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

oh, good catch. We did it in SolveAll(), but not yet in Solve(), fixed

Comment thread cpmpy/solvers/ortools.py
if round(ort_obj_val) == ort_obj_val: # its integer
self.objective_value_ = int(ort_obj_val)
else: # FloatSum objective, must be read through FloatSum.value()
self.objective_value_ = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yes so for OR-Tools its probably fine to do it like this, but for the MIP solvers we should do an extra whether we have posted a FloatSum I think

@tias tias requested a review from IgnaceBleukx June 13, 2026 21:36
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