Skip to content
Open
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
23 changes: 23 additions & 0 deletions changelog.in
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,29 @@ Date: unreleased
[DESCRIPTION]
This is the development changelog for the next Gecode release.

[ENTRY]
Module: flatzinc
What: bug
Rank: minor
Thanks: Hillel Wayne
[DESCRIPTION]
Make the improvement cut for float objectives strict in the objective bound.
The cut kept the incumbent objective value until the objective was assigned,
so proving optimality could split other float variables down to single ulps
and fail to finish. Assigned objectives are accepted exactly as before, also
with step 0.

[ENTRY]
Module: minimodel
What: bug
Rank: minor
[DESCRIPTION]
Make the improvement cuts of FloatMinimizeSpace and FloatMaximizeSpace strict
in the cost bound, so the incumbent cost is pruned before the cost is
assigned. A new solution must now improve by at least the step, without the
rounding of the step subtraction admitting a smaller improvement. With step 0,
assigned costs are accepted exactly as before.

[RELEASE]
Version: 6.5.0
Date: 2026-10-31
Expand Down
40 changes: 30 additions & 10 deletions gecode/flatzinc/flatzinc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#include <string>
#include <sstream>
#include <limits>
#include <cmath>
#include <memory>
#include <unordered_set>

Expand Down Expand Up @@ -865,7 +866,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 @@ -1871,6 +1876,9 @@ namespace Gecode { namespace FlatZinc {
void
FlatZincSpace::runMeta(std::ostream& out, const Printer& p,
const FlatZincOptions& opt, Support::Timer& t_total) {
#ifdef GECODE_HAS_FLOAT_VARS
step = opt.step();
#endif
#ifdef GECODE_HAS_GIST
if (opt.mode() == SM_GIST) {
BlackBoxContextHandle& black_box_context = BlackBoxAccess::context(*this);
Expand Down Expand Up @@ -1917,9 +1925,6 @@ namespace Gecode { namespace FlatZinc {

#endif

#ifdef GECODE_HAS_FLOAT_VARS
step = opt.step();
#endif
o.threads = opt.threads();
o.nogoods_limit = opt.nogoods() ? opt.nogoods_limit() : 0;
if (opt.restart() != RM_NONE)
Expand Down Expand Up @@ -2068,12 +2073,27 @@ namespace Gecode { namespace FlatZinc {
static_cast<const FlatZincSpace*>(&s)->iv[_optVar].val());
} else {
#ifdef GECODE_HAS_FLOAT_VARS
if (_method == MIN)
rel(*this, fv[_optVar], FRT_LE,
static_cast<const FlatZincSpace*>(&s)->fv[_optVar].val()-step);
else if (_method == MAX)
rel(*this, fv[_optVar], FRT_GR,
static_cast<const FlatZincSpace*>(&s)->fv[_optVar].val()+step);
FloatVal best = static_cast<const FlatZincSpace*>(&s)->fv[_optVar].val();
// When minimizing, an assigned objective is below best - step
// exactly when its upper bound is below the lower bound of that
// interval (symmetrically when maximizing). Posting this strict
// bound prunes the incumbent value before the objective is
// assigned, and also guarantees progress when it never is.
if (_method == MIN) {
FloatNum n = (best-step).min();
if (n <= Float::Limits::min)
fail();
else
rel(*this, fv[_optVar], FRT_LQ,
std::nextafter(n, Float::Limits::min));
} else if (_method == MAX) {
FloatNum n = (best+step).max();
if (n >= Float::Limits::max)
fail();
else
rel(*this, fv[_optVar], FRT_GQ,
std::nextafter(n, Float::Limits::max));
}
#endif
}
}
Expand Down
18 changes: 16 additions & 2 deletions gecode/minimodel/optimize.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

#include <gecode/minimodel.hh>

#include <cmath>

namespace Gecode {

void
Expand Down Expand Up @@ -89,7 +91,15 @@ namespace Gecode {
dynamic_cast<const FloatMinimizeSpace*>(&_best);
if (best == nullptr)
throw DynamicCastFailed("FloatMinimizeSpace::constrain");
rel(*this, cost(), FRT_LE, best->cost().max()-step);
// An assigned cost is below the bound of the best solution minus the
// step exactly when its upper bound is below the lower bound of that
// difference. Posting this strict bound prunes the incumbent value
// before the cost is assigned.
FloatNum n = (FloatVal(best->cost().max())-step).min();
if (n <= Float::Limits::min)
fail();
else
rel(*this, cost(), FRT_LQ, std::nextafter(n, Float::Limits::min));
}


Expand All @@ -99,7 +109,11 @@ namespace Gecode {
dynamic_cast<const FloatMaximizeSpace*>(&_best);
if (best == nullptr)
throw DynamicCastFailed("FloatMaximizeSpace::constrain");
rel(*this, cost(), FRT_GR, best->cost().min()+step);
FloatNum n = (FloatVal(best->cost().min())+step).max();
if (n >= Float::Limits::max)
fail();
else
rel(*this, cost(), FRT_GQ, std::nextafter(n, Float::Limits::max));
}

#endif
Expand Down
48 changes: 48 additions & 0 deletions test/flatzinc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,54 @@ namespace Test { namespace FlatZinc {

TupleSetAutoRepresentation tuple_set_auto_representation;

#ifdef GECODE_HAS_FLOAT_VARS
/// Verify that the float improvement cut prunes the incumbent value
/// before the objective is assigned.
class FloatCutExcludesIncumbent : public Base {
private:
/// Return the objective bound after constraining by an incumbent
/// with objective 0.5
static bool cutBound(const std::string& source,
Gecode::FloatNum& bound) {
using Gecode::FlatZinc::FlatZincSpace;
Gecode::FlatZinc::Printer p;
std::stringstream ss(source);
std::unique_ptr<FlatZincSpace>
fg(Gecode::FlatZinc::parse(ss, p, olog));
if (!fg || (fg->status() == Gecode::SS_FAILED))
return false;
std::unique_ptr<FlatZincSpace>
best(static_cast<FlatZincSpace*>(fg->clone()));
Gecode::rel(*best, best->fv[0], Gecode::FRT_EQ, 0.5);
if (best->status() == Gecode::SS_FAILED)
return false;
std::unique_ptr<FlatZincSpace>
s(static_cast<FlatZincSpace*>(fg->clone()));
s->constrain(*best);
if ((s->status() == Gecode::SS_FAILED) || s->fv[0].assigned())
return false;
bound = (source.find("minimize") != std::string::npos) ?
s->fv[0].max() : s->fv[0].min();
return true;
}
public:
FloatCutExcludesIncumbent(void)
: Base("FlatZinc::Float::CutExcludesIncumbent") {}

virtual bool run(void) {
Gecode::FloatNum minBound, maxBound;
return
cutBound("var 0.0..1.0: x :: output_var;\nsolve minimize x;\n",
minBound) &&
cutBound("var 0.0..1.0: x :: output_var;\nsolve maximize x;\n",
maxBound) &&
(minBound < 0.5) && (maxBound > 0.5);
}
};

FloatCutExcludesIncumbent float_cut_excludes_incumbent;
#endif

/// Verify that statistics do not override an explicit Gist mode.
class GistStatisticsMode : public Base {
private:
Expand Down
63 changes: 63 additions & 0 deletions test/float/basic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@

#include "test/float.hh"

#include <gecode/minimodel.hh>

#include <cmath>
#include <memory>

namespace Test { namespace Float {

Expand Down Expand Up @@ -116,6 +119,66 @@ namespace Test { namespace Float {
namespace {
Median median;
}

/// Space minimizing or maximizing a single float variable
template<class Optimize>
class OptimizeSpace : public Optimize {
public:
/// The cost variable
Gecode::FloatVar x;
/// Create space with cost variable in [0,1]
OptimizeSpace(void) : x(*this,0.0,1.0) {}
/// Constructor for cloning \a s
OptimizeSpace(OptimizeSpace& s) : Optimize(s) {
x.update(*this,s.x);
}
/// Copy during cloning
virtual Gecode::Space* copy(void) {
return new OptimizeSpace(*this);
}
/// Return cost
virtual Gecode::FloatVar cost(void) const {
return x;
}
};

/// Test that the MiniModel float cuts prune the incumbent value
class OptimizeCut : public Base {
private:
/// Return whether constraining by an incumbent with cost 0.5 leaves
/// an unassigned cost and store its remaining domain in \a d
template<class Optimize>
static bool cut(Gecode::FloatVal& d) {
using namespace Gecode;
OptimizeSpace<Optimize> root;
std::unique_ptr<OptimizeSpace<Optimize>>
best(static_cast<OptimizeSpace<Optimize>*>(root.clone()));
rel(*best, best->x, FRT_EQ, 0.5);
if (best->status() == SS_FAILED)
return false;
std::unique_ptr<OptimizeSpace<Optimize>>
s(static_cast<OptimizeSpace<Optimize>*>(root.clone()));
s->constrain(*best);
if ((s->status() == SS_FAILED) || s->x.assigned())
return false;
d = s->x.domain();
return true;
}
public:
/// Create and register test
OptimizeCut(void) : Base("Float::Basic::OptimizeCut") {}
/// Perform test
virtual bool run(void) {
using namespace Gecode;
FloatVal min, max;
return cut<FloatMinimizeSpace>(min) && cut<FloatMaximizeSpace>(max) &&
(min.max() < 0.5) && (max.min() > 0.5);
}
};

namespace {
OptimizeCut optimize_cut;
}
//@}

}
Expand Down
Loading