diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 2de97eba04..ced1a21177 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -127,7 +127,7 @@ jobs: if: ${{ env.IS_PUSH == 'true' }} run: | cd _build - ctest -C Release --output-on-failure -R unfeasible + ctest -C Release --output-on-failure -R "^unfeasible$" - name: Run unit and end-to-end tests if: ${{ env.IS_PUSH == 'true' }} diff --git a/.github/workflows/windows-vcpkg.yml b/.github/workflows/windows-vcpkg.yml index 70bc233220..a4dc8eac5a 100644 --- a/.github/workflows/windows-vcpkg.yml +++ b/.github/workflows/windows-vcpkg.yml @@ -125,7 +125,7 @@ jobs: - name: Run unfeasibility-related tests run: | cd _build - ctest -C Release --output-on-failure -R unfeasible + ctest -C Release --output-on-failure -R "^unfeasible$" - name: Run unit and end-to-end tests run: | diff --git a/src/solver/infeasible-problem-analysis/CMakeLists.txt b/src/solver/infeasible-problem-analysis/CMakeLists.txt index 97864d3ef8..ca08ef42da 100644 --- a/src/solver/infeasible-problem-analysis/CMakeLists.txt +++ b/src/solver/infeasible-problem-analysis/CMakeLists.txt @@ -1,14 +1,17 @@ project(infeasible-problem-analysis) set(SRC_INFEASIBLE_PROBLEM_ANALYSIS - problem.cpp - problem.h + unfeasibility-analysis.h + constraint-slack-analysis.h + constraint-slack-analysis.cpp + variables-bounds-consistency.h + variables-bounds-consistency.cpp + unfeasible-pb-analyzer.cpp + unfeasible-pb-analyzer.h report.h report.cpp constraint.h constraint.cpp - exceptions.h - exceptions.cpp ) add_library(infeasible_problem_analysis ${SRC_INFEASIBLE_PROBLEM_ANALYSIS}) @@ -16,3 +19,7 @@ target_link_libraries(infeasible_problem_analysis PUBLIC ortools::ortools sirius_solver utils #ortools-utils, not Antares::utils ) +target_include_directories(infeasible_problem_analysis + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) diff --git a/src/solver/infeasible-problem-analysis/constraint-slack-analysis.cpp b/src/solver/infeasible-problem-analysis/constraint-slack-analysis.cpp new file mode 100644 index 0000000000..36667da64a --- /dev/null +++ b/src/solver/infeasible-problem-analysis/constraint-slack-analysis.cpp @@ -0,0 +1,85 @@ +#include +#include "constraint-slack-analysis.h" +#include +#include "report.h" + +using namespace operations_research; + +namespace Antares::Optimization +{ + +void ConstraintSlackAnalysis::run(MPSolver* problem) +{ + addSlackVariables(problem); + if (slackVariables_.empty()) + { + logs.error() << title() << " : no constraints have been selected"; + return; + } + + buildObjective(problem); + + const MPSolver::ResultStatus status = problem->Solve(); + if ((status != MPSolver::OPTIMAL) && (status != MPSolver::FEASIBLE)) + { + logs.error() << title() << " : modified linear problem could not be solved"; + return; + } + + hasDetectedInfeasibilityCause_ = true; +} + +void ConstraintSlackAnalysis::addSlackVariables(MPSolver* problem) +{ + /* Optimization: + We assess that less than 1 every 3 constraint will match + the regex. If more, push_back may force the copy of memory blocks. + This should not happen in most cases. + */ + const unsigned int selectedConstraintsInverseRatio = 3; + slackVariables_.reserve(problem->NumConstraints() / selectedConstraintsInverseRatio); + std::regex rgx(constraint_name_pattern); + const double infinity = MPSolver::infinity(); + for (MPConstraint* constraint : problem->constraints()) + { + if (std::regex_search(constraint->name(), rgx)) + { + if (constraint->lb() != -infinity) + { + const MPVariable* slack + = problem->MakeNumVar(0, infinity, constraint->name() + "::low"); + constraint->SetCoefficient(slack, 1.); + slackVariables_.push_back(slack); + } + + if (constraint->ub() != infinity) + { + const MPVariable* slack + = problem->MakeNumVar(0, infinity, constraint->name() + "::up"); + constraint->SetCoefficient(slack, -1.); + slackVariables_.push_back(slack); + } + } + } +} + +void ConstraintSlackAnalysis::buildObjective(MPSolver* problem) const +{ + MPObjective* objective = problem->MutableObjective(); + // Reset objective function + objective->Clear(); + // Only slack variables have a non-zero cost + for (const MPVariable* slack : slackVariables_) + { + objective->SetCoefficient(slack, 1.); + } + objective->SetMinimization(); +} + +void ConstraintSlackAnalysis::printReport() const +{ + InfeasibleProblemReport report(slackVariables_); + report.prettyPrint(); +} + +} // namespace Antares::Optimization \ No newline at end of file diff --git a/src/solver/infeasible-problem-analysis/constraint-slack-analysis.h b/src/solver/infeasible-problem-analysis/constraint-slack-analysis.h new file mode 100644 index 0000000000..7fc54160c7 --- /dev/null +++ b/src/solver/infeasible-problem-analysis/constraint-slack-analysis.h @@ -0,0 +1,30 @@ +#pragma once + +#include "unfeasibility-analysis.h" + +namespace Antares::Optimization +{ + +/*! + * That particular analysis relaxes all constraints by + * adding slack variables for each one. + */ +class ConstraintSlackAnalysis : public UnfeasibilityAnalysis +{ +public: + ConstraintSlackAnalysis() = default; + ~ConstraintSlackAnalysis() override = default; + + void run(operations_research::MPSolver* problem) override; + void printReport() const override; + std::string title() const override { return "Slack variables analysis"; } + +private: + void buildObjective(operations_research::MPSolver* problem) const; + void addSlackVariables(operations_research::MPSolver* problem); + + std::vector slackVariables_; + const std::string constraint_name_pattern = "^AreaHydroLevel::|::hourly::|::daily::|::weekly::|^FictiveLoads::"; +}; + +} // namespace Antares::Optimization diff --git a/src/solver/infeasible-problem-analysis/exceptions.cpp b/src/solver/infeasible-problem-analysis/exceptions.cpp deleted file mode 100644 index 1f225fe486..0000000000 --- a/src/solver/infeasible-problem-analysis/exceptions.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "exceptions.h" - -/* This file contains exceptions that can be thrown by the - infeasible problem analyser. -*/ - -namespace Antares -{ -namespace Optimization -{ -SlackVariablesEmpty::SlackVariablesEmpty(const std::string& s) : std::runtime_error(s) -{ -} -ProblemResolutionFailed::ProblemResolutionFailed(const std::string& s) : std::runtime_error(s) -{ -} -} // namespace Optimization -} // namespace Antares diff --git a/src/solver/infeasible-problem-analysis/exceptions.h b/src/solver/infeasible-problem-analysis/exceptions.h deleted file mode 100644 index 5b88b77c06..0000000000 --- a/src/solver/infeasible-problem-analysis/exceptions.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include - -namespace Antares -{ -namespace Optimization -{ -class SlackVariablesEmpty : public std::runtime_error -{ -public: - explicit SlackVariablesEmpty(const std::string& message); -}; -class ProblemResolutionFailed : public std::runtime_error -{ -public: - explicit ProblemResolutionFailed(const std::string& message); -}; -} // namespace Optimization -} // namespace Antares diff --git a/src/solver/infeasible-problem-analysis/problem.cpp b/src/solver/infeasible-problem-analysis/problem.cpp deleted file mode 100644 index c6d399b568..0000000000 --- a/src/solver/infeasible-problem-analysis/problem.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include "problem.h" -#include "exceptions.h" - -#include -#include -#include - -using namespace operations_research; - -namespace Antares -{ -namespace Optimization -{ -InfeasibleProblemAnalysis::InfeasibleProblemAnalysis(const std::string& solverName, const PROBLEME_SIMPLEXE_NOMME* ProbSpx) -{ - mSolver - = std::unique_ptr(ProblemSimplexeNommeConverter(solverName, ProbSpx).Convert()); -} - -void InfeasibleProblemAnalysis::addSlackVariables() -{ - /* Optimization: - We assess that less than 1 every 3 constraint will match - the regex. If more, push_back may force the copy of memory blocks. - This should not happen in most cases. - */ - const unsigned int selectedConstraintsInverseRatio = 3; - mSlackVariables.reserve(mSolver->NumConstraints() / selectedConstraintsInverseRatio); - std::regex rgx(constraint_name_pattern); - const double infinity = MPSolver::infinity(); - for (MPConstraint* constraint : mSolver->constraints()) - { - if (std::regex_search(constraint->name(), rgx)) - { - if (constraint->lb() != -infinity) - { - const MPVariable* slack - = mSolver->MakeNumVar(0, infinity, constraint->name() + "::low"); - constraint->SetCoefficient(slack, 1.); - mSlackVariables.push_back(slack); - } - - if (constraint->ub() != infinity) - { - const MPVariable* slack - = mSolver->MakeNumVar(0, infinity, constraint->name() + "::up"); - constraint->SetCoefficient(slack, -1.); - mSlackVariables.push_back(slack); - } - } - } -} - -void InfeasibleProblemAnalysis::buildObjective() const -{ - MPObjective* objective = mSolver->MutableObjective(); - // Reset objective function - objective->Clear(); - // Only slack variables have a non-zero cost - for (const MPVariable* slack : mSlackVariables) - { - objective->SetCoefficient(slack, 1.); - } - objective->SetMinimization(); -} - -MPSolver::ResultStatus InfeasibleProblemAnalysis::Solve() const -{ - return mSolver->Solve(); -} - -InfeasibleProblemReport InfeasibleProblemAnalysis::produceReport() -{ - addSlackVariables(); - if (mSlackVariables.empty()) - { - throw SlackVariablesEmpty( - "Cannot generate infeasibility report: no constraints have been selected"); - } - buildObjective(); - const MPSolver::ResultStatus status = Solve(); - if ((status != MPSolver::OPTIMAL) && (status != MPSolver::FEASIBLE)) - { - throw ProblemResolutionFailed( - "Linear problem could not be solved, and infeasibility analysis could not help"); - } - return InfeasibleProblemReport(mSlackVariables); -} -} // namespace Optimization -} // namespace Antares diff --git a/src/solver/infeasible-problem-analysis/problem.h b/src/solver/infeasible-problem-analysis/problem.h deleted file mode 100644 index d0e277492a..0000000000 --- a/src/solver/infeasible-problem-analysis/problem.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "../utils/ortools_utils.h" -#include "report.h" - -namespace Antares -{ -namespace Optimization -{ -class InfeasibleProblemAnalysis -{ -public: - InfeasibleProblemAnalysis() = delete; - explicit InfeasibleProblemAnalysis(const std::string& solverName, const PROBLEME_SIMPLEXE_NOMME* ProbSpx); - InfeasibleProblemReport produceReport(); - -private: - void buildObjective() const; - void addSlackVariables(); - operations_research::MPSolver::ResultStatus Solve() const; - - std::unique_ptr mSolver; - std::vector mSlackVariables; - const std::string constraint_name_pattern = "^AreaHydroLevel::|::hourly::|::daily::|::weekly::|^FictiveLoads::"; - -}; -} // namespace Optimization -} // namespace Antares diff --git a/src/solver/infeasible-problem-analysis/report.cpp b/src/solver/infeasible-problem-analysis/report.cpp index 96719116a7..05338687da 100644 --- a/src/solver/infeasible-problem-analysis/report.cpp +++ b/src/solver/infeasible-problem-analysis/report.cpp @@ -3,29 +3,42 @@ #include #include +using namespace operations_research; + static bool compareSlackSolutions(const Antares::Optimization::Constraint& a, const Antares::Optimization::Constraint& b) { return a.getSlackValue() > b.getSlackValue(); } -namespace Antares +namespace Antares::Optimization { -namespace Optimization +InfeasibleProblemReport::InfeasibleProblemReport(const std::vector& slackVariables) { -InfeasibleProblemReport::InfeasibleProblemReport( - const std::vector& slackVariables) + turnSlackVarsIntoConstraints(slackVariables); + sortConstraints(); + trimConstraints(); +} + +void InfeasibleProblemReport::turnSlackVarsIntoConstraints(const std::vector& slackVariables) { - for (const operations_research::MPVariable* slack : slackVariables) + for (const MPVariable* slack : slackVariables) { - append(slack->name(), slack->solution_value()); + mConstraints.emplace_back(slack->name(), slack->solution_value()); } - trim(); } -void InfeasibleProblemReport::append(const std::string& constraintName, double value) +void InfeasibleProblemReport::sortConstraints() { - mConstraints.emplace_back(constraintName, value); + std::sort(std::begin(mConstraints), std::end(mConstraints), ::compareSlackSolutions); +} + +void InfeasibleProblemReport::trimConstraints() +{ + if (nbVariables <= mConstraints.size()) + { + mConstraints.resize(nbVariables); + } } void InfeasibleProblemReport::extractItems() @@ -74,14 +87,4 @@ void InfeasibleProblemReport::prettyPrint() logSuspiciousConstraints(); } -void InfeasibleProblemReport::trim() -{ - std::sort(std::begin(mConstraints), std::end(mConstraints), ::compareSlackSolutions); - if (nbVariables <= mConstraints.size()) - { - mConstraints.resize(nbVariables); - } -} - -} // namespace Optimization -} // namespace Antares +} // namespace Antares::Optimization \ No newline at end of file diff --git a/src/solver/infeasible-problem-analysis/report.h b/src/solver/infeasible-problem-analysis/report.h index db15744f19..368950ceb0 100644 --- a/src/solver/infeasible-problem-analysis/report.h +++ b/src/solver/infeasible-problem-analysis/report.h @@ -15,15 +15,16 @@ class InfeasibleProblemReport { public: InfeasibleProblemReport() = delete; - explicit InfeasibleProblemReport( - const std::vector& slackVariables); + explicit InfeasibleProblemReport(const std::vector& slackVariables); void prettyPrint(); private: - void trim(); + void turnSlackVarsIntoConstraints(const std::vector& slackVariables); + void sortConstraints(); + void trimConstraints(); void extractItems(); void logSuspiciousConstraints(); - void append(const std::string& constraintName, double value); + std::vector mConstraints; std::map mTypes; const unsigned int nbVariables = 10; diff --git a/src/solver/infeasible-problem-analysis/unfeasibility-analysis.h b/src/solver/infeasible-problem-analysis/unfeasibility-analysis.h new file mode 100644 index 0000000000..30f36354f2 --- /dev/null +++ b/src/solver/infeasible-problem-analysis/unfeasibility-analysis.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include "ortools/linear_solver/linear_solver.h" + +namespace Antares::Optimization +{ + +/*! + * Interface for all elementary analysis. + */ +class UnfeasibilityAnalysis +{ +public: + UnfeasibilityAnalysis() = default; + virtual ~UnfeasibilityAnalysis() = default; + + virtual void run(operations_research::MPSolver* problem) = 0; + virtual void printReport() const = 0; + virtual std::string title() const = 0; + bool hasDetectedInfeasibilityCause() const { return hasDetectedInfeasibilityCause_; } + +protected: + bool hasDetectedInfeasibilityCause_ = false; +}; + +} diff --git a/src/solver/infeasible-problem-analysis/unfeasible-pb-analyzer.cpp b/src/solver/infeasible-problem-analysis/unfeasible-pb-analyzer.cpp new file mode 100644 index 0000000000..a4e59a3686 --- /dev/null +++ b/src/solver/infeasible-problem-analysis/unfeasible-pb-analyzer.cpp @@ -0,0 +1,58 @@ +#include +#include + +#include "unfeasible-pb-analyzer.h" +#include "variables-bounds-consistency.h" +#include "constraint-slack-analysis.h" +#include + + +using namespace operations_research; + +namespace Antares::Optimization +{ + +std::unique_ptr makeUnfeasiblePbAnalyzer() +{ + std::vector> analysisList; + analysisList.push_back(std::make_unique()); + analysisList.push_back(std::make_unique()); + + return std::make_unique(std::move(analysisList)); +} + +UnfeasiblePbAnalyzer::UnfeasiblePbAnalyzer(std::vector> analysisList) + : analysisList_(std::move(analysisList)) +{} + +void UnfeasiblePbAnalyzer::run(MPSolver* problem) +{ + logs.info(); + logs.info() << "Solver: Starting unfeasibility analysis..."; + + for (auto& analysis : analysisList_) + { + logs.info(); + logs.info() << analysis->title() << " : running..."; + analysis->run(problem); + if (analysis->hasDetectedInfeasibilityCause()) + return; + + logs.notice() << analysis->title() << " : nothing detected."; + } +} + +void UnfeasiblePbAnalyzer::printReport() const +{ + for (auto& analysis : analysisList_) + { + if (analysis->hasDetectedInfeasibilityCause()) + { + logs.info() << analysis->title() << " : printing report"; + analysis->printReport(); + return; + } + } + logs.notice() << "Solver: unfeasibility analysis : could not find the cause of unfeasibility."; +} +} // namespace Antares::Optimization \ No newline at end of file diff --git a/src/solver/infeasible-problem-analysis/unfeasible-pb-analyzer.h b/src/solver/infeasible-problem-analysis/unfeasible-pb-analyzer.h new file mode 100644 index 0000000000..c64a099ac7 --- /dev/null +++ b/src/solver/infeasible-problem-analysis/unfeasible-pb-analyzer.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +#include "../utils/ortools_utils.h" +#include "unfeasibility-analysis.h" + +namespace Antares::Optimization +{ + +/*! + * In charge of anayzing the possible reasons for the unfeasibility of an optimization problem. + * + * The analyzer relies on the execution of possibly multiple UnfeasibilityAnalysis. + */ +class UnfeasiblePbAnalyzer +{ +public: + UnfeasiblePbAnalyzer() = delete; + explicit UnfeasiblePbAnalyzer(std::vector> analysisList); + void run(MPSolver* problem); + void printReport() const; + +private: + std::vector> analysisList_; +}; + +std::unique_ptr makeUnfeasiblePbAnalyzer(); + +} // namespace Antares::Optimization \ No newline at end of file diff --git a/src/solver/infeasible-problem-analysis/variables-bounds-consistency.cpp b/src/solver/infeasible-problem-analysis/variables-bounds-consistency.cpp new file mode 100644 index 0000000000..e1c28f5dc9 --- /dev/null +++ b/src/solver/infeasible-problem-analysis/variables-bounds-consistency.cpp @@ -0,0 +1,43 @@ +#include "variables-bounds-consistency.h" +#include + +using namespace operations_research; + +namespace Antares::Optimization +{ + +void VariablesBoundsConsistency::run(MPSolver* problem) +{ + for (const auto& var : problem->variables()) + { + double lowBound = var->lb(); + double upBound = var->ub(); + std::string name = var->name(); + if (lowBound > upBound) + { + storeIncorrectVariable(name, lowBound, upBound); + } + } + + if (foundIncorrectVariables()) + hasDetectedInfeasibilityCause_ = true; +} + +void VariablesBoundsConsistency::storeIncorrectVariable(std::string name, double lowBound, double upBound) +{ + incorrectVars_.push_back(VariableBounds(name, lowBound, upBound)); +} + +bool VariablesBoundsConsistency::foundIncorrectVariables() +{ + return !incorrectVars_.empty(); +} + +void VariablesBoundsConsistency::printReport() const +{ + for (const auto& var : incorrectVars_) + { + logs.notice() << var.name << " : low bound = " << var.lowBound << ", up bound = " << var.upBound; + } +} +} // namespace Antares::Optimization \ No newline at end of file diff --git a/src/solver/infeasible-problem-analysis/variables-bounds-consistency.h b/src/solver/infeasible-problem-analysis/variables-bounds-consistency.h new file mode 100644 index 0000000000..2282f77681 --- /dev/null +++ b/src/solver/infeasible-problem-analysis/variables-bounds-consistency.h @@ -0,0 +1,41 @@ +#pragma once + +#include "unfeasibility-analysis.h" + +namespace Antares::Optimization +{ + +struct VariableBounds +{ + VariableBounds(const std::string& var_name, double low_bound, double up_bound) + : name(var_name), lowBound(low_bound), upBound(up_bound) + {} + + std::string name; + double lowBound; + double upBound; +}; + +/*! + * That particular analysis simply checks that all variables + * are within their minimum and maximum bounds. + */ +class VariablesBoundsConsistency : public UnfeasibilityAnalysis +{ +public: + VariablesBoundsConsistency() = default; + ~VariablesBoundsConsistency() override = default; + + void run(operations_research::MPSolver* problem) override; + void printReport() const override; + std::string title() const override { return "Variables bounds consistency check"; } + + const std::vector& incorrectVars() const { return incorrectVars_; } + +private: + void storeIncorrectVariable(std::string name, double lowBound, double upBound); + bool foundIncorrectVariables(); + + std::vector incorrectVars_; +}; +} diff --git a/src/solver/optimisation/opt_appel_solveur_lineaire.cpp b/src/solver/optimisation/opt_appel_solveur_lineaire.cpp index 060784fe36..bcbda69832 100644 --- a/src/solver/optimisation/opt_appel_solveur_lineaire.cpp +++ b/src/solver/optimisation/opt_appel_solveur_lineaire.cpp @@ -46,8 +46,9 @@ extern "C" #include "../utils/mps_utils.h" #include "../utils/filename.h" -#include "../infeasible-problem-analysis/problem.h" -#include "../infeasible-problem-analysis/exceptions.h" +#include "../infeasible-problem-analysis/unfeasible-pb-analyzer.h" +#include "../infeasible-problem-analysis/variables-bounds-consistency.h" +#include "../infeasible-problem-analysis/constraint-slack-analysis.h" #include @@ -375,21 +376,12 @@ bool OPT_AppelDuSimplexe(const OptimizationOptions& options, } Probleme.SetUseNamedProblems(true); - Optimization::InfeasibleProblemAnalysis analysis(options.solverName, &Probleme); - logs.notice() << " Solver: Starting infeasibility analysis..."; - try - { - Optimization::InfeasibleProblemReport report = analysis.produceReport(); - report.prettyPrint(); - } - catch (const Optimization::SlackVariablesEmpty& ex) - { - logs.error() << ex.what(); - } - catch (const Optimization::ProblemResolutionFailed& ex) - { - logs.error() << ex.what(); - } + + auto MPproblem = std::shared_ptr(ProblemSimplexeNommeConverter(options.solverName, &Probleme).Convert()); + + auto analyzer = makeUnfeasiblePbAnalyzer(); + analyzer->run(MPproblem.get()); + analyzer->printReport(); auto mps_writer_on_error = simplexResult.mps_writer_factory.createOnOptimizationError(); const std::string filename = createMPSfilename(optPeriodStringGenerator, optimizationNumber); diff --git a/src/solver/utils/ortools_utils.cpp b/src/solver/utils/ortools_utils.cpp index cbe93d1971..6d3911f76c 100644 --- a/src/solver/utils/ortools_utils.cpp +++ b/src/solver/utils/ortools_utils.cpp @@ -32,9 +32,9 @@ namespace Antares namespace Optimization { ProblemSimplexeNommeConverter::ProblemSimplexeNommeConverter( - const std::string& solverName, - const Antares::Optimization::PROBLEME_SIMPLEXE_NOMME* problemeSimplexe) : - solverName_(solverName), problemeSimplexe_(problemeSimplexe) + const std::string& solverName, + const Antares::Optimization::PROBLEME_SIMPLEXE_NOMME* problemeSimplexe) + : solverName_(solverName), problemeSimplexe_(problemeSimplexe) { if (problemeSimplexe_->UseNamedProblems()) { diff --git a/src/tests/src/solver/CMakeLists.txt b/src/tests/src/solver/CMakeLists.txt index cd2c028bf1..5d8de7047c 100644 --- a/src/tests/src/solver/CMakeLists.txt +++ b/src/tests/src/solver/CMakeLists.txt @@ -1,3 +1,4 @@ add_subdirectory(simulation) add_subdirectory(optimisation) +add_subdirectory(infeasible-problem-analysis) diff --git a/src/tests/src/solver/infeasible-problem-analysis/CMakeLists.txt b/src/tests/src/solver/infeasible-problem-analysis/CMakeLists.txt new file mode 100644 index 0000000000..77a07ef0ab --- /dev/null +++ b/src/tests/src/solver/infeasible-problem-analysis/CMakeLists.txt @@ -0,0 +1,19 @@ +add_executable(test-unfeasible-problem-analyzer) +target_sources(test-unfeasible-problem-analyzer + PRIVATE + test-unfeasible-problem-analyzer.cpp) +target_link_libraries(test-unfeasible-problem-analyzer + PRIVATE + Boost::unit_test_framework + infeasible_problem_analysis +) + +# TODO: this is necessary so that windows can find the DLL without running "cmake --install" +# Is there a better way to achieve this ? +copy_dependency(sirius_solver test-unfeasible-problem-analyzer) + +add_test(NAME test-unfeasible-problem-analyzer COMMAND test-unfeasible-problem-analyzer) + +# Storing the executable under the folder Unit-tests in Visual Studio +set_target_properties(test-unfeasible-problem-analyzer PROPERTIES FOLDER Unit-tests) +set_property(TEST test-unfeasible-problem-analyzer PROPERTY LABELS unit) diff --git a/src/tests/src/solver/infeasible-problem-analysis/test-unfeasible-problem-analyzer.cpp b/src/tests/src/solver/infeasible-problem-analysis/test-unfeasible-problem-analyzer.cpp new file mode 100644 index 0000000000..50c30983f7 --- /dev/null +++ b/src/tests/src/solver/infeasible-problem-analysis/test-unfeasible-problem-analyzer.cpp @@ -0,0 +1,220 @@ +/* +** Copyright 2007-2023 RTE +** Authors: Antares_Simulator Team +** +** This file is part of Antares_Simulator. +** +** Antares_Simulator is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** There are special exceptions to the terms and conditions of the +** license as they are applied to this software. View the full text of +** the exceptions in file COPYING.txt in the directory of this software +** distribution +** +** Antares_Simulator is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with Antares_Simulator. If not, see . +** +** SPDX-License-Identifier: licenceRef-GPL3_WITH_RTE-Exceptions +*/ +#define WIN32_LEAN_AND_MEAN +#define BOOST_TEST_MODULE unfeasible_problem_analyzer +#define BOOST_TEST_DYN_LINK + +#include +#include +#include + +#include "infeasible-problem-analysis/unfeasible-pb-analyzer.h" +#include "infeasible-problem-analysis/variables-bounds-consistency.h" +#include "infeasible-problem-analysis/constraint-slack-analysis.h" + +namespace bdata = boost::unit_test::data; + +using operations_research::MPSolver; + +using Antares::Optimization::VariablesBoundsConsistency; +using Antares::Optimization::VariableBounds; +using Antares::Optimization::ConstraintSlackAnalysis; +using Antares::Optimization::UnfeasiblePbAnalyzer; +using Antares::Optimization::UnfeasibilityAnalysis; + + +bool variableEquals(const VariableBounds& lhs, const VariableBounds& rhs) +{ + return lhs.name == rhs.name && + lhs.lowBound == rhs.lowBound && + lhs.upBound == rhs.upBound; +} + +/*! + * Analysis mock, used to assess which step has been run by the analyzer + */ +class AnalysisMock : public UnfeasibilityAnalysis +{ +public: + AnalysisMock(bool shouldDetectCause, bool& hasRun, bool& hasPrinted): + shouldDetectCause_(shouldDetectCause), + hasRun_(hasRun), + hasPrinted_(hasPrinted) + {} + + ~AnalysisMock() override = default; + + void run(operations_research::MPSolver *problem) override + { + hasRun_ = true; + hasDetectedInfeasibilityCause_ = shouldDetectCause_; + } + + void printReport() const override + { + hasPrinted_ = true; + } + + std::string title() const override + { + return "mock"; + } + +private: + bool shouldDetectCause_; + bool& hasRun_; + bool& hasPrinted_; +}; + + +BOOST_AUTO_TEST_SUITE(unfeasible_problem_analyzer) + +BOOST_AUTO_TEST_CASE(analyzer_should_call_analysis_and_print_detected_issues) +{ + bool hasRun1 = false; + bool hasPrinted1 = false; + bool hasRun2 = false; + bool hasPrinted2 = false; + std::vector> analysis; + analysis.push_back(std::make_unique(false, hasRun1, hasPrinted1)); + analysis.push_back(std::make_unique(true, hasRun2, hasPrinted2)); + std::unique_ptr problem(MPSolver::CreateSolver("GLOP")); + + UnfeasiblePbAnalyzer analyzer(std::move(analysis)); + BOOST_CHECK(!hasRun1); + BOOST_CHECK(!hasPrinted1); + BOOST_CHECK(!hasRun2); + BOOST_CHECK(!hasPrinted2); + + analyzer.run(problem.get()); + BOOST_CHECK(hasRun1); + BOOST_CHECK(!hasPrinted1); + BOOST_CHECK(hasRun2); + BOOST_CHECK(!hasPrinted2); + + // only failing analysis will print + analyzer.printReport(); + BOOST_CHECK(hasRun1); + BOOST_CHECK(!hasPrinted1); + BOOST_CHECK(hasRun2); + BOOST_CHECK(hasPrinted2); +} + +BOOST_AUTO_TEST_CASE(analysis_should_detect_inconsistent_variable_bounds) +{ + std::unique_ptr problem(MPSolver::CreateSolver("GLOP")); + problem->MakeNumVar(-1, 1, "ok-var"); + problem->MakeNumVar(1, -1, "not-ok-var"); + + VariablesBoundsConsistency analysis; + analysis.run(problem.get()); + auto incorrectVars = analysis.incorrectVars(); + BOOST_CHECK_EQUAL(incorrectVars.size(), 1); + + auto expected = VariableBounds("not-ok-var", 1, -1); + BOOST_CHECK(variableEquals(incorrectVars[0], expected)); +} + +/*! + * Creates a problem with 2 variables linked by 1 constraint: + * - Variable 1 must be greater than 1 + * - Variable 2 must be lesser than -1 + * - but if feasible is false, constraint enforces that variable 2 is greater than variable 1 --> infeasible + */ +std::unique_ptr createProblem(const std::string& constraintName, bool feasible) +{ + std::unique_ptr problem(MPSolver::CreateSolver("GLOP")); + const double infinity = problem->infinity(); + auto var1 = problem->MakeNumVar(1, infinity, "var1"); + auto var2 = problem->MakeNumVar(-infinity, -1, "var2"); + auto constraint = problem->MakeRowConstraint(constraintName); + constraint->SetBounds(0, infinity); + if (feasible) { + constraint->SetCoefficient(var1, 1); + constraint->SetCoefficient(var2, -1); + } else { + constraint->SetCoefficient(var1, -1); + constraint->SetCoefficient(var2, 1); + } + return problem; +} + +std::unique_ptr createFeasibleProblem(const std::string& constraintName) +{ + return createProblem(constraintName, true); +} + +std::unique_ptr createUnfeasibleProblem(const std::string& constraintName) +{ + return createProblem(constraintName, false); +} + +static const std::string validConstraintNames[] = +{ + "BC::hourly::hour<36>", + "BC::daily::day<67>", + "BC::weekly::week<12>", + "FictiveLoads::hour<25>", + "AreaHydroLevel::hour<8>", +}; + +BOOST_DATA_TEST_CASE(analysis_should_detect_unfeasible_constraint, + bdata::make(validConstraintNames), constraintName) +{ + std::unique_ptr unfeasibleProblem = createUnfeasibleProblem(constraintName); + BOOST_CHECK(unfeasibleProblem->Solve() == MPSolver::INFEASIBLE); + + ConstraintSlackAnalysis analysis; + analysis.run(unfeasibleProblem.get()); + BOOST_CHECK(analysis.hasDetectedInfeasibilityCause()); +} + +BOOST_AUTO_TEST_CASE(analysis_should_ignore_ill_named_constraint) +{ + std::unique_ptr unfeasibleProblem = createUnfeasibleProblem("ignored-name"); + BOOST_CHECK(unfeasibleProblem->Solve() == MPSolver::INFEASIBLE); + + ConstraintSlackAnalysis analysis; + analysis.run(unfeasibleProblem.get()); + BOOST_CHECK(!analysis.hasDetectedInfeasibilityCause()); +} + + +// TODO: this test should be improved by changing the API, the current interface does not allow +// to check that no constraint was identified... +BOOST_AUTO_TEST_CASE(analysis_should_ignore_feasible_constraints) +{ + std::unique_ptr feasibleProblem = createFeasibleProblem("BC::hourly::hour<36>"); + BOOST_CHECK(feasibleProblem->Solve() == MPSolver::OPTIMAL); + + ConstraintSlackAnalysis analysis; + analysis.run(feasibleProblem.get()); + BOOST_CHECK(analysis.hasDetectedInfeasibilityCause()); // Would expect false here instead? +} + +BOOST_AUTO_TEST_SUITE_END() +