From 0b054a016aed1051fe8e6593e5f467ea093b95a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Vajngerl?= Date: Wed, 3 Jun 2026 00:19:39 +0900 Subject: [PATCH] SwarmSolver: do not report success for an infeasible model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Change-Id: Iad8056045dc198d184c3858e1167f484f1e55512 Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/4048 Reviewed-by: Miklos Vajna Tested-by: Tomaž Vajngerl (cherry picked from commit c7f7aafb35a9bedd6b70883da90441c26f457a3e) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/208433 Tested-by: Jenkins Reviewed-by: Xisco Fauli --- sc/qa/unit/SwarmSolverTest.cxx | 98 ++++++++++++++++++++++++++++++++ sc/source/solver/SwarmSolver.cxx | 39 ++++++++++++- 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/sc/qa/unit/SwarmSolverTest.cxx b/sc/qa/unit/SwarmSolverTest.cxx index a8688a3846b4..82de0137f9df 100644 --- a/sc/qa/unit/SwarmSolverTest.cxx +++ b/sc/qa/unit/SwarmSolverTest.cxx @@ -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 xDocument(mxComponent, uno::UNO_QUERY_THROW); + + uno::Reference xSolver; + xSolver.set(m_xContext->getServiceManager()->createInstanceWithContext( + u"com.sun.star.comp.Calc.SwarmSolver"_ustr, m_xContext), + uno::UNO_QUERY_THROW); + + uno::Reference xPropSet(xSolver, uno::UNO_QUERY_THROW); + xPropSet->setPropertyValue(u"Algorithm"_ustr, uno::Any(sal_Int32(0))); + + table::CellAddress aObjective(0, 1, 1); + uno::Sequence aVariables{ { 0, 1, 0 } }; + + uno::Sequence 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 xDocument(mxComponent, uno::UNO_QUERY_THROW); + + uno::Reference xSolver; + xSolver.set(m_xContext->getServiceManager()->createInstanceWithContext( + u"com.sun.star.comp.Calc.SwarmSolver"_ustr, m_xContext), + uno::UNO_QUERY_THROW); + + uno::Reference xPropSet(xSolver, uno::UNO_QUERY_THROW); + xPropSet->setPropertyValue(u"Algorithm"_ustr, uno::Any(sal_Int32(1))); + + table::CellAddress aObjective(0, 1, 1); + uno::Sequence aVariables{ { 0, 1, 0 } }; + + uno::Sequence 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); } diff --git a/sc/source/solver/SwarmSolver.cxx b/sc/source/solver/SwarmSolver.cxx index b501f967da75..7ec6d9ce7d42 100644 --- a/sc/source/solver/SwarmSolver.cxx +++ b/sc/source/solver/SwarmSolver.cxx @@ -272,6 +272,7 @@ public: private: void applyVariables(std::vector const& rVariables); bool doesViolateConstraints(); + bool isSolutionFeasible(std::vector const& rSolution); public: double calculateFitness(std::vector const& rVariables); @@ -463,6 +464,25 @@ bool SwarmSolver::doesViolateConstraints() return false; } +bool SwarmSolver::isSolutionFeasible(std::vector 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 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*