SwarmSolver: handle an empty or reversed variable range
When a variable's lower and upper bound are equal, or when contradictory constraints leave the lower bound above the upper one, the range is empty or reversed. boundVariable kept wrapping an out of range value without the step ever bringing it inside, so solve never returned. The random distributions in initializeVariables and std::clamp in clampVariable are also undefined when the lower bound is not below the upper one. Treat an empty or reversed range as having only the lower bound: return it directly in clampVariable and boundVariable, and seed it as the value in initializeVariables. boundVariable now also wraps a normal out of range value in one step instead of looping. Add a test with a variable bounded both below 5 and above 10. Before the fix solve looped forever. It now returns and reports the model as not solved. Signed-off-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.co.uk> Change-Id: Ia1789490160f93ef986d8eabb564f3aa78d94dc0 Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/4052 Tested-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.com> Reviewed-by: Miklos Vajna <vmiklos@collabora.com> (cherry picked from commit 40306021956262ebeac85f575780904b644f56ec) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/208437 Tested-by: Xisco Fauli <xiscofauli@libreoffice.org> Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
This commit is contained in:
committed by
Xisco Fauli
parent
5a487764c0
commit
22f84b1b58
@@ -73,6 +73,7 @@ class SwarmSolverTest : public UnoApiTest
|
||||
void testParticleSwarmResultLength();
|
||||
void testParticleSwarmVelocityNotInitializedAsPosition();
|
||||
void testUnreadableConstraintStillChecksOthers();
|
||||
void testContradictoryBoundsTerminate();
|
||||
|
||||
public:
|
||||
SwarmSolverTest()
|
||||
@@ -93,6 +94,7 @@ public:
|
||||
CPPUNIT_TEST(testParticleSwarmResultLength);
|
||||
CPPUNIT_TEST(testParticleSwarmVelocityNotInitializedAsPosition);
|
||||
CPPUNIT_TEST(testUnreadableConstraintStillChecksOthers);
|
||||
CPPUNIT_TEST(testContradictoryBoundsTerminate);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
};
|
||||
|
||||
@@ -651,6 +653,48 @@ void SwarmSolverTest::testUnreadableConstraintStillChecksOthers()
|
||||
!xSolver->getSuccess());
|
||||
}
|
||||
|
||||
void SwarmSolverTest::testContradictoryBoundsTerminate()
|
||||
{
|
||||
// Regression: a variable bounded below 5 and above 10 has an empty, reversed
|
||||
// range. The reflection in boundVariable used to keep wrapping such a value
|
||||
// without ever landing inside the range, so solve never returned. It must now
|
||||
// finish and report the model as not solved.
|
||||
|
||||
loadFromFile(u"Simple.ods");
|
||||
|
||||
uno::Reference<sheet::XSpreadsheetDocument> xDocument(mxComponent, uno::UNO_QUERY_THROW);
|
||||
|
||||
uno::Reference<sheet::XSolver> xSolver;
|
||||
xSolver.set(m_xContext->getServiceManager()->createInstanceWithContext(
|
||||
u"com.sun.star.comp.Calc.SwarmSolver"_ustr, m_xContext),
|
||||
uno::UNO_QUERY_THROW);
|
||||
|
||||
table::CellAddress aObjective(0, 1, 1);
|
||||
uno::Sequence<table::CellAddress> aVariables{ { 0, 1, 0 } };
|
||||
|
||||
// Both act on the variable, so they fold into its bounds: lower 10, upper 5
|
||||
uno::Sequence<sheet::SolverConstraint> aConstraints{
|
||||
{ /* [0] Left */ table::CellAddress(0, 1, 0),
|
||||
/* Operator */ sheet::SolverConstraintOperator_GREATER_EQUAL,
|
||||
/* Right */ uno::Any(10.0) },
|
||||
{ /* [1] Left */ table::CellAddress(0, 1, 0),
|
||||
/* Operator */ sheet::SolverConstraintOperator_LESS_EQUAL,
|
||||
/* Right */ uno::Any(5.0) }
|
||||
};
|
||||
|
||||
xSolver->setDocument(xDocument);
|
||||
xSolver->setObjective(aObjective);
|
||||
xSolver->setVariables(aVariables);
|
||||
xSolver->setConstraints(aConstraints);
|
||||
xSolver->setMaximize(false);
|
||||
|
||||
// The guarantee here is that solve returns at all.
|
||||
xSolver->solve();
|
||||
|
||||
CPPUNIT_ASSERT_MESSAGE("A model with contradictory bounds is not solvable",
|
||||
!xSolver->getSuccess());
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(SwarmSolverTest);
|
||||
}
|
||||
|
||||
|
||||
@@ -365,7 +365,14 @@ void SwarmSolver::initializeVariables(std::vector<double>& rVariables, std::mt19
|
||||
for (size_t i = 0; i < noVariables; ++i)
|
||||
{
|
||||
Bound const& rBound = maBounds[i];
|
||||
if (mbInteger)
|
||||
if (rBound.lower >= rBound.upper)
|
||||
{
|
||||
// An empty or reversed range has nothing to draw from. The
|
||||
// random distributions are undefined when the lower bound is
|
||||
// not below the upper one, so take the lower bound directly.
|
||||
rVariables[i] = rBound.lower;
|
||||
}
|
||||
else if (mbInteger)
|
||||
{
|
||||
sal_Int64 intLower(rBound.lower);
|
||||
sal_Int64 intUpper(rBound.upper);
|
||||
@@ -389,6 +396,12 @@ void SwarmSolver::initializeVariables(std::vector<double>& rVariables, std::mt19
|
||||
double SwarmSolver::clampVariable(size_t nVarIndex, double fValue)
|
||||
{
|
||||
Bound const& rBound = maBounds[nVarIndex];
|
||||
|
||||
// An empty or reversed range has no room to clamp into. std::clamp with a
|
||||
// lower bound above the upper bound is undefined, so return the lower bound.
|
||||
if (rBound.lower >= rBound.upper)
|
||||
return mbInteger ? std::trunc(rBound.lower) : rBound.lower;
|
||||
|
||||
double fResult = std::clamp(fValue, rBound.lower, rBound.upper);
|
||||
|
||||
if (mbInteger)
|
||||
@@ -400,14 +413,24 @@ double SwarmSolver::clampVariable(size_t nVarIndex, double fValue)
|
||||
double SwarmSolver::boundVariable(size_t nVarIndex, double fValue)
|
||||
{
|
||||
Bound const& rBound = maBounds[nVarIndex];
|
||||
// double fResult = std::max(std::min(fValue, rBound.upper), rBound.lower);
|
||||
|
||||
double fResult = fValue;
|
||||
while (fResult < rBound.lower || fResult > rBound.upper)
|
||||
double const fRange = rBound.upper - rBound.lower;
|
||||
if (fRange <= 0.0)
|
||||
{
|
||||
if (fResult < rBound.lower)
|
||||
fResult = rBound.upper - (rBound.lower - fResult);
|
||||
if (fResult > rBound.upper)
|
||||
fResult = (fResult - rBound.upper) + rBound.lower;
|
||||
// An empty range, where the lower and upper bound are equal, or a
|
||||
// reversed range from contradictory constraints, holds a single
|
||||
// value. Return the lower bound.
|
||||
fResult = rBound.lower;
|
||||
}
|
||||
else if (fResult < rBound.lower || fResult > rBound.upper)
|
||||
{
|
||||
// Wrap an out of range value back into the range, keeping its offset
|
||||
// from the lower bound.
|
||||
double fOffset = std::fmod(fResult - rBound.lower, fRange);
|
||||
if (fOffset < 0.0)
|
||||
fOffset += fRange;
|
||||
fResult = rBound.lower + fOffset;
|
||||
}
|
||||
|
||||
if (mbInteger)
|
||||
|
||||
Reference in New Issue
Block a user