SwarmSolver: do not report success for an infeasible model

Check the returned values before setting mbSuccess. They must cover
every variable, stay within each variable's bound, and satisfy the
remaining constraints. Otherwise report failure and set the status to
the infeasible-model message.

Add a test covering both algorithms.

Signed-off-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.co.uk>
Change-Id: Iad8056045dc198d184c3858e1167f484f1e55512
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/4048
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.com>
(cherry picked from commit c7f7aafb35a9bedd6b70883da90441c26f457a3e)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/208433
Tested-by: Jenkins
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
This commit is contained in:
Tomaž Vajngerl
2026-07-22 14:29:56 +02:00
committed by Xisco Fauli
parent 35a685a006
commit 0b054a016a
2 changed files with 134 additions and 3 deletions
+98
View File
@@ -27,6 +27,8 @@ class SwarmSolverTest : public UnoApiTest
void testVariableConstrained();
void testTwoVariables();
void testMultipleVariables();
void testInfeasibleConstraints_Differential_Evolution();
void testInfeasibleConstraints_Particle_Swarm();
public:
SwarmSolverTest()
@@ -40,6 +42,8 @@ public:
CPPUNIT_TEST(testVariableConstrained);
CPPUNIT_TEST(testMultipleVariables);
CPPUNIT_TEST(testTwoVariables);
CPPUNIT_TEST(testInfeasibleConstraints_Differential_Evolution);
CPPUNIT_TEST(testInfeasibleConstraints_Particle_Swarm);
CPPUNIT_TEST_SUITE_END();
};
@@ -317,6 +321,100 @@ void SwarmSolverTest::testMultipleVariables()
#endif
}
void SwarmSolverTest::testInfeasibleConstraints_Differential_Evolution()
{
// Regression: the solver used to report success even when no point satisfies
// the model.
// Differential Evolution returns no candidate at all
// result is infeasible and getSuccess must be false.
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);
uno::Reference<beans::XPropertySet> xPropSet(xSolver, uno::UNO_QUERY_THROW);
xPropSet->setPropertyValue(u"Algorithm"_ustr, uno::Any(sal_Int32(0)));
table::CellAddress aObjective(0, 1, 1);
uno::Sequence<table::CellAddress> aVariables{ { 0, 1, 0 } };
uno::Sequence<sheet::SolverConstraint> aConstraints{
{ /* [0] Left */ table::CellAddress(0, 1, 0),
/* Operator */ sheet::SolverConstraintOperator_GREATER_EQUAL,
/* Right */ uno::Any(-100.0) },
{ /* [1] Left */ table::CellAddress(0, 1, 0),
/* Operator */ sheet::SolverConstraintOperator_LESS_EQUAL,
/* Right */ uno::Any(100.0) },
// The objective B2 is 10*B1^2 - 60*B1 - 40, whose lowest value is
// -130 at B1 = 3, so forcing it below -1000 can never be met.
{ /* [2] Left */ table::CellAddress(0, 1, 1),
/* Operator */ sheet::SolverConstraintOperator_LESS_EQUAL,
/* Right */ uno::Any(-1000.0) }
};
xSolver->setDocument(xDocument);
xSolver->setObjective(aObjective);
xSolver->setVariables(aVariables);
xSolver->setConstraints(aConstraints);
xSolver->setMaximize(false);
xSolver->solve();
CPPUNIT_ASSERT_MESSAGE("Infeasible model must not report success", !xSolver->getSuccess());
}
void SwarmSolverTest::testInfeasibleConstraints_Particle_Swarm()
{
// Regression: the solver used to report success even when no point satisfies
// the model.
// Particle Swarm Optimization returns its starting point.
// result is infeasible and getSuccess must be false.
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);
uno::Reference<beans::XPropertySet> xPropSet(xSolver, uno::UNO_QUERY_THROW);
xPropSet->setPropertyValue(u"Algorithm"_ustr, uno::Any(sal_Int32(1)));
table::CellAddress aObjective(0, 1, 1);
uno::Sequence<table::CellAddress> aVariables{ { 0, 1, 0 } };
uno::Sequence<sheet::SolverConstraint> aConstraints{
{ /* [0] Left */ table::CellAddress(0, 1, 0),
/* Operator */ sheet::SolverConstraintOperator_GREATER_EQUAL,
/* Right */ uno::Any(-100.0) },
{ /* [1] Left */ table::CellAddress(0, 1, 0),
/* Operator */ sheet::SolverConstraintOperator_LESS_EQUAL,
/* Right */ uno::Any(100.0) },
// The objective B2 is 10*B1^2 - 60*B1 - 40, whose lowest value is
// -130 at B1 = 3, so forcing it below -1000 can never be met.
{ /* [2] Left */ table::CellAddress(0, 1, 1),
/* Operator */ sheet::SolverConstraintOperator_LESS_EQUAL,
/* Right */ uno::Any(-1000.0) }
};
xSolver->setDocument(xDocument);
xSolver->setObjective(aObjective);
xSolver->setVariables(aVariables);
xSolver->setConstraints(aConstraints);
xSolver->setMaximize(false);
xSolver->solve();
CPPUNIT_ASSERT_MESSAGE("Infeasible model must not report success", !xSolver->getSuccess());
}
CPPUNIT_TEST_SUITE_REGISTRATION(SwarmSolverTest);
}
+36 -3
View File
@@ -272,6 +272,7 @@ public:
private:
void applyVariables(std::vector<double> const& rVariables);
bool doesViolateConstraints();
bool isSolutionFeasible(std::vector<double> const& rSolution);
public:
double calculateFitness(std::vector<double> const& rVariables);
@@ -463,6 +464,25 @@ bool SwarmSolver::doesViolateConstraints()
return false;
}
bool SwarmSolver::isSolutionFeasible(std::vector<double> const& rSolution)
{
// The search must have produced a value for every variable.
if (sal_Int32(rSolution.size()) != maVariables.getLength())
return false;
// A value outside the variable's bound is not a valid solution.
for (size_t i = 0; i < rSolution.size(); ++i)
{
Bound const& rBound = maBounds[i];
if (rSolution[i] < rBound.lower || rSolution[i] > rBound.upper)
return false;
}
// Put the candidate into the document and check the remaining constraints.
applyVariables(rSolution);
return !doesViolateConstraints();
}
namespace
{
template <typename SwarmAlgorithm> class SwarmRunner
@@ -588,12 +608,25 @@ void SAL_CALL SwarmSolver::solve()
aSolution = aSwarmSolver.solve();
}
// Only report success when the returned values actually satisfy the
// model. The search can run out of generations or time without ever
// reaching a feasible point.
bool bFeasible = isSolutionFeasible(aSolution);
xModel->unlockControllers();
mbSuccess = true;
mbSuccess = bFeasible;
maSolution.realloc(aSolution.size());
std::copy(aSolution.begin(), aSolution.end(), maSolution.getArray());
if (bFeasible)
{
maSolution.realloc(aSolution.size());
std::copy(aSolution.begin(), aSolution.end(), maSolution.getArray());
}
else
{
maSolution.realloc(0);
maStatus = getResourceString(RID_ERROR_INFEASIBLE);
}
}
extern "C" SAL_DLLPUBLIC_EXPORT uno::XInterface*