Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .zdev/comparison/TASKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- Generated by zdev. Edit task files, not this summary. -->

# Tasks: comparison

- Total: 5
- Ready: 0
- Blocked: 0
- Done: 5

| ID | Task | State | Blocked by |
| --- | --- | --- | --- |
| [comparison-001](tasks/001-add-space-comparison-and-integer-objective-implementations.md) | Add Space comparison and integer objective implementations | done | — |
| [comparison-002](tasks/002-compare-float-quality-independently-of-the-improvement-step.md) | Compare float quality independently of the improvement step | done | comparison-001 |
| [comparison-003](tasks/003-use-comparison-for-sequential-bab-and-restart-incumbent-upda.md) | Use comparison for sequential BAB and restart incumbent updates | done | comparison-001 |
| [comparison-004](tasks/004-use-comparison-for-parallel-bab-and-safely-report-comparison.md) | Use comparison for parallel BAB and safely report comparison failures | done | comparison-002, comparison-003 |
| [comparison-005](tasks/005-integrate-comparison-through-portfolios-and-complete-the-mig.md) | Integrate comparison through portfolios and complete the migration | done | comparison-004 |
7 changes: 7 additions & 0 deletions .zdev/comparison/area.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
schema_version = 1
tag = "comparison"
title = "Non-mutating solution comparison"
objective = "Define and integrate a non-mutating solution comparison contract for Gecode best-solution search."
branch = "feature/comparison"
lifecycle = "open"
base_commit = "6b7de57b0414fe2f8af3513743c9835840a3c019"
333 changes: 333 additions & 0 deletions .zdev/comparison/background/comparison-and-search.md

Large diffs are not rendered by default.

326 changes: 326 additions & 0 deletions .zdev/comparison/brief.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
+++
schema_version = 1
id = "comparison-001"
key = "integer-comparison"
area = "comparison"
status = "done"
complexity = "standard"
afk = true
priority = "high"
blocked_by = []
+++
# Add Space comparison and integer objective implementations

## Outcome

Models can compare spaces without mutation, with integer convenience classes and FlatZinc providing the standard implementations.

## Context

The approved [brief](../brief.md) defines four ordering outcomes and model-defined comparability. Space currently only has constrain(); MiniModel supplies scalar and lexicographic integer objectives, while Driver::ScriptBase and FlatZinc already have Gist display compare overloads. Start in kernel/core.hpp and core.cpp, minimodel.hh and optimize.cpp, driver.hh, and flatzinc.hh/flatzinc.cpp; use test/search.cpp for focused coverage.

## Boundaries

- Add the API, integer implementations, their documentation, and overload compatibility. Leave search arbitration and float implementations to dependent tasks; do not add generic assignment checks or a capability API.

## Done when

- [x] Space::compare(const Space&) const returns the four SpaceComparison outcomes; its default reports unsupported use through a Gecode exception. No unavailable result is added.
- [x] Scalar min/max and lexicographic min/max, plus FlatZinc integer optimization, compare in the agreed direction without mutation. Lexicographic comparison accepts a decisive assigned prefix without requiring later components; insufficient information for the requested comparison raises the appropriate error.
- [x] Focused tests cover direction, equivalence, a decisive prefix with an unassigned suffix, insufficient scalar data, and incompatible objective families/dimensions. The ordering and Gist display overloads remain callable, including through a custom script.

## Validation

- Build gecode-test and run the new comparison tests plus relevant existing integer/FlatZinc tests. Compile a representative custom script using both compare overloads; no new test harness.

## Result

Added and independently verified non-mutating Space comparison with integer objective implementations and overload compatibility.

Validation:

- Clean external CMake build of gecode-test passed.
- Search::Comparison and FlatZinc::IntegerObjectiveComparison passed.
- Complete existing FlatZinc suite and representative custom-script overload compilation passed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
+++
schema_version = 1
id = "comparison-002"
key = "float-comparison"
area = "comparison"
status = "done"
complexity = "advanced"
afk = true
priority = "normal"
blocked_by = ["comparison-001"]
+++
# Compare float quality independently of the improvement step

## Outcome

MiniModel and FlatZinc float optimization expose a documented comparison consistent with their existing pruning boundaries, without enforcing the step between compared solutions.

## Context

The [brief](../brief.md) specifies MiniModel upper-bound minimization/lower-bound maximization and FlatZinc lower-bound minimization/upper-bound maximization. The difference follows from scalar versus interval-valued strict thresholds; see [float analysis](../background/comparison-and-search.md). Start in minimodel/optimize.cpp, flatzinc/flatzinc.cpp, float/rel.cpp, and float/rel/nq.hpp.

## Boundaries

- Implement comparison and focused tests/documentation. Preserve constrain(), strictness, interval arithmetic, and step semantics; do not add a reporting-step filter or legacy comparison fallback.

## Done when

- [x] Both MiniModel float bases and FlatZinc float optimization implement the agreed family-specific ordering and document what makes their operands comparable, including tight non-singleton intervals.
- [x] Tests demonstrate ranking of sub-step improvements, equal keys, both directions, zero/nonzero step, strict threshold boundaries, and adjacent-endpoint intervals. Equal keys produce equivalent cuts and improved keys produce nested cuts on representative comparable solutions.
- [x] Comparison leaves both inputs unchanged. If a boundary check disproves cut compatibility, report its concrete counterexample rather than silently changing the approved semantics.

## Validation

- Run focused float comparison/cut tests and relevant existing float and FlatZinc tests. Derive the cut-nesting argument from the current posting code; no timing tests or broad floating-point matrix.

## Result

Implemented and independently verified endpoint-based float comparison with existing step-based pruning preserved.

Validation:

- Built gecode-test and ran the exact focused MiniModel and FlatZinc float comparison tests.
- Strict float relation checks and the complete FlatZinc test group passed.
- Independent review confirmed equal-cut equivalence, nested improved cuts, adjacent-endpoint behavior, and non-mutating comparison.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
+++
schema_version = 1
id = "comparison-003"
key = "sequential-incumbents"
area = "comparison"
status = "done"
complexity = "standard"
afk = true
priority = "normal"
blocked_by = ["comparison-001"]
+++
# Use comparison for sequential BAB and restart incumbent updates

## Outcome

Sequential BAB and RBS select external incumbents by comparison while preserving ordinary pruning and restart behavior.

## Context

Seq::BAB::constrain in search/seq/bab.hpp and RBS::constrain in search/seq/rbs.cpp currently mutate the retained solution and call status(). test/search.cpp has a balanced custom objective and existing restart/portfolio tests. CarSequencing is an example with a custom scalar constrain(). Follow the [brief](../brief.md); the [source map](../background/comparison-and-search.md) distinguishes arbitration from pruning.

## Boundaries

- Change external-incumbent arbitration, the directly affected model overrides, focused tests, and migration documentation. Keep ordinary sequential next(), path pruning, and restart hooks; no best-effort mode or policy framework.

## Done when

- [x] Incoming incumbents replace retained ones only when better; equal/worse leave them intact. A valid incomparable outcome is explicitly rejected by the initial engine policy, separately from model comparison errors.
- [x] Actual working spaces still receive constrain(), and recomputation marks and restart state remain correct. A small internal acceptance helper may keep later BAB/PBS policy handling consistent.
- [x] Relevant custom optimization test models and examples implement their ordering rather than receiving a generic default ranking. Focused tests cover late better/equal/worse updates, the balanced objective, and direct comparison-policy errors.

## Validation

- Run focused external-update tests and existing sequential BAB/RBS search tests, including stop/resume. Smoke-check the migrated optimization example. No new tests of unrelated pruning internals.

## Result

Implemented and independently verified comparison-based external incumbent updates for sequential BAB and RBS.

Validation:

- Search::ExternalIncumbent and focused balanced BAB stop/resume tests passed.
- RBS BAB coverage and car-sequencing smoke run passed.
- Independent review confirmed correct ordering, error separation, and continued pruning/restart behavior.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
+++
schema_version = 1
id = "comparison-004"
key = "parallel-bab"
area = "comparison"
status = "done"
complexity = "advanced"
afk = true
priority = "normal"
blocked_by = ["comparison-002", "comparison-003"]
+++
# Use comparison for parallel BAB and safely report comparison failures

## Outcome

Parallel BAB arbitrates solutions without mutating them, preserves pending-result correctness, and returns comparison failures to the controlling caller.

## Context

Par::BAB::solution and constrain in search/par/bab.hpp currently probe with constrain/status under m_search. search/par/engine.hpp returns queued results before resuming workers; support/thread/thread.cpp has no exception forwarding. The preceding tasks supply objective implementations and the initial acceptance policy. Follow the ownership and nesting requirements in the [brief](../brief.md).

## Boundaries

- Change BAB arbitration, its required queue/error handling, and focused tests/documentation. Preserve worker locks, ownership, pruning, and recomputation. Limit error transport to comparison and acceptance failures; no general thread-runtime rewrite.

## Done when

- [x] Candidates and external incumbents use incoming-versus-retained comparison. Only accepted incumbents are cloned/broadcast; completed solutions are never constrained or propagated to rank them.
- [x] After an external incumbent update, queued results that violate the new incumbent contract are not returned. Accepted queue ordering and ownership remain valid.
- [x] Missing comparison, model comparison errors, and initial-policy rejection of incomparability wake/block workers safely and reach the controlling call instead of terminating a detached thread, hanging, or reporting normal exhaustion. The engine can be safely destroyed after failure.
- [x] Focused tests check monotone results and the known optimum for a small custom objective, late external bounds with pending results, sub-step float ranking, and safe failure delivery.

## Validation

- Run focused parallel BAB tests and existing BAB stop/resume/recomputation coverage. Use bounded failure tests that detect hangs; do not require deterministic parallel solution counts or schedules.

## Result

Parallel BAB now arbitrates incumbents with non-mutating comparison, invalidates superseded queued results, and safely delivers comparison failures with restart support.

Validation:

- Built the gecode-test target successfully.
- Search::ParallelBABComparison passed 25 bounded iterations, including all failure categories and reset/reuse.
- Focused float and external-incumbent regressions passed repeatedly.
- Existing parallel BAB coverage passed under bounded execution, and git diff --check passed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
+++
schema_version = 1
id = "comparison-005"
key = "portfolio-comparison"
area = "comparison"
status = "done"
complexity = "advanced"
afk = true
priority = "normal"
blocked_by = ["comparison-004"]
+++
# Integrate comparison through portfolios and complete the migration

## Outcome

PBS, including nested RBS/BAB assets, consistently applies the initial comparison policy and safely delivers failures, with the complete change documented and regression-checked.

## Context

Par::PBS::CollectBest::add/constrain mutate the retained solution; reporter controls subsequent broadcasts. PBS::report and Slave::run coordinate n_busy, tostop, and the completion handshake. Nested asset next() calls can now raise comparison failures. Use search/par/pbs.hpp and hh, search/seq/pbs.hpp, and the existing SEBPBS tests in test/search.cpp; follow the [brief](../brief.md).

## Boundaries

- Integrate portfolio arbitration, necessary nested failure transport, focused tests, and user-facing migration notes. Preserve satisfaction collection, asset scheduling, and completion handshakes. Do not implement best-effort replacement, Pareto archives, or diversity search.

## Done when

- [x] CollectBest accepts only better candidates initially, preserves reporter on rejection, and never mutates a solution merely to rank it. External updates and pending results use the same policy as BAB/RBS.
- [x] Comparison and policy failures from the collector or nested asset calls stop/wake and complete safely, reaching the outer caller; deletion after failure does not hang or race worker completion.
- [x] Focused direct and mixed PBS/RBS/BAB tests establish expected objective improvement/optimum, sub-step float ranking, equivalent-candidate handling, and safe incomparable/error reporting. Existing satisfaction paths remain functional.
- [x] API/search documentation and the Gecode 7 changelog explain migration, model-defined comparability, float ranking, and current rejection policy. They explicitly preserve the possible future best-effort rule 'incomparable incoming solution becomes current best' without claiming completeness or requiring a changed comparison interface.

## Validation

- Run focused portfolio and nested-failure tests, then the existing Search:: suite and relevant FlatZinc checks. Audit remaining constrain/status calls to confirm they are pruning rather than arbitration. Run the existing no-thread build/check path for touched conditional code; no new configuration matrix.

## Result

PBS now applies non-mutating comparison consistently, safely returns nested comparison failures, and documents the Gecode 7 migration and future incomparability policy option.

Validation:

- Threaded and no-thread gecode-test builds passed.
- The complete Search::PBS:: matrix and focused mixed PBS/RBS/BAB, nested-failure, float, external-incumbent, and FlatZinc checks passed.
- Independent verification confirmed ownership, reporter retention, failure quiescence, satisfaction behavior, and remaining constrain/status pruning uses.
- git diff --check and immutable snapshot comparison passed.
6 changes: 6 additions & 0 deletions .zdev/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
schema_version = 1

[project]
name = "gecode"
record = "pull-request"
trunk = "main"
18 changes: 18 additions & 0 deletions changelog.in
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,24 @@ Date: unreleased
[DESCRIPTION]
This is the development changelog for the next Gecode release.

[ENTRY]
Module: search
What: change
Rank: major
[DESCRIPTION]
Best-solution search now uses Space::compare() when choosing between
independently produced solutions, including external incumbents and portfolio
assets. Optimization spaces with a custom objective must override compare() as
well as constrain(). compare() ranks solutions without changing them;
constrain() still posts improvement restrictions. Comparability is defined by
the model. The built-in floating-point models compare their documented
objective bounds, independently of the improvement step. Current exact engines
reject incomparable incoming solutions. A future best-effort policy could
instead make an incomparable incoming solution current best, without changing
the comparison interface, but that would not by itself guarantee completeness
or maximality. Adding the virtual comparison function changes the C++ ABI of
Space.

[RELEASE]
Version: 6.4.0
Date: 2026-07-15
Expand Down
12 changes: 11 additions & 1 deletion examples/car-sequencing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,17 @@ class CarSequencing : public Script {
rel(*this, nstall, IRT_LE, best.nstall.val());
}

/// Compare objective values
virtual SpaceComparison compare(const Space& _other) const {
const CarSequencing& other =
dynamic_cast<const CarSequencing&>(_other);
if (nstall.val() < other.nstall.val())
return SC_BETTER;
if (nstall.val() > other.nstall.val())
return SC_WORSE;
return SC_EQUIVALENT;
}

/// Print solution
virtual void
print(std::ostream& os) const {
Expand Down Expand Up @@ -634,4 +645,3 @@ namespace {
};

// STATISTICS: example-any

1 change: 1 addition & 0 deletions gecode/driver.hh
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,7 @@ namespace Gecode { namespace Driver {
template<class BaseSpace>
class ScriptBase : public BaseSpace {
public:
using BaseSpace::compare;
/// Constructor
ScriptBase(const Options& opt);
/// Constructor used for cloning
Expand Down
11 changes: 10 additions & 1 deletion gecode/flatzinc.hh
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ namespace Gecode { namespace FlatZinc {
Gecode::FloatVarArray fv_aux;
/// Indicates whether a float variable is introduced by mzn2fzn
std::vector<bool> fv_introduced;
/// Step by which a next solution has to have lower cost
/// Step used by float optimization cuts and comparison compatibility
Gecode::FloatNum step;
#endif
/// Whether the introduced variables still need to be copied
Expand Down Expand Up @@ -626,6 +626,15 @@ namespace Gecode { namespace FlatZinc {
/// Compare this space with space \a s and print the differences on
/// \a out
void compare(const Space& s, std::ostream& out) const;
/**
* \brief Compare compatible optimization objectives
*
* Integer objectives compare assigned values. Float objectives require
* the same direction and step, and assigned (possibly adjacent-endpoint)
* intervals. Float minimization ranks lower endpoints and maximization
* ranks upper endpoints, independently of the step used by constrain().
*/
virtual SpaceComparison compare(const Space& s) const;
/// Compare this space with space \a s and print the differences on
/// \a out using \a p
void compare(const FlatZincSpace& s, std::ostream& out,
Expand Down
35 changes: 34 additions & 1 deletion gecode/flatzinc/flatzinc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,11 @@ namespace Gecode { namespace FlatZinc {
intVarCount(-1), boolVarCount(-1), floatVarCount(-1), setVarCount(-1),
_optVar(-1), _optVarIsInt(true), _lns(0), _lnsInitialSolution(0),
_random(random),
_solveAnnotations(nullptr), needAuxVars(true) {
_solveAnnotations(nullptr),
#ifdef GECODE_HAS_FLOAT_VARS
step(0.0),
#endif
needAuxVars(true) {
branchInfo.init();
}

Expand Down Expand Up @@ -2076,6 +2080,35 @@ namespace Gecode { namespace FlatZinc {
}
}

SpaceComparison
FlatZincSpace::compare(const Space& s) const {
const FlatZincSpace* other = dynamic_cast<const FlatZincSpace*>(&s);
if (other == nullptr)
throw DynamicCastFailed("FlatZincSpace::compare");
if ((_optVarIsInt != other->_optVarIsInt) ||
(_method != other->_method) ||
((_method != MIN) && (_method != MAX)))
throw DynamicCastFailed("FlatZincSpace::compare");
if (_optVarIsInt) {
int a=iv[_optVar].val(), b=other->iv[other->_optVar].val();
if (a == b)
return SC_EQUIVALENT;
return ((a < b) == (_method == MIN)) ? SC_BETTER : SC_WORSE;
}
#ifdef GECODE_HAS_FLOAT_VARS
if (step != other->step)
throw DynamicCastFailed("FlatZincSpace::compare");
FloatVal a=fv[_optVar].val(), b=other->fv[other->_optVar].val();
FloatNum ak=(_method == MIN) ? a.min() : a.max();
FloatNum bk=(_method == MIN) ? b.min() : b.max();
if (ak == bk)
return SC_EQUIVALENT;
return ((ak < bk) == (_method == MIN)) ? SC_BETTER : SC_WORSE;
#else
throw DynamicCastFailed("FlatZincSpace::compare");
#endif
}

bool
FlatZincSpace::slave(const MetaInfo& mi) {
if (mi.type() == MetaInfo::RESTART) {
Expand Down
Loading
Loading