Skip to content

Conversation

@IgnaceBleukx
Copy link
Collaborator

@IgnaceBleukx IgnaceBleukx commented Jun 18, 2025

Proposal to omit the end variables in scheduling constraints such as Cumulative and NoOverlap.

Makes end None by default, and adapts solver behaviour accordingly.
It was not possible to make the "dummy" end variables in the constructor, as then they would be added to user_vars in the solver interface, which is incorrect.
Most solvers don't actually need the end variables in their interface either, except for OR-Tools.

Added a helper-function get_end_vars to create these dummy end variables.

@IgnaceBleukx IgnaceBleukx requested a review from Dimosts June 18, 2025 10:03
assert is_any_list(end), "end should be a list"

assert demand is not None, "demand should be provided but was None"
assert capacity is not None, "capacity should be provided but was None"
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't know what should be done here, but I am not really a fan of giving them a default value (making them optional) and then not allowing the optional value.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I also agree it is quite annoying... For now this is the best I could come up with, as you cannot make an argument in the middle optional...

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am still troubled by this. So, someone either has to actually use the end var after all, or has to use kwargs to define demand and capacity, which are optional but not that optional after all.

Both minizinc (https://docs.minizinc.dev/en/stable/predicates.html) and essence (https://arxiv.org/pdf/2201.03472) actually do not accept end variables at all, and create them for solvers that need them. Should we stick to having them after all?

The question is what if the user needs the end variables for something else I guess; In this case, if the users define themselves the end = start + duration constraints, it is ok. Common subexpression elimination should capture it during the decomposition if needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm yeah, but removing it all together will break every CPMpy model currently out there that uses a Cumulative constraint...
I don't really have too much of an opinion on whether we want to keep the end variables or not, but if we decide to remove them, we should wait a couple of releases and raise a deprecation warning here

@IgnaceBleukx IgnaceBleukx changed the title Omit end variables in scheduling constraints Refactor and update Cumulative and NoOverlap constraints Sep 9, 2025
@IgnaceBleukx
Copy link
Collaborator Author

I used this PR to add some more changes to the Cumulative and NoOverlap constraints.
In particular:

  • Refactored .decompose() and .value() functions
  • Specified allowed types for arguments in documentation
  • Allow for negative durations, and ensure consistent solver behaviour
  • Allow for negative demands, and ensure consistent solver behaviour
  • Allow to omit end variables (original scope of this PR...)

This PR now also fixes #685

@Dimosts can you re-review these changes?

@Dimosts Dimosts self-requested a review September 9, 2025 13:00
Copy link
Collaborator

@Dimosts Dimosts left a comment

Choose a reason for hiding this comment

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

Mostly approved. See my first comment on end variables and whether we actually want to have them. Not a required change though, I just think it overcomplicates the code if we keep it.

assert is_any_list(end), "end should be a list"

assert demand is not None, "demand should be provided but was None"
assert capacity is not None, "capacity should be provided but was None"
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am still troubled by this. So, someone either has to actually use the end var after all, or has to use kwargs to define demand and capacity, which are optional but not that optional after all.

Both minizinc (https://docs.minizinc.dev/en/stable/predicates.html) and essence (https://arxiv.org/pdf/2201.03472) actually do not accept end variables at all, and create them for solvers that need them. Should we stick to having them after all?

The question is what if the user needs the end variables for something else I guess; In this case, if the users define themselves the end = start + duration constraints, it is ok. Common subexpression elimination should capture it during the decomposition if needed.

@IgnaceBleukx
Copy link
Collaborator Author

@Dimosts and/or @tias could you do a (re-)review on this one? It currently blocks #676 as there is a lot of refactoring in this one, that I believe should go in first before we get to the optional intervals

@Dimosts Dimosts self-requested a review November 3, 2025 12:49
@tias tias self-requested a review November 3, 2025 12:53
@tias tias added this to the v.0.10.0 milestone Nov 17, 2025
Copy link
Collaborator

@tias tias left a comment

Choose a reason for hiding this comment

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

some comments, looks great otherwise

demand_at_i = demand[i]
for j in range(len(start)):
if i != j:
demand_at_i += demand[j] * ((start[j] <= start[i]) & (end[j] > start[i]))
Copy link
Collaborator

Choose a reason for hiding this comment

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

I find this a hard to read style...
I would prefer

demands_at_i = []
for ...

cons.add( demand[i] + cp.sum(demands_at_i) <= capacity )

which is closer to the textbook formula

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it needs a bit more explanation, e.g. "only check at the start-time of each task"
and maybe rename 'demand_at_i' to 'demand_with_i' or so

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated both the doc and the code to make this more explicit

demand_at_t += demand * ((start[job] <= t) & (t < end[job]))
else:
demand_at_t += demand[job] * ((start[job] <= t) & (t < end[job]))
demand_at_t += demand[job] * ((start[job] <= t) & (end[job] > t))
Copy link
Collaborator

Choose a reason for hiding this comment

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

like above, please do this as a list instead of the less readable accumulating sum, so that it shows cons.add( cp.sum(demands_at_t) <= capacity )

Or you could actually also make a helper function demands_at(t) and then
for t in ...:
cons.add( cp.sum(demands_at(t)) <= capacity )

not sure what is more readable

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Rewrote it as a single sum. As this is for each time-step, I think it's more natural to write it functionally

@IgnaceBleukx IgnaceBleukx merged commit e536404 into master Dec 18, 2025
1 check passed
@IgnaceBleukx IgnaceBleukx deleted the no_end_cumulative branch December 18, 2025 11:40
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.

4 participants