Skip to content

refactor(pumpkin-solver): Bulk create propagators before root propagation in FlatZinc reader#407

Closed
maartenflippo wants to merge 14 commits intomainfrom
feat/solver-from-state
Closed

refactor(pumpkin-solver): Bulk create propagators before root propagation in FlatZinc reader#407
maartenflippo wants to merge 14 commits intomainfrom
feat/solver-from-state

Conversation

@maartenflippo
Copy link
Copy Markdown
Contributor

@maartenflippo maartenflippo commented Apr 9, 2026

We change when we run root-level propagation. Previously, we ran propagation after every individual constraint was added. This is slightly slower than running all propagation at once, but more importantly, running the root-propagation once makes debugging a lot simpler :).

This PR encompasses the following change:

  • The Constraint::post function takes a State instead of a Solver
  • The Constraint::post function does not return a Result anymore, since it cannot detect a ConstraintOperationError.
  • Whether equals can use domain propagation is now an argument of the various constraint functions, since it does not have access to whether the solver is proof logging or not
  • The nogood propagator handle used by the solver is publicly exposed, to allow adding of nogoods through the conjunction and clause constraints
  • Introduced Solver::from_state to construct the solver from a preconfigured State.

Note that Solver::add_constraint still exists.

Aside from these changes, there are various test cleanups to now use State directly, rather than construct Assignments, NotificationEngine, etc.

Copy link
Copy Markdown
Contributor

@ImkoMarijnissen ImkoMarijnissen left a comment

Choose a reason for hiding this comment

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

My main gripe is that we now need to pass the nogood propagator when creating all of these constraints.

However, I do not see an easy way out of this.

Comment on lines -144 to -178

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn less_than_conflict() {
let mut solver = Solver::default();

let constraint_tag = solver.new_constraint_tag();
let x = solver.new_named_bounded_integer(0, 0, "x");

let result = less_than([x], 0, constraint_tag).post(&mut solver);
assert_eq!(
result,
Err(ConstraintOperationError::InfeasiblePropagator),
"Expected {result:?} to be an `InfeasiblePropagator` error"
);
}

#[test]
fn greater_than_conflict() {
let mut solver = Solver::default();

let constraint_tag = solver.new_constraint_tag();
let x = solver.new_named_bounded_integer(0, 0, "x");

let result = greater_than([x], 0, constraint_tag).post(&mut solver);
assert_eq!(
result,
Err(ConstraintOperationError::InfeasiblePropagator),
"Expected {result:?} to be an `InfeasiblePropagator` error"
);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These should not be removed

literals: impl Into<Vec<Literal>>,
literals: impl IntoIterator<Item = Predicate>,
constraint_tag: ConstraintTag,
propagator_handle: PropagatorHandle<NogoodPropagator>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requires documentation; I would not find it clear why this is necessary as a user, or how to get this information

Comment on lines +16 to +25
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EqualityConsistency {
/// Use bound consistent propagation.
Bound,
/// If the constraint is over two variables, use domain consistent propagation.
///
/// This is only useful when variables are actually equal. In the future this will likely
/// become a preprocessing step to unify the variables.
Domain,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think that it would be better to create a separate binary_equals constraint for this behaviour, no? Now it is a lot of passing around in places where it does not make sense

Comment on lines -565 to -572
/// Default brancher implementation
impl Solver {
/// Creates an instance of the [`DefaultBrancher`].
pub fn default_brancher(&self) -> DefaultBrancher {
DefaultBrancher::default_over_all_variables(self.satisfaction_solver.assignments())
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

?

Comment on lines +531 to +540
pub fn add_nogood(
&mut self,
nogood: Vec<Predicate>,
inference_code: InferenceCode,
context: &mut PropagationContext,
) -> PropagationStatusCP {
self.add_permanent_nogood(nogood, inference_code, context)
nogood: impl IntoIterator<Item = Predicate>,
constraint_tag: ConstraintTag,
) {
let nogood = nogood.into_iter().collect::<Vec<_>>();

assert!(!nogood.is_empty(), "cannot add empty nogoods");

self.newly_added_nogoods.push((constraint_tag, nogood));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wouldn't this lead to issues with enqueuing?

Now you add a nogood but you do not register anything. It would be possible for every predicate in that nogood to become true (i.e., a conflict to occur) without the propagator being notified, no?

@maartenflippo
Copy link
Copy Markdown
Contributor Author

The interface we end up with is not ergonomic, so we'll close this in favor of more discovery

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