feat(pumpkin-solver): Implement consistency checker infrastructure#449
feat(pumpkin-solver): Implement consistency checker infrastructure#449maartenflippo wants to merge 23 commits into
Conversation
|
Following up on our discussion from Friday: (this is a long post, and some of the things are interconnected. It might be best to discuss it instead of text.) Support generatorConceptually, it would make sense that propagators generate propagation/retention certificates, since the propagator is to prove that it is working correctly. Currently, the propagator generates propagation certificates as explanations, good. But retention certificates are attached to inference checkers through the support generator trait. I think we could have inference checkers only do inference checking, and leave the retention certificates to the propagator. This also makes sense since the propagator may use some internal data structures / algorithms to construct the certificates. But in any case, generating retention certificates should be an immutable operation so the trace does not change. Interface for inference and retention checkersI like the interface for inference checkers, because it is reasonably decoupled from the solver, e.g., it has its own variables as input to which the solver translates to. I think the retention checker could use a similar interface. Now the retention checkers rely on the internal solver details through the scope and local ids, but probably they could use the same as the inference checkers. This way we could have the retention checkers more decoupled from the solver. Reified propagator wrappings and different way of registering variables for propagatorsI understand that we have special consideration for the reified propagator, e.g., the constructor context has Is the main problem that we do not know how many local ids will the underlying propagator make, which makes it difficult for the generic reified propagator to understand which local id it will take? I think we could consider a slightly different way of registering variables for propagators, which could solve this issue. Currently we ask propagators to, within the constructor, call "context.register". Instead, maybe we could do the following: as part of the return value of the propagator constructor, we return the propagator AND a mapping 'variable -> (local id, domain_event)'. Essentially, the propagator constructor tells the solver how it renamed the variables locally. The solver then registers the variables for watching after creating the propagator. This might be a bit cleaner, and it will be harder to forget to register variables, since registering variables is part of the output of the create function. It would also help with the reified propagator situation, since then we would not need to wrap checkers (but see point below, very related), simply the create function will be called of the underlying propagator, and then the reified propagator can process that and see which local id it needs to select for the reified variable. Creating propagators together with checkersIn the current design, the user is responsible for calling "add checkers" functions, and there is a lot of flexibility. I am thinking we might be able to streamline this a bit. Ideally, the propagator constructor returns an initialised propagator, but also returns inference checker(s) and a retention checker, e.g., (propagator, list of inference checkers, list of retention checkers, possibly also a mapping from variables to local ids and their domain events). This way it is clearer that checkers are core parts, and not optional. Now this seems to be difficult to do fully, but maybe we can still come up with something in this direction, see below. First observation is that perhaps the propagator and both checkers should take as input the PropagatorArgs struct. Conceptually this makes sense, since those input define what the propagator is, which is clearly what the checkers care about. Being sure the correct number of checkers is returned: We discussed it, but after looking through the code again, I get the impression that each Specifying the retention checker: the propagator specifies the checker using an enum, which has the types (that may have additional fields) to specify which type of consistency is guaranteed (domain/bounds weak/regular consistency, or custom). This is a bit tricky because I am not entirely sure how to define consistency for a propagator if it can have multiple inferences or constraints. Should propagators define consistency for each individual variable? Say these variables are going to be bound-consistent, and these domain-consistent? Is it a fair assumption that a propagator has one consistency type per constraint tag it uses? This seems reasonable to me for bound/domain consistent propagators. For weakly consistent ones, it seems to me that retention consistency is actually defined on the level of an inference code and not on a constraint. Assuming one retention per constraint tag, the propagator specifies the consistency as an enum with several types (with each type having its own fields). These enum types dictate how feasible solutions are going to checked (using one of the inference checkers) or how the infeasibility function will be used to check consistency (see below for infeasibility function). And then there is a fifth option ‘custom’ which then allows for any type of consistency checking that we have not yet covered with the ‘standard’ options. Infeasibility function interfaceFor weakly consistent propagators, we use the infeasibility function to check whether it is consistent. Since we already learned that this can be a major bottleneck, then we can design the interface to allow the option for more efficient checking. The infeasibility function could have this type of workflow (functions): (The alternative to the above is to have a checker with no initialisation apart from copying the domain, so then checking needs to recompute things. This is more trustworthy but too slow potentially. The solver would simply initialise the function, and ask for each relevant value. Contexts for checkers?Minor point: do we want checkers to have "contexts" instead of receiving a list of arguments? We use this design everywhere so I am wondering if we should also use it for checkers. |
Support generator
This could also work. As long as something implements the Interface for inference and retention checkers
We could extract a trimmed-down version of our The only thing that is not clear to me is how we would deal with views. I suppose the In general, I think it would be amazing to extract the retention checkers into a separate library. Just as with everything I do in pumpkin, I want the code to be good and re-usable, which is no different here. However, at the moment I do have to make choices with my time, and as I raised in the meeting as well, especially in the next two months I feel that extracting the retention checkers would just be one of the lowest priority things for me. That means we could end up blocking this PR, since it would require an interface change. So we would have to discuss what the pragmatic way forward is on this. Reified propagator wrappings and different way of registering variables for propagators
I suppose this can work as well. For e.g. the nogood propagator, which does not register anything in the constructor, this mapping can be empty. I agree it is also much more composable than the interface we have now. For this I would suggest doing a separate PR first, though. It would be better to review this change in isolation.
This is not true. I remind you of the example where a clausal encoding of a constraint that introduces Upon reflection, I suppose it is true for the inference checkers in one propagator during the constructor. In the example of the clausal encoding, the inference checkers are created when the constraint is added to the propagator, but the constructor for the clausal propagator does not register any. Similarly, in the example of
We actually used to have this design. Yet we moved away from it because we gained little from it, whilst making it difficult to debug (due to the indirection), and we had to always carry the mapping Perhaps we will land on a preferable API that would benefit from from requesting inference codes, in which case we can revert the change. One benefit immediately is that the codes would implement Creating propagators together with checkers
I think this is not entirely true. E.g. if we post a time-table retention checker and an edge-finding retention checker for cumulative, even though the reasoning is not comparable, at fixed point both would need to accept the domains as they are for the consistency check to pass. But you are right that it means there is a many to one relationship between consistency checkers and constraint tags, which is already supported to handle decompositions.
I do not see what the enum version gives us that is not already afforded by the current design. The enum variants will just be implementations of the Infeasibility function interface
This is a great idea. I assume you intend that we call the initialization function once per fixed point, but that state can then be re-used for subsequent calls to determine whether a value is supported?
I would assume this separation is worth it, since we know from experience that it is still important to get somewhat deep into the search tree to be confident in the propagators. Contexts for checkers?
I personally don't think so. The checkers take only few arguments, which means it is much clearer to spell those arguments out. The contexts make sense where we need many arguments (e.g. the resolution context combines so many fields that they would be impractical as individual arguments). |
ImkoMarijnissen
left a comment
There was a problem hiding this comment.
About halfway through, but these are some intermediate comments.
Overall, I like the interface, though I think it is difficult to understand all of the different types (which is unavoidable with how general we want it to be). Perhaps we should add a dedicated page to all the different types in the checkers and how they interact with one another.
| use crate::checkers::Scope; | ||
| use crate::propagation::Domains; | ||
|
|
||
| /// A runtime verifier that determines whether domains are sufficiently pruned. |
There was a problem hiding this comment.
I would argue that this is a bit too vague
| impl Clone for BoxedRetentionChecker { | ||
| fn clone(&self) -> Self { | ||
| BoxedRetentionChecker(dyn_clone::clone_box(&*self.0)) | ||
| } | ||
| } |
There was a problem hiding this comment.
Couldn't this be implemented using the dyn_clone::clone_trait_object! macro?
| impl_scope_from_tuple!(la,va: VA, lb,vb: VB); | ||
| impl_scope_from_tuple!(la,va: VA, lb,vb: VB, lc,vc: VC); |
There was a problem hiding this comment.
Maybe add a small comment what this is doing; spacing is off as well
| impl_scope_from_tuple!(la,va: VA, lb,vb: VB); | ||
| impl_scope_from_tuple!(la,va: VA, lb,vb: VB, lc,vc: VC); | ||
|
|
||
| pub trait ScopeItem { |
| /// The checkers to run the next time. | ||
| queue: Vec<CheckerId>, |
There was a problem hiding this comment.
Seems like we do not need this field when we have enqueued? Is it just to prevent the iteration complexity of a bitset?
| // The support was incomplete or not a solution. Either way, the | ||
| // consistency check fails. | ||
| return false; | ||
| } |
There was a problem hiding this comment.
I think this is one example where it would be nice to have either a Result with an error enum or just an enum to provide this info directly
| let lb = domains.lower_bound(&domain) as f32; | ||
| let ub = domains.upper_bound(&domain) as f32; | ||
|
|
||
| lb <= *self && *self <= ub |
There was a problem hiding this comment.
Could suffer from floating point inaccuracies; should we use f32::EPSILON for this comparison (similar to np.isclose)?
| #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] | ||
| pub struct UnsupportedValue(pub(crate) i32); | ||
|
|
||
| /// Implementors know how to unpack an [`UnsupportedValue`]. |
There was a problem hiding this comment.
I would rewrite this documentation, it is not clear what the trait itself represents
| } | ||
| } | ||
|
|
||
| /// A domain value that needs to be unpacked through [`UnpackUnsupportedValue::unpack`]. |
There was a problem hiding this comment.
Could add information about what it means to be unsupported
| impl<Key> Clone for KeyedBitSet<Key> { | ||
| fn clone(&self) -> Self { | ||
| Self { | ||
| bitset: self.bitset.clone(), | ||
| key: PhantomData, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl<Key> Default for KeyedBitSet<Key> { | ||
| fn default() -> Self { | ||
| Self { | ||
| bitset: BitSet::default(), | ||
| key: PhantomData, | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Just out of curiosity; these could not be derived?
|
Summary of the meeting with @EmirDe and @ImkoMarijnissen: Most of the interface changes proposed by @EmirDe are good, and we will make the necessary changes. A few things of note:
Sketch of part of the design: |
commit b95ad41 Author: Maarten Flippo <maartenflippo@outlook.com> Date: Wed May 27 23:29:26 2026 +1000 Also update the proof processor commit b456892 Author: Maarten Flippo <maartenflippo@outlook.com> Date: Wed May 27 23:18:14 2026 +1000 Implement the new interface in the rest of pumpkin commit c5290b1 Author: Maarten Flippo <maartenflippo@outlook.com> Date: Wed May 27 22:46:16 2026 +1000 Remove Watchers::watch_all in favor of event target commit 27ed751 Author: Maarten Flippo <maartenflippo@outlook.com> Date: Wed May 27 22:28:36 2026 +1000 Do the registration in the state commit 4dd4222 Author: Maarten Flippo <maartenflippo@outlook.com> Date: Wed May 27 22:26:41 2026 +1000 Remove registration from PropagatorConstructorContext commit 39f97fd Author: Maarten Flippo <maartenflippo@outlook.com> Date: Wed May 27 22:09:21 2026 +1000 PropagatatorConstructor::create now returns an EventRegistration commit 7777f42 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun May 24 12:56:42 2026 +1000 chore(deps): bump enumset from 1.1.12 to 1.1.13 (#452) Bumps [enumset](https://github.com/Lymia/enumset) from 1.1.12 to 1.1.13. <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/Lymia/enumset/blob/main/RELEASES.md">enumset's changelog</a>.</em></p> <blockquote> <h1>Version 1.1.13 (2026-05-18)</h1> <ul> <li>Revert a semver breaking change introduced in 1.1.12 relating to using functions like <code>EnumSet<T>.symmetric_difference</code> with <code>Enum::A.into()</code>.</li> <li>Deprecate <code>EnumSet<T>.symmetrical_difference</code> in favor of <code>symmetric_difference</code>, to better match the standard library sets.</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/Lymia/enumset/commit/3ccc5704ec01c3d269852045577511ac21b06ff4"><code>3ccc570</code></a> Bump version to 1.1.13</li> <li><a href="https://github.com/Lymia/enumset/commit/fec58cc76ded5a1a050544abdd7f5e73cab1b11a"><code>fec58cc</code></a> Bless new trybuild output.</li> <li><a href="https://github.com/Lymia/enumset/commit/2bc3757fa104e13ce8388de93d48795a6f2be44b"><code>2bc3757</code></a> Deprecate symmetrical_difference in favor of symmetric_difference</li> <li><a href="https://github.com/Lymia/enumset/commit/9cfdb1d6736e4e4c9bd4482988d27a6f046ac8fa"><code>9cfdb1d</code></a> Fix accidental breaking change in the signatures of e.g. EnumSet.is_disjoint</li> <li><a href="https://github.com/Lymia/enumset/commit/117824aa804d883145c14101e4342885b6445ed7"><code>117824a</code></a> Clarify the format of the bit indexes returned.</li> <li>See full diff in <a href="https://github.com/Lymia/enumset/compare/v1.1.12...v1.1.13">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Maarten Flippo <maartenflippo@outlook.com> commit dfbdff1 Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com> Date: Sun May 24 04:54:59 2026 +0200 fix: README links + updating component overview (#453) commit bbfd857 Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com> Date: Thu May 21 09:38:47 2026 +0200 chore: avoid rebuilding unnecessarily (#450) #438 introduced a `rerun-if-changed` based on git directories. However, this causes rebuilding even if nothing has changed. I propose to remove these lines (i.e., to go back to the rebuilding strategy before #438). commit 7425839 Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com> Date: Thu May 21 09:33:32 2026 +0200 fix: disambiguate .msc files (#451) Currently, we have two separate [`.msc`](https://docs.minizinc.dev/en/stable/fzn-spec.html#solver-configuration-files) files: one for "default" Pumpkin and another for Pumpkin with proof logging. However, there is a name clash when using these two approaches, making it unclear which library is in use. This PR fixes this by changing the name in the `pumpkin-for-proofs.msc` file. It will then show up as follows in the listing: ``` Pumpkin 0.1 (nl.tudelft.algorithmics.pumpkin, cp, lcg, int) PumpkinProof 0.1 (nl.tudelft.algorithmics.pumpkin, cp, lcg, int) ``` We could also consider moving it to a separate folder.
A proper implementation of consistency checkers (see #383), with lessons learned from #374.
TODO
PropagatorConstructor::createnow returns event registrations #456