diff --git a/docs/developer-guide/CHANGELOG.md b/docs/developer-guide/CHANGELOG.md index 2b650deb55..c6957116c9 100644 --- a/docs/developer-guide/CHANGELOG.md +++ b/docs/developer-guide/CHANGELOG.md @@ -12,6 +12,7 @@ toc_depth: 2 * Short term storage costs [ANT-1854] (#2302) * Add ts-generation for links [ANT-1084] (#1986) * Make it possible to specify the final hydro reservoir level [ANT-1084] (#1521) +* Add support for more QP solvers [ANT-2546] (#2574) #### Removed features * Remove hydro hotstart (#2131) @@ -69,7 +70,7 @@ toc_depth: 2 #### Build * vcpkg (linux, sirius) (#2078) (#2090) (#2145) * Remove src/antares-deps (#2182) -* Use OR-Tools v9.11-rte1.1 (#2437) +* Use OR-Tools v9.11-rte1.2 (#2574) * Fix or-tools integration (#2402) * Better dependencies with cmake, antares matrix (#2369) diff --git a/docs/user-guide/solver/02-command-line.md b/docs/user-guide/solver/02-command-line.md index 88defa9062..526f54b6f8 100644 --- a/docs/user-guide/solver/02-command-line.md +++ b/docs/user-guide/solver/02-command-line.md @@ -17,7 +17,8 @@ hide: | --adequacy | Force the simulation in [adequacy](static-modeler/04-parameters.md#mode) mode | | --parallel | Enable [parallel](optional-features/multi-threading.md) computation of MC years | | --force-parallel=VALUE | Override the max number of years computed [simultaneously](optional-features/multi-threading.md) | -| --solver=VALUE | The optimization solver to use. Possible values are: `sirius` (default), `coin`, `xpress`, `scip` | +| --linear-solver=VALUE | The optimization solver to use for linear problems. Possible values are: `sirius` (default), `coin`, `xpress`, `scip` | +| --quadratic-solver=VALUE | The optimization solver to use for quadratic problems. Possible values are: `sirius` (default), `pdlp` | ## Parameters @@ -43,7 +44,8 @@ hide: | -m, --mps-export | Export anonymous MPS, weekly or daily optimal UC+dispatch linear (MPS will be named if the problem is infeasible) | | -s, --named-mps-problems | Export named MPS, weekly or daily optimal UC+dispatch linear | | --solver-logs | Print solver logs | -| --solver-parameters | Set solver-specific parameters, for instance `--solver-parameters="THREADS 1 PRESOLVE 1"` for XPRESS or `--solver-parameters="parallel/maxnthreads 1, lp/presolving TRUE"` for SCIP. Syntax is solver-dependent, and only supported for SCIP & XPRESS. | +| --linear-solver-parameters | Set solver-specific parameters for linear problems, for instance `--solver-parameters="THREADS 1 PRESOLVE 1"` for XPRESS or `--solver-parameters="parallel/maxnthreads 1, lp/presolving TRUE"` for SCIP. Syntax is solver-dependent, and only supported for SCIP & XPRESS. | +| --quadratic-solver-parameters | Set solver-specific parameters for quadratic problems. | ## Misc. diff --git a/docs/user-guide/solver/static-modeler/04-parameters.md b/docs/user-guide/solver/static-modeler/04-parameters.md index a50f982135..8e11763544 100644 --- a/docs/user-guide/solver/static-modeler/04-parameters.md +++ b/docs/user-guide/solver/static-modeler/04-parameters.md @@ -483,14 +483,20 @@ _**This section is under construction**_ > _**Note:**_ You can find more information on this parameter [here](../03-appendix.md#details-on-the-include-unfeasible-problem-behavior-parameter). --- -#### solver-parameters -[//]: # (TODO: document this parameter) -_**This section is under construction**_ +#### linear-solver-parameters +- **Expected value:** a string +- **Required:** **no** +- **Default value:** empty +- **Usage:** Set solver-specific parameters for linear problems, for instance `--solver-parameters="THREADS 1 PRESOLVE 1"` + for XPRESS or `--solver-parameters="parallel/maxnthreads 1, lp/presolving TRUE"` for SCIP. Syntax is solver-dependent, and only supported for SCIP & XPRESS. -- **Expected value:** -- **Required:** **yes** -- **Default value:** -- **Usage:** +--- +#### quadratic-solver-parameters +- **Expected value:** a string +- **Required:** **no** +- **Default value:** empty +- **Usage:** Set solver-specific parameters for quadratic problems, for instance `--solver-parameters="THREADS 1 PRESOLVE 1"` + for XPRESS or `--solver-parameters="parallel/maxnthreads 1, lp/presolving TRUE"` for SCIP. Syntax is solver-dependent, and only supported for SCIP & XPRESS. --- ## Adequacy-patch parameters diff --git a/ortools_tag b/ortools_tag index 4121b49d4c..6eccc18013 100644 --- a/ortools_tag +++ b/ortools_tag @@ -1 +1 @@ -v9.11-rte1.1 \ No newline at end of file +v9.11-rte1.2 \ No newline at end of file diff --git a/src/libs/antares/InfoCollection/StudyInfoCollector.cpp b/src/libs/antares/InfoCollection/StudyInfoCollector.cpp index 7d07df9690..531f984ab5 100644 --- a/src/libs/antares/InfoCollection/StudyInfoCollector.cpp +++ b/src/libs/antares/InfoCollection/StudyInfoCollector.cpp @@ -145,8 +145,11 @@ void StudyInfoCollector::solverVersionToFileContent(FileContent& file_content) void StudyInfoCollector::ORToolsSolver(FileContent& file_content) { - std::string solverName = study_.parameters.optOptions.ortoolsSolver; - file_content.addItemToSection("study", "ortools solver", solverName); + std::string linearSolverName = study_.parameters.optOptions.linearSolver; + file_content.addItemToSection("study", "linear solver", linearSolverName); + + std::string quadraticSolverName = study_.parameters.optOptions.linearSolver; + file_content.addItemToSection("study", "quadratic solver", quadraticSolverName); } // Collecting data optimization problem diff --git a/src/libs/antares/optimization-options/include/antares/optimization-options/options.h b/src/libs/antares/optimization-options/include/antares/optimization-options/options.h index 19fea8b2e6..62aff96c36 100644 --- a/src/libs/antares/optimization-options/include/antares/optimization-options/options.h +++ b/src/libs/antares/optimization-options/include/antares/optimization-options/options.h @@ -28,8 +28,10 @@ namespace Antares::Solver::Optimization struct OptimizationOptions { //! The solver name, sirius is the default - std::string ortoolsSolver = "sirius"; + std::string linearSolver = "sirius"; + std::string quadraticSolver = "sirius"; + std::string linearSolverParameters; + std::string quadraticSolverParameters; bool solverLogs = false; - std::string solverParameters; }; } // namespace Antares::Solver::Optimization diff --git a/src/libs/antares/study/include/antares/study/load-options.h b/src/libs/antares/study/include/antares/study/load-options.h index 41eee86cbb..8a7353b63c 100644 --- a/src/libs/antares/study/include/antares/study/load-options.h +++ b/src/libs/antares/study/include/antares/study/load-options.h @@ -81,7 +81,7 @@ class StudyLoadOptions //! A non-zero value if the data will be used for a simulation bool usedByTheSolver; - //! All options related to optimization + //! All options related to linear & quadratic optimization Antares::Solver::Optimization::OptimizationOptions optOptions; //! Temporary string for passing log message diff --git a/src/libs/antares/study/include/antares/study/parameters.h b/src/libs/antares/study/include/antares/study/parameters.h index 664d574b32..c2a7fef593 100644 --- a/src/libs/antares/study/include/antares/study/parameters.h +++ b/src/libs/antares/study/include/antares/study/parameters.h @@ -503,7 +503,7 @@ class Parameters final // Naming constraints and variables in problems bool namedProblems; - // All options related to optimization + // All options related to linear & quadratic optimization Antares::Solver::Optimization::OptimizationOptions optOptions; private: diff --git a/src/libs/antares/study/parameters.cpp b/src/libs/antares/study/parameters.cpp index 60a3781bd2..d6846b9660 100644 --- a/src/libs/antares/study/parameters.cpp +++ b/src/libs/antares/study/parameters.cpp @@ -1283,8 +1283,10 @@ bool Parameters::loadFromINI(const IniFile& ini, const StudyVersion& version) void Parameters::handleOptimizationOptions(const StudyLoadOptions& options) { // Options only set from the command-line - optOptions.ortoolsSolver = options.optOptions.ortoolsSolver; - optOptions.solverParameters = options.optOptions.solverParameters; + optOptions.linearSolver = options.optOptions.linearSolver; + optOptions.linearSolverParameters = options.optOptions.linearSolverParameters; + optOptions.quadraticSolver = options.optOptions.quadraticSolver; + optOptions.quadraticSolverParameters = options.optOptions.quadraticSolverParameters; // Options that can be set both in command-line and file optOptions.solverLogs = options.optOptions.solverLogs || optOptions.solverLogs; @@ -1780,8 +1782,11 @@ void Parameters::prepareForSimulation(const StudyLoadOptions& options) logs.info() << " :: ignoring solution export"; } - logs.info() << " :: solver " << options.optOptions.ortoolsSolver - << " is used for problem resolution"; + logs.info() << " :: solver " << options.optOptions.linearSolver + << " is used for linear problem resolution"; + + logs.info() << " :: solver " << options.optOptions.quadraticSolver + << " is used for quadratic problem resolution"; // indicated that Problems will be named if (namedProblems) diff --git a/src/solver/application/application.cpp b/src/solver/application/application.cpp index 43dfa3368d..c0df0acb51 100644 --- a/src/solver/application/application.cpp +++ b/src/solver/application/application.cpp @@ -48,7 +48,10 @@ namespace { void printSolvers() { - std::cout << "Available solvers: " << availableOrToolsSolversString() << std::endl; + std::cout << "Available linear solvers: " // NOSONAR + << availableOrToolsSolversString(SolverClass::LINEAR) << std::endl; // NOSONAR + std::cout << "Available quadratic solvers: " // NOSONAR + << availableOrToolsSolversString(SolverClass::QUADRATIC) << std::endl; // NOSONAR } } // namespace @@ -275,7 +278,7 @@ void Application::postParametersChecks() const { // Some more checks require the existence of pParameters, hence of a study. // Their execution is delayed up to this point. checkSolverMILPincompatibility(pParameters->unitCommitment.ucMode, - pParameters->optOptions.ortoolsSolver); + pParameters->optOptions.linearSolver); checkSimplexRangeHydroPricing(pParameters->simplexOptimizationRange, pParameters->hydroPricing.hpMode); diff --git a/src/solver/misc/include/antares/solver/misc/options.h b/src/solver/misc/include/antares/solver/misc/options.h index 00daf5aec7..1b0bc8f418 100644 --- a/src/solver/misc/include/antares/solver/misc/options.h +++ b/src/solver/misc/include/antares/solver/misc/options.h @@ -75,5 +75,5 @@ std::unique_ptr CreateParser(Settings& settings, void checkAndCorrectSettingsAndOptions(Settings& settings, Data::StudyLoadOptions& options); -void checkOrtoolsSolver(const Antares::Solver::Optimization::OptimizationOptions& optOptions); +void checkSolvers(Data::StudyLoadOptions& options); #endif /* __SOLVER_MISC_GETOPT_H__ */ diff --git a/src/solver/misc/options.cpp b/src/solver/misc/options.cpp index 00572039a9..b22e7f90b6 100644 --- a/src/solver/misc/options.cpp +++ b/src/solver/misc/options.cpp @@ -27,6 +27,8 @@ #include #include +#include + #include #include @@ -75,22 +77,53 @@ std::unique_ptr CreateParser(Settings& settings, StudyLoad "force-parallel", "Override the max number of years computed simultaneously"); + //--linear-solver + parser->add(options.optOptions.linearSolver, + ' ', + "linear-solver", + "Solver used for linear optimizations during simulation\nAvailable solver list : " + + availableOrToolsSolversString(SolverClass::LINEAR)); + //--solver - parser->add(options.optOptions.ortoolsSolver, + parser->add(options.optOptions.linearSolver, ' ', "solver", - "Solver used for simulation\nAvailable solver list : " - + availableOrToolsSolversString()); + "Deprecated, use linear-solver instead."); - //--solver-parameters + //--linear-solver-parameters parser->add( - options.optOptions.solverParameters, + options.optOptions.linearSolverParameters, ' ', - "solver-parameters", - "Set solver-specific parameters, for instance --solver-parameters=\"THREADS 1 PRESOLVE 1\"" + "linear-solver-parameters", + "Set linear solver-specific parameters, for instance --linear-solver-parameters=\"THREADS 1 " + "PRESOLVE 1\"" "for XPRESS or --solver-parameters=\"parallel/maxnthreads 1, lp/presolving TRUE\" for SCIP." "Syntax is solver-dependent, and only supported for SCIP & XPRESS."); + //--solver-parameters + parser->add(options.optOptions.linearSolverParameters, + ' ', + "solver-parameters", + "Deprecated, use linear-solver-parameters instead."); + + //--quadratic-solver + parser->add( + options.optOptions.quadraticSolver, + ' ', + "quadratic-solver", + "Solver used for quadratic optimizations during simulation\nAvailable solver list : " + + availableOrToolsSolversString(SolverClass::QUADRATIC)); + + //--quadratic-solver-parameters + parser->add( + options.optOptions.quadraticSolverParameters, + ' ', + "quadratic-solver-parameters", + "Set quadratic solver-specific parameters, for instance " + "--quadratic-solver-parameters=\"THREADS 1 PRESOLVE 1\"" + "for XPRESS or --solver-parameters=\"parallel/maxnthreads 1, lp/presolving TRUE\" for SCIP." + "Syntax is solver-dependent."); + parser->addParagraph("\nParameters"); // --name parser->add(settings.simulationName, @@ -246,7 +279,7 @@ void checkAndCorrectSettingsAndOptions(Settings& settings, Data::StudyLoadOption } options.checkForceSimulationMode(); - checkOrtoolsSolver(options.optOptions); + checkSolvers(options); // no-output and force-zip-output if (settings.noOutput && settings.forceZipOutput) @@ -255,19 +288,24 @@ void checkAndCorrectSettingsAndOptions(Settings& settings, Data::StudyLoadOption } } -void checkOrtoolsSolver(const Antares::Solver::Optimization::OptimizationOptions& optOptions) +void checkSolverExists(std::string solverName, const std::list availableSolversList) { - const std::string& solverName = optOptions.ortoolsSolver; - const std::list availableSolverList = getAvailableOrtoolsSolverName(); - // Check if solver is available - bool found = (std::ranges::find(availableSolverList, solverName) != availableSolverList.end()); + bool found = std::ranges::find(availableSolversList, solverName) != availableSolversList.end(); if (!found) { - throw Error::InvalidSolver(optOptions.ortoolsSolver, availableOrToolsSolversString()); + throw Error::InvalidSolver(solverName, boost::algorithm::join(availableSolversList, ",")); } } +void checkSolvers(StudyLoadOptions& options) +{ + checkSolverExists(options.optOptions.linearSolver, + getAvailableSolverNames(SolverClass::LINEAR)); + checkSolverExists(options.optOptions.quadraticSolver, + getAvailableSolverNames(SolverClass::QUADRATIC)); +} + void Settings::checkAndSetStudyFolder(const std::string& folder) { // The study folder diff --git a/src/solver/optimisation/adequacy_patch_csr/adq_patch_curtailment_sharing.cpp b/src/solver/optimisation/adequacy_patch_csr/adq_patch_curtailment_sharing.cpp index 731aaac79c..0742dad644 100644 --- a/src/solver/optimisation/adequacy_patch_csr/adq_patch_curtailment_sharing.cpp +++ b/src/solver/optimisation/adequacy_patch_csr/adq_patch_curtailment_sharing.cpp @@ -198,12 +198,12 @@ void HourlyCSRProblem::setProblemCost() } } -void HourlyCSRProblem::solveProblem(uint week, int year) +void HourlyCSRProblem::solveProblem(uint week, int year, const OptimizationOptions& options) { - ADQ_PATCH_CSR(problemeAResoudre_, *this, adqPatchParams_, week, year); + ADQ_PATCH_CSR(options, problemeAResoudre_, *this, adqPatchParams_, week, year); } -void HourlyCSRProblem::run(uint week, uint year) +void HourlyCSRProblem::run(uint week, uint year, const OptimizationOptions& options) { calculateCsrParameters(); buildProblemVariables(); @@ -211,5 +211,5 @@ void HourlyCSRProblem::run(uint week, uint year) setVariableBounds(); buildProblemConstraintsRHS(); setProblemCost(); - solveProblem(week, year); + solveProblem(week, year, options); } diff --git a/src/solver/optimisation/adequacy_patch_csr/solve_problem.cpp b/src/solver/optimisation/adequacy_patch_csr/solve_problem.cpp index 4d209f1ade..6163fa5161 100644 --- a/src/solver/optimisation/adequacy_patch_csr/solve_problem.cpp +++ b/src/solver/optimisation/adequacy_patch_csr/solve_problem.cpp @@ -31,6 +31,8 @@ */ #include +#include "antares/solver/utils/ortools_quadratic_wrapper.h" + extern "C" { #include "pi_define.h" @@ -56,6 +58,9 @@ std::unique_ptr buildInteriorPointProblem( Probleme->NombreDeVariables = ProblemeAResoudre.NombreDeVariables; Probleme->TypeDeVariable = ProblemeAResoudre.TypeDeVariable.data(); + // The problem has no binary variable + // We use the fact that CoutsReduits is a vector of 1 element per variable, initialized to 0 + // TODO: make this cleaner Probleme->VariableBinaire = (char*)ProblemeAResoudre.CoutsReduits.data(); Probleme->NombreDeContraintes = ProblemeAResoudre.NombreDeContraintes; @@ -142,7 +147,7 @@ void storeOrDisregardInteriorPointResults(const PROBLEME_ANTARES_A_RESOUDRE& Pro } } -double calculateCSRcost(const PROBLEME_POINT_INTERIEUR& Probleme, +double calculateCSRcost(const PROBLEME_ANTARES_A_RESOUDRE& Probleme, const HourlyCSRProblem& hourlyCsrProblem, const AdqPatchParams& adqPatchParams) { @@ -203,7 +208,7 @@ double calculateCSRcost(const PROBLEME_POINT_INTERIEUR& Probleme, return cost; } -void CSR_DEBUG_HANDLE(const PROBLEME_POINT_INTERIEUR& Probleme) +void CSR_DEBUG_HANDLE(PROBLEME_ANTARES_A_RESOUDRE& Probleme) { logs.info(); logs.info() << LOG_UI_DISPLAY_MESSAGES_OFF; @@ -238,7 +243,7 @@ void CSR_DEBUG_HANDLE(const PROBLEME_POINT_INTERIEUR& Probleme) } } -void handleInteriorPointError([[maybe_unused]] const PROBLEME_POINT_INTERIEUR& Probleme, +void handleInteriorPointError([[maybe_unused]] PROBLEME_ANTARES_A_RESOUDRE& Probleme, int hour, uint weekNb, int yearNb) @@ -253,23 +258,21 @@ void handleInteriorPointError([[maybe_unused]] const PROBLEME_POINT_INTERIEUR& P #endif } -bool ADQ_PATCH_CSR(PROBLEME_ANTARES_A_RESOUDRE& ProblemeAResoudre, +bool Solve(const OptimizationOptions& options, PROBLEME_ANTARES_A_RESOUDRE& ProblemeAResoudre); + +bool ADQ_PATCH_CSR(const OptimizationOptions& options, + PROBLEME_ANTARES_A_RESOUDRE& ProblemeAResoudre, HourlyCSRProblem& hourlyCsrProblem, const AdqPatchParams& adqPatchParams, uint weekNb, int yearNb) { - auto interiorPointProblem = buildInteriorPointProblem(ProblemeAResoudre); - double costPriorToCsr = calculateCSRcost(*interiorPointProblem, - hourlyCsrProblem, - adqPatchParams); - PI_Quamin(interiorPointProblem.get()); // resolution - if (interiorPointProblem->ExistenceDUneSolution == OUI_PI) + double costPriorToCsr = calculateCSRcost(ProblemeAResoudre, hourlyCsrProblem, adqPatchParams); + bool feasible = Solve(options, ProblemeAResoudre); + if (feasible) { setToZeroIfBelowThreshold(ProblemeAResoudre, hourlyCsrProblem); - double costAfterCsr = calculateCSRcost(*interiorPointProblem, - hourlyCsrProblem, - adqPatchParams); + double costAfterCsr = calculateCSRcost(ProblemeAResoudre, hourlyCsrProblem, adqPatchParams); storeOrDisregardInteriorPointResults(ProblemeAResoudre, hourlyCsrProblem, adqPatchParams, @@ -281,10 +284,36 @@ bool ADQ_PATCH_CSR(PROBLEME_ANTARES_A_RESOUDRE& ProblemeAResoudre, } else { - handleInteriorPointError(*interiorPointProblem, - hourlyCsrProblem.triggeredHour, - weekNb, - yearNb); + handleInteriorPointError(ProblemeAResoudre, hourlyCsrProblem.triggeredHour, weekNb, yearNb); return false; } } + +bool SolveWithSirius(const OptimizationOptions& options, + PROBLEME_ANTARES_A_RESOUDRE& ProblemeAResoudre) +{ + if (!options.quadraticSolverParameters.empty()) + { + logs.warning() + << "Quadratic solver parameters are not supported by SIRIUS; they will be ignored."; + } + auto interiorPointProblem = buildInteriorPointProblem(ProblemeAResoudre); + PI_Quamin(interiorPointProblem.get()); // resolution + return interiorPointProblem->ExistenceDUneSolution == OUI_PI; +} + +bool SolveWithOrtools(const OptimizationOptions& options, + PROBLEME_ANTARES_A_RESOUDRE& ProblemeAResoudre) +{ + SolveQuadraticProblemWithOrtools(options, &ProblemeAResoudre); + return ProblemeAResoudre.ExistenceDUneSolution == OUI_PI; +} + +bool Solve(const OptimizationOptions& options, PROBLEME_ANTARES_A_RESOUDRE& ProblemeAResoudre) +{ + if (options.quadraticSolver.compare("sirius") == 0) + { + return SolveWithSirius(options, ProblemeAResoudre); + } + return SolveWithOrtools(options, ProblemeAResoudre); +} diff --git a/src/solver/optimisation/adequacy_patch_csr/solve_problem.h b/src/solver/optimisation/adequacy_patch_csr/solve_problem.h index 59b0619ca2..bcd7bdd4a6 100644 --- a/src/solver/optimisation/adequacy_patch_csr/solve_problem.h +++ b/src/solver/optimisation/adequacy_patch_csr/solve_problem.h @@ -7,7 +7,8 @@ using namespace Antares::Data::AdequacyPatch; -bool ADQ_PATCH_CSR(PROBLEME_ANTARES_A_RESOUDRE&, +bool ADQ_PATCH_CSR(const OptimizationOptions& options, + PROBLEME_ANTARES_A_RESOUDRE&, HourlyCSRProblem&, const AdqPatchParams&, unsigned int week, diff --git a/src/solver/optimisation/include/antares/solver/optimisation/adequacy_patch_csr/hourly_csr_problem.h b/src/solver/optimisation/include/antares/solver/optimisation/adequacy_patch_csr/hourly_csr_problem.h index 6ebb9734e4..cbc751874f 100644 --- a/src/solver/optimisation/include/antares/solver/optimisation/adequacy_patch_csr/hourly_csr_problem.h +++ b/src/solver/optimisation/include/antares/solver/optimisation/adequacy_patch_csr/hourly_csr_problem.h @@ -27,6 +27,7 @@ #include #include +#include "antares/solver/optimisation/opt_fonctions.h" #include "antares/solver/optimisation/opt_structure_probleme_a_resoudre.h" #include "../variables/VariableManagerUtils.h" @@ -92,7 +93,9 @@ class HourlyCSRProblem triggeredHour = hour; } - void run(uint week, uint year); + void run(unsigned int week, + unsigned int year, + const Antares::Solver::Optimization::OptimizationOptions& options); private: void calculateCsrParameters(); @@ -102,7 +105,9 @@ class HourlyCSRProblem void buildProblemConstraintsLHS(); void buildProblemConstraintsRHS(); void setProblemCost(); - void solveProblem(uint week, int year); + void solveProblem(unsigned int week, + int year, + const Antares::Solver::Optimization::OptimizationOptions& options); void allocateProblem(); // variable construction diff --git a/src/solver/optimisation/include/antares/solver/optimisation/opt_fonctions.h b/src/solver/optimisation/include/antares/solver/optimisation/opt_fonctions.h index 4d761bd558..51cd8302a1 100644 --- a/src/solver/optimisation/include/antares/solver/optimisation/opt_fonctions.h +++ b/src/solver/optimisation/include/antares/solver/optimisation/opt_fonctions.h @@ -54,14 +54,16 @@ void OPT_InitialiserLeSecondMembreDuProblemeLineaire(PROBLEME_HEBDO*, int, int, void OPT_InitialiserLeSecondMembreDuProblemeQuadratique(PROBLEME_HEBDO*, int); void OPT_InitialiserLesCoutsLineaire(PROBLEME_HEBDO*, const int, const int); void OPT_InitialiserLesCoutsQuadratiques(PROBLEME_HEBDO*, int); -bool OPT_AppelDuSolveurQuadratique(PROBLEME_ANTARES_A_RESOUDRE*, const int); +bool OPT_AppelDuSolveurQuadratique(const OptimizationOptions& options, + PROBLEME_ANTARES_A_RESOUDRE*, + const int); bool OPT_PilotageOptimisationLineaire(const OptimizationOptions& options, PROBLEME_HEBDO* problemeHebdo, Solver::IResultWriter& writer, Solver::Simulation::ISimulationObserver& simulationObserver); void OPT_VerifierPresenceReserveJmoins1(PROBLEME_HEBDO*); -bool OPT_PilotageOptimisationQuadratique(PROBLEME_HEBDO*); +bool OPT_PilotageOptimisationQuadratique(const OptimizationOptions& options, PROBLEME_HEBDO*); /*! ** \brief Appel du solver diff --git a/src/solver/optimisation/opt_appel_solveur_lineaire.cpp b/src/solver/optimisation/opt_appel_solveur_lineaire.cpp index 304aab13df..760c25785a 100644 --- a/src/solver/optimisation/opt_appel_solveur_lineaire.cpp +++ b/src/solver/optimisation/opt_appel_solveur_lineaire.cpp @@ -190,7 +190,7 @@ static SimplexResult OPT_TryToCallSimplex(const OptimizationOptions& options, Probleme.NombreDeContraintesCoupes = 0; auto ortoolsProblem = std::make_unique(Probleme.isMIP(), - options.ortoolsSolver); + options.linearSolver); auto legacyOrtoolsFiller = std::make_unique(&Probleme); std::vector fillersCollection = {legacyOrtoolsFiller.get()}; LinearProblemData LP_Data; @@ -353,7 +353,7 @@ bool OPT_AppelDuSimplexe(const OptimizationOptions& options, Probleme.SetUseNamedProblems(true); auto ortoolsProblem = std::make_unique(Probleme.isMIP(), - options.ortoolsSolver); + options.linearSolver); auto legacyOrtoolsFiller = std::make_unique(&Probleme); std::vector fillersCollection = {legacyOrtoolsFiller.get()}; LinearProblemData LP_Data; diff --git a/src/solver/optimisation/opt_appel_solveur_quadratique.cpp b/src/solver/optimisation/opt_appel_solveur_quadratique.cpp index d59c6d8779..9c7958cd83 100644 --- a/src/solver/optimisation/opt_appel_solveur_quadratique.cpp +++ b/src/solver/optimisation/opt_appel_solveur_quadratique.cpp @@ -36,13 +36,44 @@ extern "C" } #include +#include "antares/optimization-options/options.h" #include "antares/solver/optimisation/opt_structure_probleme_a_resoudre.h" +#include "antares/solver/utils/ortools_quadratic_wrapper.h" +#include "antares/solver/utils/ortools_utils.h" using namespace Antares; -bool OPT_AppelDuSolveurQuadratique(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre, +void SolveWithSirius(const Solver::Optimization::OptimizationOptions& options, + PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre); +void ProcessResult(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre, int PdtHebdo); + +bool OPT_AppelDuSolveurQuadratique(const Solver::Optimization::OptimizationOptions& options, + PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre, const int PdtHebdo) { + // as long as sirius quadratic optimization is not supported through or-tools, we have to keep + // this code separate + if (options.quadraticSolver.compare("sirius") == 0) + { + SolveWithSirius(options, ProblemeAResoudre); + } + else + { + SolveQuadraticProblemWithOrtools(options, ProblemeAResoudre); + } + ProcessResult(ProblemeAResoudre, PdtHebdo); + return ProblemeAResoudre->ExistenceDUneSolution == OUI_PI; +} + +void SolveWithSirius(const Solver::Optimization::OptimizationOptions& options, + PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre) +{ + if (!options.quadraticSolverParameters.empty()) + { + logs.warning() + << "Quadratic solver parameters are not supported by SIRIUS; they will be ignored."; + } + PROBLEME_POINT_INTERIEUR Probleme; double ToleranceSurLAdmissibilite = 1.e-5; @@ -63,6 +94,9 @@ bool OPT_AppelDuSolveurQuadratique(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudr Probleme.NombreDeVariables = ProblemeAResoudre->NombreDeVariables; Probleme.TypeDeVariable = ProblemeAResoudre->TypeDeVariable.data(); + // The problem has no binary variable + // We use the fact that CoutsReduits is a vector of 1 element per variable, initialized to 0 + // TODO: make this cleaner Probleme.VariableBinaire = (char*)ProblemeAResoudre->CoutsReduits.data(); Probleme.NombreDeContraintes = ProblemeAResoudre->NombreDeContraintes; @@ -106,13 +140,9 @@ bool OPT_AppelDuSolveurQuadratique(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudr *pt = ProblemeAResoudre->X[i]; } } - - return true; } else { - logs.warning() << "Quadratic Optimisation: No solution, hour " << PdtHebdo; - for (int i = 0; i < ProblemeAResoudre->NombreDeVariables; i++) { double* pt = ProblemeAResoudre->AdresseOuPlacerLaValeurDesVariablesOptimisees[i]; @@ -121,7 +151,14 @@ bool OPT_AppelDuSolveurQuadratique(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudr *pt = std::numeric_limits::quiet_NaN(); } } + } +} +void ProcessResult(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre, const int PdtHebdo) +{ + if (ProblemeAResoudre->ExistenceDUneSolution == NON_PI) + { + logs.warning() << "Quadratic Optimisation: No solution, hour " << PdtHebdo; #ifndef NDEBUG { @@ -160,7 +197,5 @@ bool OPT_AppelDuSolveurQuadratique(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudr } } #endif - - return false; } } diff --git a/src/solver/optimisation/opt_optimisation_hebdo.cpp b/src/solver/optimisation/opt_optimisation_hebdo.cpp index bd0d78d037..042f889151 100644 --- a/src/solver/optimisation/opt_optimisation_hebdo.cpp +++ b/src/solver/optimisation/opt_optimisation_hebdo.cpp @@ -27,13 +27,13 @@ using namespace Antares::Data; -using Antares::Solver::Optimization::OptimizationOptions; +using Solver::Optimization::OptimizationOptions; bool OPT_PilotageOptimisationLineaire(const OptimizationOptions&, PROBLEME_HEBDO*, Solver::IResultWriter&, Solver::Simulation::ISimulationObserver&); -bool OPT_PilotageOptimisationQuadratique(PROBLEME_HEBDO*); +bool OPT_PilotageOptimisationQuadratique(const OptimizationOptions&, PROBLEME_HEBDO*); void OPT_LiberationProblemesSimplexe(const PROBLEME_HEBDO*); void OPT_OptimisationHebdomadaire(const OptimizationOptions& options, @@ -52,7 +52,7 @@ void OPT_OptimisationHebdomadaire(const OptimizationOptions& options, else if (pProblemeHebdo->TypeDOptimisation == OPTIMISATION_QUADRATIQUE) { OPT_LiberationProblemesSimplexe(pProblemeHebdo); - if (!OPT_PilotageOptimisationQuadratique(pProblemeHebdo)) + if (!OPT_PilotageOptimisationQuadratique(options, pProblemeHebdo)) { logs.error() << "Quadratic optimization failed"; throw UnfeasibleProblemError("Quadratic optimization failed"); diff --git a/src/solver/optimisation/opt_pilotage_optimisation_quadratique.cpp b/src/solver/optimisation/opt_pilotage_optimisation_quadratique.cpp index 1337fb763b..5ff6e806e6 100644 --- a/src/solver/optimisation/opt_pilotage_optimisation_quadratique.cpp +++ b/src/solver/optimisation/opt_pilotage_optimisation_quadratique.cpp @@ -23,7 +23,8 @@ #include "antares/solver/optimisation/constraints/constraint_builder_utils.h" #include "antares/solver/optimisation/opt_fonctions.h" -bool OPT_PilotageOptimisationQuadratique(PROBLEME_HEBDO* problemeHebdo) +bool OPT_PilotageOptimisationQuadratique(const OptimizationOptions& options, + PROBLEME_HEBDO* problemeHebdo) { if (!problemeHebdo->LeProblemeADejaEteInstancie) { @@ -51,7 +52,9 @@ bool OPT_PilotageOptimisationQuadratique(PROBLEME_HEBDO* problemeHebdo) OPT_InitialiserLesCoutsQuadratiques(problemeHebdo, pdtHebdo); - result = OPT_AppelDuSolveurQuadratique(problemeHebdo->ProblemeAResoudre.get(), pdtHebdo) + result = OPT_AppelDuSolveurQuadratique(options, + problemeHebdo->ProblemeAResoudre.get(), + pdtHebdo) && result; } } diff --git a/src/solver/optimisation/post_process_commands.cpp b/src/solver/optimisation/post_process_commands.cpp index 83446402a9..8ce0e8c546 100644 --- a/src/solver/optimisation/post_process_commands.cpp +++ b/src/solver/optimisation/post_process_commands.cpp @@ -276,7 +276,7 @@ void CurtailmentSharingPostProcessCmd::execute(const optRuntimeData& opt_runtime logs.info() << "[adq-patch] CSR triggered for Year:" << year + 1 << " Hour:" << week * nbHoursInWeek + hourInWeek + 1; hourlyCsrProblem.setHour(hourInWeek); - hourlyCsrProblem.run(week, year); + hourlyCsrProblem.run(week, year, opt_runtime_data.options); } } diff --git a/src/solver/simulation/economy.cpp b/src/solver/simulation/economy.cpp index 0b8b341148..eb62882f95 100644 --- a/src/solver/simulation/economy.cpp +++ b/src/solver/simulation/economy.cpp @@ -163,7 +163,10 @@ bool Economy::year(Progression::Task& progression, weeklyOptProblems_[numSpace].solve(); // Runs all the post processes in the list of post-process commands - optRuntimeData opt_runtime_data(state.year, w, hourInTheYear); + optRuntimeData opt_runtime_data(state.year, + w, + hourInTheYear, + study.parameters.optOptions); postProcessesList_[numSpace]->runAll(opt_runtime_data); variables.weekBegin(state); diff --git a/src/solver/simulation/include/antares/solver/simulation/base_post_process.h b/src/solver/simulation/include/antares/solver/simulation/base_post_process.h index f1e6215415..82f880c397 100644 --- a/src/solver/simulation/include/antares/solver/simulation/base_post_process.h +++ b/src/solver/simulation/include/antares/solver/simulation/base_post_process.h @@ -23,6 +23,7 @@ #include #include +#include "antares/solver/optimisation/opt_fonctions.h" #include "antares/solver/simulation/sim_structure_probleme_economique.h" using namespace Antares::Data; @@ -34,16 +35,18 @@ namespace Antares::Solver::Simulation struct optRuntimeData { - optRuntimeData(unsigned int y, unsigned int w, unsigned int h): + optRuntimeData(unsigned int y, unsigned int w, unsigned int h, const OptimizationOptions& opt): year(y), week(w), - hourInTheYear(h) + hourInTheYear(h), + options(opt) { } unsigned int year = 0; unsigned int week = 0; unsigned int hourInTheYear = 0; + const OptimizationOptions& options; }; class basePostProcessCommand diff --git a/src/solver/simulation/include/antares/solver/simulation/solver.hxx b/src/solver/simulation/include/antares/solver/simulation/solver.hxx index 7a47a75e69..add3dee009 100644 --- a/src/solver/simulation/include/antares/solver/simulation/solver.hxx +++ b/src/solver/simulation/include/antares/solver/simulation/solver.hxx @@ -30,6 +30,7 @@ #include "antares/concurrency/concurrency.h" #include "antares/solver/hydro/management/HydroInputsChecker.h" #include "antares/solver/hydro/management/management.h" +#include "antares/solver/optimisation/opt_fonctions.h" #include "antares/solver/simulation/opt_time_writer.h" #include "antares/solver/simulation/timeseries-numbers.h" #include "antares/solver/ts-generator/generator.h" diff --git a/src/solver/utils/CMakeLists.txt b/src/solver/utils/CMakeLists.txt index 16cf2a96e5..b1d58fc746 100644 --- a/src/solver/utils/CMakeLists.txt +++ b/src/solver/utils/CMakeLists.txt @@ -1,6 +1,8 @@ set(SRC include/antares/solver/utils/ortools_utils.h ortools_utils.cpp + include/antares/solver/utils/ortools_quadratic_wrapper.h + ortools_quadratic_wrapper.cpp include/antares/solver/utils/filename.h filename.cpp include/antares/solver/utils/named_problem.h @@ -11,10 +13,10 @@ set(SRC name_translator.cpp include/antares/solver/utils/opt_period_string_generator.h opt_period_string_generator.cpp - basis_status.cpp - include/antares/solver/utils/basis_status.h - basis_status_impl.cpp - basis_status_impl.h + basis_status.cpp + include/antares/solver/utils/basis_status.h + basis_status_impl.cpp + basis_status_impl.h ) add_library(utils ${SRC}) @@ -23,22 +25,19 @@ if (NOT MSVC) target_compile_options(utils PUBLIC "-Wno-unused-variable") -else () - #set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS} /wd 4101") # unused local variable - #set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS} /wd 4101") # unused local variable endif () target_link_libraries(utils PUBLIC ortools::ortools sirius_solver Antares::study - Antares::result_writer #ortools_utils.h + Antares::result_writer Antares::optimization-options - antares-core #enum.h + antares-core ) target_include_directories(utils - PUBLIC + PUBLIC $ ) diff --git a/src/solver/utils/include/antares/solver/utils/ortools_quadratic_wrapper.h b/src/solver/utils/include/antares/solver/utils/ortools_quadratic_wrapper.h new file mode 100644 index 0000000000..a823a4bfbb --- /dev/null +++ b/src/solver/utils/include/antares/solver/utils/ortools_quadratic_wrapper.h @@ -0,0 +1,35 @@ +/* +** Copyright 2007-2025, RTE (https://www.rte-france.com) +** See AUTHORS.txt +** SPDX-License-Identifier: MPL-2.0 +** This file is part of Antares-Simulator, +** Adequacy and Performance assessment for interconnected energy networks. +** +** Antares_Simulator is free software: you can redistribute it and/or modify +** it under the terms of the Mozilla Public Licence 2.0 as published by +** the Mozilla Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** 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 +** Mozilla Public Licence 2.0 for more details. +** +** You should have received a copy of the Mozilla Public Licence 2.0 +** along with Antares_Simulator. If not, see . +*/ + +#pragma once + +#include +#include + +/** + * This wrapper is an adapter that solves a QP stored in a PROBLEME_ANTARES_A_RESOUDRE + * using OR-Tools MathOpt API & solvers. + * Currently, QP support in MathOpt is implemented for Gurobi and SIP (though SCIP in OR-Tools + * has compilation issues), and under development for XPRESS. + */ +void SolveQuadraticProblemWithOrtools( + const Antares::Solver::Optimization::OptimizationOptions& options, + PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre); diff --git a/src/solver/utils/include/antares/solver/utils/ortools_utils.h b/src/solver/utils/include/antares/solver/utils/ortools_utils.h index e5fae32c9d..7b0c5b26d1 100644 --- a/src/solver/utils/include/antares/solver/utils/ortools_utils.h +++ b/src/solver/utils/include/antares/solver/utils/ortools_utils.h @@ -33,25 +33,36 @@ #include "ortools_wrapper.h" +namespace operations_research::math_opt +{ +enum class SolverType; +} + using namespace operations_research; +enum class SolverClass +{ + LINEAR, + QUADRATIC +}; + void ORTOOLS_EcrireJeuDeDonneesLineaireAuFormatMPS(MPSolver* solver, Antares::Solver::IResultWriter& writer, const std::string& filename); /*! - * \brief Return list of available ortools solver name on our side + * \brief Return list of available ortools solver names on our side * - * \return List of available ortools solver name + * \return List of available ortools solver names */ -std::list getAvailableOrtoolsSolverName(); +std::list getAvailableSolverNames(SolverClass solverClass); /*! * \brief Return a single string containing all solvers available, separated by a ", " and ending * with a ".". * */ -std::string availableOrToolsSolversString(); +std::string availableOrToolsSolversString(SolverClass solverClass); /*! * \brief Create a MPSolver with correct linear or mixed variant @@ -70,5 +81,7 @@ class OrtoolsUtils { std::string LPSolverName, MIPSolverName; }; - static const std::map solverMap; + + static const std::map mpSolverMap; + static const std::map mathoptSolverMap; }; diff --git a/src/solver/utils/ortools_quadratic_wrapper.cpp b/src/solver/utils/ortools_quadratic_wrapper.cpp new file mode 100644 index 0000000000..025dd81d7b --- /dev/null +++ b/src/solver/utils/ortools_quadratic_wrapper.cpp @@ -0,0 +1,261 @@ +/* +** Copyright 2007-2025, RTE (https://www.rte-france.com) +** See AUTHORS.txt +** SPDX-License-Identifier: MPL-2.0 +** This file is part of Antares-Simulator, +** Adequacy and Performance assessment for interconnected energy networks. +** +** Antares_Simulator is free software: you can redistribute it and/or modify +** it under the terms of the Mozilla Public Licence 2.0 as published by +** the Mozilla Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** 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 +** Mozilla Public Licence 2.0 for more details. +** +** You should have received a copy of the Mozilla Public Licence 2.0 +** along with Antares_Simulator. If not, see . +*/ + +#include +#include +#include +#include + +#include +#include +#include + +using Antares::Solver::Optimization::OptimizationOptions; +using namespace operations_research::math_opt; + +constexpr double kInf = std::numeric_limits::infinity(); + +void BuildVariablesAndObjective(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre, Model& model); +void BuildConstraints(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre, Model& model); + +// TODO: interpret the following lines sent to sirius & adapt them to mathopt if needed +// Probleme->UtiliserLaToleranceDAdmissibiliteParDefaut = OUI_PI; +// Probleme->UtiliserLaToleranceDeStationnariteParDefaut = OUI_PI; +// Probleme->UtiliserLaToleranceDeComplementariteParDefaut = OUI_PI; + +void checkOptions(const OptimizationOptions& options) +{ + auto availableSolversList = getAvailableSolverNames(SolverClass::QUADRATIC); + bool solverFound = std::ranges::find(availableSolversList, options.quadraticSolver) + != availableSolversList.end(); + if (!solverFound || options.quadraticSolver.compare("sirius") == 0) + { + throw std::invalid_argument( + "Solver " + options.quadraticSolver + + " is not supported for quadratic problems optimization through MathOpt."); + } + if (!options.quadraticSolverParameters.empty()) + { + // TODO: handle these by mapping them to generic or solver-specific params in mathopt + // TODO: or remove this for now? + Antares::logs.warning() + << "Quadratic solver parameters are not supported yet; they will be ignored."; + } +} + +void ProcessSolveResult(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre, + Model& model, + absl::StatusOr resultStatus); + +void SolveQuadraticProblemWithOrtools(const OptimizationOptions& options, + PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre) +{ + checkOptions(options); + Model model("Quadratic problem"); + BuildVariablesAndObjective(ProblemeAResoudre, model); + BuildConstraints(ProblemeAResoudre, model); + SolveArguments args; + if (options.solverLogs) + { + args.parameters.enable_output = true; + } + auto solverType = OrtoolsUtils::mathoptSolverMap.at(options.quadraticSolver); + auto resultStatus = Solve(model, solverType, args); + ProcessSolveResult(ProblemeAResoudre, model, resultStatus); +} + +void BuildVariablesAndObjective(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre, Model& model) +{ + QuadraticExpression objective(0); + for (auto i = 0; i < ProblemeAResoudre->NombreDeVariables; ++i) + { + double lb, ub; + switch (ProblemeAResoudre->TypeDeVariable[i]) + { + case VARIABLE_BORNEE_DES_DEUX_COTES: + lb = ProblemeAResoudre->Xmin[i]; + ub = ProblemeAResoudre->Xmax[i]; + break; + case VARIABLE_BORNEE_INFERIEUREMENT: + lb = ProblemeAResoudre->Xmin[i]; + ub = kInf; + break; + case VARIABLE_BORNEE_SUPERIEUREMENT: + lb = -kInf; + ub = ProblemeAResoudre->Xmax[i]; + break; + case VARIABLE_NON_BORNEE: + lb = -kInf; + ub = kInf; + break; + default: + throw std::invalid_argument("Unknown variable type: " + + std::to_string(ProblemeAResoudre->TypeDeVariable[i])); + } + std::string name = ProblemeAResoudre->NomDesVariables[i].empty() + ? "C" + std::to_string(i) + : ProblemeAResoudre->NomDesVariables[i]; + auto var = model.AddVariable(lb, ub, ProblemeAResoudre->VariablesEntieres[i], name); + objective += QuadraticExpression( + {QuadraticTerm(var, var, ProblemeAResoudre->CoutQuadratique[i])}, + {LinearTerm(var, ProblemeAResoudre->CoutLineaire[i])}, + 0.0); + } + model.SetObjective(objective, false); +} + +void BuildConstraints(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre, Model& model) +{ + for (auto iCt = 0; iCt < ProblemeAResoudre->NombreDeContraintes; ++iCt) + { + LinearExpression linear_expression(0); + for (auto iCoef = 0; iCoef < ProblemeAResoudre->NombreDeTermesDesLignes[iCt]; ++iCoef) + { + int iVar = ProblemeAResoudre->IndicesColonnes + .data()[ProblemeAResoudre->IndicesDebutDeLigne[iCt] + iCoef]; + auto coef = ProblemeAResoudre->CoefficientsDeLaMatriceDesContraintes + .data()[ProblemeAResoudre->IndicesDebutDeLigne[iCt] + iCoef]; + linear_expression += model.variable(iVar) * coef; + } + double lb = -kInf; + double ub = kInf; + switch (ProblemeAResoudre->Sens[iCt]) + { + case '=': + lb = ub = ProblemeAResoudre->SecondMembre[iCt]; + break; + case '<': + ub = ProblemeAResoudre->SecondMembre[iCt]; + break; + case '>': + lb = ProblemeAResoudre->SecondMembre[iCt]; + break; + default: + throw std::invalid_argument("Expected constraint sense to be =, <, or >, but was: " + + ProblemeAResoudre->Sens.substr(iCt, 1)); + } + BoundedLinearExpression bounded_linear_expression(std::move(linear_expression), lb, ub); + std::string name = ProblemeAResoudre->NomDesContraintes[iCt].empty() + ? "R" + std::to_string(iCt) + : ProblemeAResoudre->NomDesContraintes[iCt]; + model.AddLinearConstraint(bounded_linear_expression, name); + } +} + +void FillWithValues(std::vector& destination, const std::vector& origin) +{ + if (destination.size() != origin.size()) + { + throw std::invalid_argument("Expected destination and origin to have the same size"); + } + for (auto i = 0; i < destination.size(); ++i) + { + destination[i] = origin[i]; + } +} + +void FillWithValues(std::vector& destination, const std::vector& origin) +{ + if (destination.size() != origin.size()) + { + throw std::invalid_argument("Expected destination and origin to have the same size"); + } + for (auto i = 0; i < destination.size(); ++i) + { + double* pt = destination[i]; + if (pt) + { + *pt = origin[i]; + } + } +} + +void FillWithNaN(std::vector& vector) +{ + for (auto i = 0; i < vector.size(); ++i) + { + vector[i] = std::numeric_limits::quiet_NaN(); + } +} + +void FillWithNaN(std::vector& vector) +{ + for (auto i = 0; i < vector.size(); ++i) + { + double* pt = vector[i]; + if (pt) + { + *pt = std::numeric_limits::quiet_NaN(); + } + } +} + +void ProcessSolveResult(PROBLEME_ANTARES_A_RESOUDRE* ProblemeAResoudre, + Model& model, + absl::StatusOr resultStatus) +{ + // Store results (status, primals, duals, reduced costs) in problem structure + if (resultStatus.ok() && resultStatus.value().has_primal_feasible_solution()) + { + ProblemeAResoudre->ExistenceDUneSolution = OUI_PI; + std::vector primals; + primals.reserve(ProblemeAResoudre->NombreDeVariables); + std::vector reducedCosts; + reducedCosts.reserve(ProblemeAResoudre->NombreDeVariables); + for (int i = 0; i < ProblemeAResoudre->NombreDeVariables; ++i) + { + auto var = model.variable(i); + primals.emplace_back( + resultStatus.value().best_primal_solution().variable_values.at(var)); + reducedCosts.emplace_back(resultStatus.value().reduced_costs().at(var)); + } + FillWithValues(ProblemeAResoudre->X, primals); + FillWithValues(ProblemeAResoudre->AdresseOuPlacerLaValeurDesVariablesOptimisees, primals); + FillWithValues(ProblemeAResoudre->CoutsReduits, reducedCosts); + FillWithValues(ProblemeAResoudre->AdresseOuPlacerLaValeurDesCoutsReduits, reducedCosts); + + // Dual values + if (resultStatus.value().has_dual_feasible_solution()) + { + std::vector duals; + duals.reserve(ProblemeAResoudre->NombreDeContraintes); + for (int i = 0; i < ProblemeAResoudre->NombreDeContraintes; ++i) + { + duals.emplace_back( + resultStatus.value().dual_values().at(model.linear_constraint(i))); + } + FillWithValues(ProblemeAResoudre->CoutsMarginauxDesContraintes, duals); + FillWithValues(ProblemeAResoudre->AdresseOuPlacerLaValeurDesCoutsMarginaux, duals); + } + } + else + { + Antares::logs.warning() << "Quadratic optimization failed: " + << resultStatus.status().ToString(); + ProblemeAResoudre->ExistenceDUneSolution = NON_PI; + FillWithNaN(ProblemeAResoudre->X); + FillWithNaN(ProblemeAResoudre->AdresseOuPlacerLaValeurDesVariablesOptimisees); + FillWithNaN(ProblemeAResoudre->CoutsMarginauxDesContraintes); + FillWithNaN(ProblemeAResoudre->AdresseOuPlacerLaValeurDesCoutsMarginaux); + FillWithNaN(ProblemeAResoudre->CoutsReduits); + FillWithNaN(ProblemeAResoudre->AdresseOuPlacerLaValeurDesCoutsReduits); + } +} diff --git a/src/solver/utils/ortools_utils.cpp b/src/solver/utils/ortools_utils.cpp index c72fb67762..3721464445 100644 --- a/src/solver/utils/ortools_utils.cpp +++ b/src/solver/utils/ortools_utils.cpp @@ -22,6 +22,9 @@ #include #include +#include + +#include #include #include @@ -208,6 +211,8 @@ void ORTOOLS_EcrireJeuDeDonneesLineaireAuFormatMPS(MPSolver* solver, removeTemporaryFile(tmpPath); } +static int iLp = 0; + bool solveAndManageStatus(MPSolver* solver, int& resultStatus, const MPSolverParameters& params) { auto status = solver->Solve(params); @@ -236,7 +241,7 @@ MPSolver* ORTOOLS_Simplexe(Antares::Optimization::PROBLEME_SIMPLEXE_NOMME* Probl { solver->EnableOutput(); } - TuneSolverSpecificOptions(solver, options.ortoolsSolver, options.solverParameters); + TuneSolverSpecificOptions(solver, options.linearSolver, options.linearSolverParameters); const bool warmStart = solverSupportsWarmStart(solver->ProblemType()); // Provide an initial simplex basis, if any if (warmStart && Probleme->basisExists()) @@ -317,18 +322,23 @@ void ORTOOLS_LibererProbleme(MPSolver* solver) delete solver; } -const std::map OrtoolsUtils::solverMap = { +const std::map OrtoolsUtils::mpSolverMap = { {"xpress", {"xpress_lp", "xpress"}}, {"sirius", {"sirius_lp", "sirius"}}, {"coin", {"clp", "cbc"}}, {"glpk", {"glpk_lp", "glpk"}}, {"scip", {"scip", "scip"}}}; -std::list getAvailableOrtoolsSolverName() +// TODO: add SCIP support when fixed by google: {"scip", math_opt::SolverType::kGscip} +// TODO: add XPRESS support when added in or-tools: {"xpress", math_opt::SolverType::kXpress} +const std::map OrtoolsUtils::mathoptSolverMap = { + {"pdlp", math_opt::SolverType::kPdlp}}; + +std::list getAvailableLinearSolverNames() { std::list result; - for (const auto& solverName: OrtoolsUtils::solverMap) + for (const auto& solverName: OrtoolsUtils::mpSolverMap) { MPSolver::OptimizationProblemType solverType; MPSolver::ParseSolverType(solverName.second.LPSolverName, &solverType); @@ -341,17 +351,27 @@ std::list getAvailableOrtoolsSolverName() return result; } -std::string availableOrToolsSolversString() +std::list getAvailableQuadraticSolverNames() { - const std::list availableSolverList = getAvailableOrtoolsSolverName(); - std::ostringstream solvers; - for (const std::string& avail: availableSolverList) + std::list result; + // Sirius is supported, but not through mathopt + result.push_back("sirius"); + for (const auto& solverName: OrtoolsUtils::mathoptSolverMap) { - bool last = &avail == &availableSolverList.back(); - std::string sep = last ? "." : ", "; - solvers << avail << sep; + result.push_back(solverName.first); } - return solvers.str(); + return result; +} + +std::list getAvailableSolverNames(SolverClass solverClass) +{ + return solverClass == SolverClass::LINEAR ? getAvailableLinearSolverNames() + : getAvailableQuadraticSolverNames(); +} + +std::string availableOrToolsSolversString(SolverClass solverClass) +{ + return boost::algorithm::join(getAvailableSolverNames(solverClass), ",") + "."; } static std::optional translateSolverName(const std::string& solverName, bool isMip) @@ -360,11 +380,11 @@ static std::optional translateSolverName(const std::string& solverN { if (isMip) { - return OrtoolsUtils::solverMap.at(solverName).MIPSolverName; + return OrtoolsUtils::mpSolverMap.at(solverName).MIPSolverName; } else { - return OrtoolsUtils::solverMap.at(solverName).LPSolverName; + return OrtoolsUtils::mpSolverMap.at(solverName).LPSolverName; } } catch (const std::out_of_range&) @@ -375,7 +395,8 @@ static std::optional translateSolverName(const std::string& solverN MPSolver* MPSolverFactory(const bool isMip, const std::string& solverName) { - const std::string notFound = "Solver " + solverName + " not found"; + const std::string notFound = "Solver " + solverName + + " not supported for linear problems optimization."; const std::invalid_argument except(notFound); auto internalSolverName = translateSolverName(solverName, isMip); diff --git a/src/tests/cucumber/features/steps/common_steps/simulator_utils.py b/src/tests/cucumber/features/steps/common_steps/simulator_utils.py index e48b5a9da3..46a3f6fb33 100644 --- a/src/tests/cucumber/features/steps/common_steps/simulator_utils.py +++ b/src/tests/cucumber/features/steps/common_steps/simulator_utils.py @@ -28,10 +28,14 @@ def init_simu(context): def build_antares_solver_command(context): command = [context.config.userdata["antares-solver"], "-i", str(context.study_path)] - solver = "sirius" - if "solver" in context.config.userdata: - solver = context.config.userdata["solver"] - command.append('--solver=' + solver) + linearSolver = "sirius" + quadraticSolver = "sirius" + if "linear-solver" in context.config.userdata: + linearSolver = context.config.userdata["linear-solver"] + command.append('--linear-solver=' + linearSolver) + if "quadratic-solver" in context.config.userdata: + quadraticSolver = context.config.userdata["quadratic-solver"] + command.append('--quadratic-solver=' + quadraticSolver) if context.named_mps_problems: command.append('--named-mps-problems') diff --git a/src/tests/end-to-end/binding_constraints/test_binding_constraints.cpp b/src/tests/end-to-end/binding_constraints/test_binding_constraints.cpp index 10f91f2a40..70946bc22a 100644 --- a/src/tests/end-to-end/binding_constraints/test_binding_constraints.cpp +++ b/src/tests/end-to-end/binding_constraints/test_binding_constraints.cpp @@ -38,6 +38,8 @@ struct StudyForBCTest: public StudyBuilder // Data members AreaLink* link = nullptr; + Area* area1 = nullptr; + Area* area2 = nullptr; std::shared_ptr cluster; std::shared_ptr BC; }; @@ -46,8 +48,8 @@ StudyForBCTest::StudyForBCTest() { simulationBetweenDays(0, 7); - Area* area1 = addAreaToStudy("Area 1"); - Area* area2 = addAreaToStudy("Area 2"); + area1 = addAreaToStudy("Area 1"); + area2 = addAreaToStudy("Area 2"); TimeSeriesConfigurer(area1->load.series.timeSeries).setColumnCount(1).fillColumnWith(0, 0); @@ -276,6 +278,7 @@ BOOST_FIXTURE_TEST_CASE(On_year_9__RHS_TS_number_4_is_taken_into_account, StudyW OutputRetriever output(simulation->rawSimu()); BOOST_TEST(output.flow(link).hour(0) == 40., tt::tolerance(0.001)); + BOOST_TEST(output.flow(link).hour(0) == 40., tt::tolerance(0.001)); } BOOST_FIXTURE_TEST_CASE(On_year_9__RHS_TS_number_4_out_of_bound_use_random_fallback_to_Oth_column, diff --git a/src/tests/end-to-end/simple_study/simple-study.cpp b/src/tests/end-to-end/simple_study/simple-study.cpp index dbc29c2ae4..dfff6c391d 100644 --- a/src/tests/end-to-end/simple_study/simple-study.cpp +++ b/src/tests/end-to-end/simple_study/simple-study.cpp @@ -201,7 +201,7 @@ BOOST_FIXTURE_TEST_CASE(milp_two_mc_single_unit_single_scenario, StudyFixture) // Use OR-Tools / COIN for MILP auto& p = study->parameters; p.unitCommitment.ucMode = ucMILP; - p.optOptions.ortoolsSolver = "coin"; + p.optOptions.linearSolver = "coin"; simulation->create(); simulation->run(); @@ -229,7 +229,7 @@ BOOST_FIXTURE_TEST_CASE(milp_two_mc_two_unit_single_scenario, StudyFixture) // Use OR-Tools / COIN for MILP auto& p = study->parameters; p.unitCommitment.ucMode = ucMILP; - p.optOptions.ortoolsSolver = "coin"; + p.optOptions.linearSolver = "coin"; simulation->create(); simulation->run(); diff --git a/src/tests/src/api_internal/test_api.cpp b/src/tests/src/api_internal/test_api.cpp index 47034cb19d..bb0897c278 100644 --- a/src/tests/src/api_internal/test_api.cpp +++ b/src/tests/src/api_internal/test_api.cpp @@ -32,7 +32,7 @@ #include "API.h" #include "in-memory-study.h" -class InMemoryStudyLoader: public Antares::IStudyLoader +class InMemoryStudyLoader: public IStudyLoader { public: explicit InMemoryStudyLoader(bool success = true): @@ -40,7 +40,7 @@ class InMemoryStudyLoader: public Antares::IStudyLoader { } - [[nodiscard]] std::unique_ptr load() const override + [[nodiscard]] std::unique_ptr load() const override { if (!success_) { @@ -60,7 +60,7 @@ class InMemoryStudyLoader: public Antares::IStudyLoader BOOST_AUTO_TEST_CASE(api_run_contains_antares_problem) { - Antares::API::APIInternal api; + API::APIInternal api; auto study_loader = std::make_unique(); auto results = api.run(*study_loader, {}, {}); @@ -70,7 +70,7 @@ BOOST_AUTO_TEST_CASE(api_run_contains_antares_problem) BOOST_AUTO_TEST_CASE(result_failure_when_study_is_null) { - Antares::API::APIInternal api; + API::APIInternal api; auto study_loader = std::make_unique(false); auto results = api.run(*study_loader, {}, {}); @@ -80,7 +80,7 @@ BOOST_AUTO_TEST_CASE(result_failure_when_study_is_null) // Test where data in problems are consistant with data in study BOOST_AUTO_TEST_CASE(result_contains_problems) { - Antares::API::APIInternal api; + API::APIInternal api; auto study_loader = std::make_unique(); auto results = api.run(*study_loader, {}, {}); @@ -89,14 +89,16 @@ BOOST_AUTO_TEST_CASE(result_contains_problems) BOOST_CHECK_EQUAL(results.antares_problems.weeklyProblems.size(), 52); } -// Test where data in problems are consistant with data in study +// Test where data in problems are consistent with data in study BOOST_AUTO_TEST_CASE(result_with_ortools_coin) { - Antares::API::APIInternal api; + API::APIInternal api; auto study_loader = std::make_unique(); - const Antares::Solver::Optimization::OptimizationOptions opt{.ortoolsSolver = "coin", - .solverLogs = false, - .solverParameters = ""}; + const Solver::Optimization::OptimizationOptions opt{.linearSolver = "coin", + .quadraticSolver = "sirius", + .linearSolverParameters = "", + .quadraticSolverParameters = "", + .solverLogs = false}; auto results = api.run(*study_loader, {}, opt); @@ -106,17 +108,21 @@ BOOST_AUTO_TEST_CASE(result_with_ortools_coin) } // Test where we use an invalid OR-Tools solver -BOOST_AUTO_TEST_CASE(invalid_ortools_solver) +BOOST_AUTO_TEST_CASE(invalid_ortools_linear_solver) { - Antares::API::APIInternal api; + API::APIInternal api; auto study_loader = std::make_unique(); - const Antares::Solver::Optimization::OptimizationOptions opt{ - .ortoolsSolver = "this-solver-does-not-exist", - .solverLogs = true, - .solverParameters = ""}; + const Solver::Optimization::OptimizationOptions opt{ + .linearSolver = "this-solver-does-not-exist", + .quadraticSolver = "sirius", + .linearSolverParameters = "", + .quadraticSolverParameters = "", + .solverLogs = true}; auto shouldThrow = [&api, &study_loader, &opt] { return api.run(*study_loader, {}, opt); }; - BOOST_CHECK_EXCEPTION(shouldThrow(), - std::invalid_argument, - checkMessage("Solver this-solver-does-not-exist not found")); + BOOST_CHECK_EXCEPTION( + shouldThrow(), + std::invalid_argument, + checkMessage( + "Solver this-solver-does-not-exist not supported for linear problems optimization.")); } diff --git a/src/tests/src/libs/antares/study/parameters/parameters-tests.cpp b/src/tests/src/libs/antares/study/parameters/parameters-tests.cpp index 6015fcbbc0..72ee23e50e 100644 --- a/src/tests/src/libs/antares/study/parameters/parameters-tests.cpp +++ b/src/tests/src/libs/antares/study/parameters/parameters-tests.cpp @@ -61,12 +61,14 @@ BOOST_FIXTURE_TEST_CASE(reset, Fixture) BOOST_CHECK_EQUAL(p.simulationDays.first, 0); BOOST_CHECK_EQUAL(p.nbTimeSeriesThermal, 1); BOOST_CHECK_EQUAL(p.synthesis, true); - BOOST_CHECK_EQUAL(p.optOptions.ortoolsSolver, "sirius"); + BOOST_CHECK_EQUAL(p.optOptions.linearSolver, "sirius"); + BOOST_CHECK_EQUAL(p.optOptions.quadraticSolver, "sirius"); } BOOST_FIXTURE_TEST_CASE(loadValid, Fixture) { - options.optOptions.ortoolsSolver = "xpress"; + options.optOptions.linearSolver = "xpress"; + options.optOptions.quadraticSolver = "scip"; writeValidFile(); p.loadFromFile(path.string(), version); @@ -76,7 +78,8 @@ BOOST_FIXTURE_TEST_CASE(loadValid, Fixture) BOOST_CHECK_EQUAL(p.nbYears, 5); BOOST_CHECK_EQUAL(p.seed[seedTsGenThermal], 5489); BOOST_CHECK_EQUAL(p.include.reserve.dayAhead, true); - BOOST_CHECK_EQUAL(p.optOptions.ortoolsSolver, "xpress"); + BOOST_CHECK_EQUAL(p.optOptions.linearSolver, "xpress"); + BOOST_CHECK_EQUAL(p.optOptions.quadraticSolver, "scip"); } BOOST_FIXTURE_TEST_CASE(fixBadValue, Fixture) diff --git a/src/tests/src/solver/utils/CMakeLists.txt b/src/tests/src/solver/utils/CMakeLists.txt index e93e01b496..c251d9306d 100644 --- a/src/tests/src/solver/utils/CMakeLists.txt +++ b/src/tests/src/solver/utils/CMakeLists.txt @@ -1,10 +1,13 @@ include(${CMAKE_SOURCE_DIR}/tests/macros.cmake) -add_boost_test(tests-basis-status - SRC - basis_status.cpp - INCLUDE - "${CMAKE_SOURCE_DIR}/solver/utils" - LIBS - ortools::ortools - Antares::solverUtils) +add_boost_test(tests-solver-utils + SRC + test_main.cpp + basis_status.cpp + test_ortools_quadratic_wrapper.cpp + INCLUDE + "${CMAKE_SOURCE_DIR}/solver/utils" + LIBS + ortools::ortools + Antares::solverUtils + test_utils_unit) diff --git a/src/tests/src/solver/utils/basis_status.cpp b/src/tests/src/solver/utils/basis_status.cpp index 292dc56c3c..ed1c03ea1a 100644 --- a/src/tests/src/solver/utils/basis_status.cpp +++ b/src/tests/src/solver/utils/basis_status.cpp @@ -18,8 +18,6 @@ * You should have received a copy of the Mozilla Public Licence 2.0 * along with Antares_Simulator. If not, see . */ -#define BOOST_TEST_MODULE test adequacy patch functions - #define WIN32_LEAN_AND_MEAN #include diff --git a/src/tests/src/solver/utils/test_main.cpp b/src/tests/src/solver/utils/test_main.cpp new file mode 100644 index 0000000000..603599dd88 --- /dev/null +++ b/src/tests/src/solver/utils/test_main.cpp @@ -0,0 +1,26 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 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 + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#define BOOST_TEST_MODULE test solver utils + +#define WIN32_LEAN_AND_MEAN + +#include diff --git a/src/tests/src/solver/utils/test_ortools_quadratic_wrapper.cpp b/src/tests/src/solver/utils/test_ortools_quadratic_wrapper.cpp new file mode 100644 index 0000000000..c968e980db --- /dev/null +++ b/src/tests/src/solver/utils/test_ortools_quadratic_wrapper.cpp @@ -0,0 +1,341 @@ +/* + * Copyright 2007-2025, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 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 + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ +#define WIN32_LEAN_AND_MEAN + +#include +#include + +#include + +#include +#include "antares/solver/optimisation/opt_fonctions.h" + +#include "spx_constantes_externes.h" +static double tolerance = 1e-6; + +struct QpFixture +{ + QpFixture() + { + // We only support PDLP solver for now + options.quadraticSolver = "pdlp"; + // Init empty problem + problemeAResoudre.NombreDeVariables = 0; + problemeAResoudre.NombreDeContraintes = 0; + problemeAResoudre.Sens = {}; + problemeAResoudre.IndicesDebutDeLigne = {}; + problemeAResoudre.NombreDeTermesDesLignes = {}; + problemeAResoudre.CoefficientsDeLaMatriceDesContraintes = {}; + problemeAResoudre.IndicesColonnes = {}; + problemeAResoudre.IncrementDAllocationMatriceDesContraintes = 0; + problemeAResoudre.NombreDeTermesDansLaMatriceDesContraintes = 0; + problemeAResoudre.CoutQuadratique = {}; + problemeAResoudre.CoutLineaire = {}; + problemeAResoudre.TypeDeVariable = {}; + problemeAResoudre.Xmin = {}; + problemeAResoudre.Xmax = {}; + problemeAResoudre.SecondMembre = {}; + problemeAResoudre.AdresseOuPlacerLaValeurDesVariablesOptimisees = {}; + problemeAResoudre.X = {}; + problemeAResoudre.AdresseOuPlacerLaValeurDesCoutsMarginaux = {}; + problemeAResoudre.CoutsMarginauxDesContraintes = {}; + problemeAResoudre.AdresseOuPlacerLaValeurDesCoutsReduits = {}; + problemeAResoudre.CoutsReduits = {}; + problemeAResoudre.ExistenceDUneSolution = NON_PI; + problemeAResoudre.NomDesVariables = {}; + problemeAResoudre.NomDesContraintes = {}; + problemeAResoudre.VariablesEntieres = {}; + } + + PROBLEME_ANTARES_A_RESOUDRE problemeAResoudre; + std::vector primals; + std::vector duals; + std::vector reducedCosts; + OptimizationOptions options; + + void solve() + { + problemeAResoudre.X.resize(problemeAResoudre.NombreDeVariables); + primals.resize(problemeAResoudre.NombreDeVariables); + problemeAResoudre.CoutsMarginauxDesContraintes.resize( + problemeAResoudre.NombreDeContraintes); + duals.resize(problemeAResoudre.NombreDeContraintes); + problemeAResoudre.CoutsReduits.resize(problemeAResoudre.NombreDeVariables); + reducedCosts.resize(problemeAResoudre.NombreDeVariables); + for (int i = 0; i < problemeAResoudre.NombreDeVariables; ++i) + { + problemeAResoudre.AdresseOuPlacerLaValeurDesVariablesOptimisees.emplace_back( + &primals[i]); + problemeAResoudre.AdresseOuPlacerLaValeurDesCoutsReduits.emplace_back(&reducedCosts[i]); + } + for (int i = 0; i < problemeAResoudre.NombreDeContraintes; ++i) + { + problemeAResoudre.AdresseOuPlacerLaValeurDesCoutsMarginaux.emplace_back(&duals[i]); + } + SolveQuadraticProblemWithOrtools(options, &problemeAResoudre); + } + + void addVar(const std::string& name, double lb, double ub, double linObj, double quadObj) + { + ++problemeAResoudre.NombreDeVariables; + problemeAResoudre.NomDesVariables.emplace_back(name); + problemeAResoudre.VariablesEntieres.emplace_back(false); + problemeAResoudre.Xmin.emplace_back(lb); + problemeAResoudre.Xmax.emplace_back(ub); + problemeAResoudre.CoutQuadratique.emplace_back(quadObj); + problemeAResoudre.CoutLineaire.emplace_back(linObj); + int type = VARIABLE_NON_BORNEE; + if (std::isfinite(lb) && std::isfinite(ub)) + { + type = VARIABLE_BORNEE_DES_DEUX_COTES; + } + else if (std::isfinite(lb)) + { + type = VARIABLE_BORNEE_INFERIEUREMENT; + } + else if (std::isfinite(ub)) + { + type = VARIABLE_BORNEE_SUPERIEUREMENT; + } + problemeAResoudre.TypeDeVariable.emplace_back(type); + } + + void addConstr(const std::string& name, + double lb, + double ub, + const std::vector& vars, + const std::vector& coefs) + { + ++problemeAResoudre.NombreDeContraintes; + problemeAResoudre.NomDesContraintes.emplace_back(name); + std::string sens; + if (lb == ub) + { + problemeAResoudre.Sens += "="; + problemeAResoudre.SecondMembre.emplace_back(lb); + } + else if (std::isinf(lb)) + { + problemeAResoudre.Sens += "<"; + problemeAResoudre.SecondMembre.emplace_back(ub); + } + else if (std::isinf(ub)) + { + problemeAResoudre.Sens += ">"; + problemeAResoudre.SecondMembre.emplace_back(lb); + } + else + { + throw std::invalid_argument( + "Constraint lb and ub must be equal, or one of them infinite"); + } + int iStart = problemeAResoudre.CoefficientsDeLaMatriceDesContraintes.size(); + problemeAResoudre.IndicesDebutDeLigne.emplace_back(iStart); + problemeAResoudre.NombreDeTermesDesLignes.emplace_back(coefs.size()); + for (int i = 0; i < coefs.size(); ++i) + { + problemeAResoudre.IndicesColonnes[iStart + i] = vars[i]; + problemeAResoudre.CoefficientsDeLaMatriceDesContraintes[iStart + i] = coefs[i]; + } + } + + void checkPrimalSolution(const std::vector& expected) + { + BOOST_TEST(problemeAResoudre.X == expected, + boost::test_tools::tolerance(tolerance) + << "comparison failed" << boost::test_tools::per_element()); + BOOST_TEST(primals == expected, + boost::test_tools::tolerance(tolerance) + << "comparison failed" << boost::test_tools::per_element()); + } + + void checkDualSolution(const std::vector& expected) + { + BOOST_TEST(problemeAResoudre.CoutsMarginauxDesContraintes == expected, + boost::test_tools::tolerance(tolerance) + << "comparison failed" << boost::test_tools::per_element()); + BOOST_TEST(duals == expected, + boost::test_tools::tolerance(tolerance) + << "comparison failed" << boost::test_tools::per_element()); + } + + void checkReducedCosts(const std::vector& expected) + { + BOOST_TEST(problemeAResoudre.CoutsReduits == expected, + boost::test_tools::tolerance(tolerance) + << "comparison failed" << boost::test_tools::per_element()); + BOOST_TEST(reducedCosts == expected, + boost::test_tools::tolerance(tolerance) + << "comparison failed" << boost::test_tools::per_element()); + } + + void checkAllNan(const std::vector& actual) + { + for (int i = 0; i < actual.size(); ++i) + { + BOOST_TEST(std::isnan(actual[i])); + } + } +}; + +BOOST_AUTO_TEST_SUITE(tests_on_ortools_quadratic_wrapper) + +BOOST_FIXTURE_TEST_CASE(solver_not_supported, QpFixture) +{ + options.quadraticSolver = "sirius"; + BOOST_CHECK_EXCEPTION( + solve(), + std::invalid_argument, + checkMessage( + "Solver sirius is not supported for quadratic problems optimization through MathOpt.")); + + options.quadraticSolver = "scip"; + BOOST_CHECK_EXCEPTION( + solve(), + std::invalid_argument, + checkMessage( + "Solver scip is not supported for quadratic problems optimization through MathOpt.")); +} + +BOOST_FIXTURE_TEST_CASE(simple_qp_one_var, QpFixture) +{ + // minimize(x * x - 0.5 * x) + // such that 0 <= x <= 1 + // expected optimal value of x : 0.25 + addVar("x", 0, 1, -0.5, 1); + solve(); + BOOST_CHECK_EQUAL(OUI_PI, problemeAResoudre.ExistenceDUneSolution); + checkPrimalSolution({0.25}); + checkDualSolution({}); + checkReducedCosts({0}); +} + +BOOST_FIXTURE_TEST_CASE(simple_qp_two_vars_1, QpFixture) +{ + // Primal: + // min 2x_0^2 + 0.5x_1^2 - x_0 - x_1 + 5 + // s.t. -inf <= x_0 + x_1 <= 1 + // 1 <= x_0 <= 2 + // -2 <= x_1 <= 4 + // + // Optimal solution: x* = (1, 0). + // + // Dual : + // max -2x_0^2 - 0.5x_1^2 + y_0 + min{r_0, 2r_0} + min{-2r_1, 4r_1} + 5 + // s.t. y_0 + r_0 = 4x_0 - 1 + // y_0 + r_1 = x_1 - 1 + // y_0 <= 0 + // + // Optimal solution: x* = (1, 0), y* = (-1), r* = (4, 0). + addVar("x0", 1, 2, -1, 2); + addVar("x1", -2, 4, -1, 0.5); + addConstr("c1", -std::numeric_limits::infinity(), 1, {0, 1}, {1, 1}); + solve(); + BOOST_CHECK_EQUAL(OUI_PI, problemeAResoudre.ExistenceDUneSolution); + checkPrimalSolution({1, 0}); + checkDualSolution({-1}); + checkReducedCosts({4, 0}); +} + +BOOST_FIXTURE_TEST_CASE(simple_qp_two_vars_2, QpFixture) +{ + // Primal: + // min 0.5x_0^2 + 0.5x_1^2 - 3x_0 - x_1 + // s.t. 2 <= x_0 - x_1 <= 2 + // 0 <= x_0 <= inf + // 0 <= x_1 <= inf + // + // Optimal solution: x* = (3, 1). + // + // Dual (go/mathopt-qp-dual): + // max -0.5x_0^2 - 0.5x_1^2 + 2y_0 + // s.t. y_0 + r_0 = x_0 - 3 + // -y_0 + r_1 = x_1 - 1 + // r_0 >= 0 + // r_1 >= 0 + // + // Optimal solution: x* = (3, 1), y* = (0), r* = (0, 0). + addVar("x0", 0, std::numeric_limits::infinity(), -3, 0.5); + addVar("x1", 0, std::numeric_limits::infinity(), -1, 0.5); + addConstr("c1", 2, 2, {0, 1}, {1, -1}); + solve(); + BOOST_CHECK_EQUAL(OUI_PI, problemeAResoudre.ExistenceDUneSolution); + checkPrimalSolution({3, 1}); + checkDualSolution({0}); + checkReducedCosts({0, 0}); +} + +BOOST_FIXTURE_TEST_CASE(infeasible_qp, QpFixture) +{ + // minimize(x * x - 0.5 * x) + // such that -inf <= x <= 1 + // and x >= 3 + addVar("x", -std::numeric_limits::infinity(), 1, -0.5, 1); + addConstr("c1", 3, std::numeric_limits::infinity(), {0}, {1}); + solve(); + BOOST_CHECK_EQUAL(NON_PI, problemeAResoudre.ExistenceDUneSolution); + checkAllNan(problemeAResoudre.X); + checkAllNan(primals); + checkAllNan(problemeAResoudre.CoutsMarginauxDesContraintes); + checkAllNan(duals); + checkAllNan(problemeAResoudre.CoutsReduits); + checkAllNan(reducedCosts); +} + +BOOST_FIXTURE_TEST_CASE(unbounded_qp, QpFixture) +{ + // minimize(x) + // such that -inf <= x <= inf + addVar("x", + -std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + 1, + 0); + solve(); + BOOST_CHECK_EQUAL(NON_PI, problemeAResoudre.ExistenceDUneSolution); + checkAllNan(problemeAResoudre.X); + checkAllNan(primals); + checkAllNan(problemeAResoudre.CoutsMarginauxDesContraintes); + checkAllNan(duals); + checkAllNan(problemeAResoudre.CoutsReduits); + checkAllNan(reducedCosts); +} + +BOOST_FIXTURE_TEST_CASE(invalid_variable_type, QpFixture) +{ + addVar("x", -std::numeric_limits::infinity(), 1, -0.5, 1); + problemeAResoudre.TypeDeVariable[0] = 15; + BOOST_CHECK_EXCEPTION(solve(), + std::invalid_argument, + checkMessage("Unknown variable type: 15")); +} + +BOOST_FIXTURE_TEST_CASE(invalid_constraint_sense, QpFixture) +{ + addVar("x", -std::numeric_limits::infinity(), 1, -0.5, 1); + addConstr("c1", 3, std::numeric_limits::infinity(), {0}, {1}); + problemeAResoudre.Sens = "+"; + BOOST_CHECK_EXCEPTION(solve(), + std::invalid_argument, + checkMessage("Expected constraint sense to be =, <, or >, but was: +")); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/ui/simulator/windows/simulation/run.cpp b/src/ui/simulator/windows/simulation/run.cpp index 086a405e08..04498d3cc2 100644 --- a/src/ui/simulator/windows/simulation/run.cpp +++ b/src/ui/simulator/windows/simulation/run.cpp @@ -20,37 +20,37 @@ */ #include "run.h" -#include -#include -#include -#include -#include -#include -#include +#include #include +#include #include #include #include +#include #include -#include -#include "../../toolbox/components/wizardheader.h" -#include "../../toolbox/components/button.h" -#include +#include +#include +#include +#include +#include -#include "../../toolbox/validator.h" -#include "../../toolbox/create.h" -#include "../../application/study.h" -#include "../../application/main/main.h" -#include "../../application/menus.h" -#include "../../windows/message.h" -#include "../../toolbox/system/diskfreespace.hxx" #include +#include +#include #include "antares/antares/Enum.hpp" #include "antares/solver/utils/ortools_utils.h" -#include +#include "../../application/main/main.h" +#include "../../application/menus.h" +#include "../../application/study.h" +#include "../../toolbox/components/button.h" +#include "../../toolbox/components/wizardheader.h" +#include "../../toolbox/create.h" +#include "../../toolbox/system/diskfreespace.hxx" +#include "../../toolbox/validator.h" +#include "../../windows/message.h" using namespace Yuni; @@ -69,22 +69,36 @@ static const Solver::Feature featuresAlias[featuresCount] = {Solver::standard, S static wxString TimeSeriesToWxString(uint m) { if (!m) + { return wxT("none"); + } wxString r; if (m & Data::timeSeriesLoad) + { r /*<< (r.empty() ? wxEmptyString : wxT(", "))*/ << wxT("load"); + } if (m & Data::timeSeriesHydro) + { r << (r.empty() ? wxEmptyString : wxT(", ")) << wxT("hydro"); + } if (m & Data::timeSeriesWind) + { r << (r.empty() ? wxEmptyString : wxT(", ")) << wxT("wind"); + } if (m & Data::timeSeriesThermal) + { r << (r.empty() ? wxEmptyString : wxT(", ")) << wxT("thermal"); + } if (r.empty()) + { return wxT("none"); + } else + { return r; + } } static inline void UpdateLabel(bool& guiUpdated, wxStaticText* label, const wxString& text) @@ -96,19 +110,21 @@ static inline void UpdateLabel(bool& guiUpdated, wxStaticText* label, const wxSt } } -class ResourcesInfoTimer final : public wxTimer +class ResourcesInfoTimer final: public wxTimer { public: - ResourcesInfoTimer(Run& form) : wxTimer(), pForm(form) + ResourcesInfoTimer(Run& form): + wxTimer(), + pForm(form) { } + virtual ~ResourcesInfoTimer() { } void Notify() override { - } private: @@ -127,8 +143,9 @@ void Run::gridAppend(wxFlexGridSizer& sizer, } else { - auto* t = Antares::Component::CreateLabel( - pBigDaddy, wxString(wxT(" ")) << title << wxT(" "), true); + auto* t = Antares::Component::CreateLabel(pBigDaddy, + wxString(wxT(" ")) << title << wxT(" "), + true); t->Enable(false); sizer.Add(t, 0, wxRIGHT | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); } @@ -181,16 +198,16 @@ void Run::gridAppend(wxFlexGridSizer& sizer, const wxString& key, const wxString gridAppend(sizer, wxEmptyString, key, value); } -Run::Run(wxWindow* parent, bool preproOnly) : - wxDialog(parent, wxID_ANY, wxString(wxT("Simulation"))), - pSimulationName(nullptr), - pSimulationComments(nullptr), - pIgnoreWarnings(nullptr), - pTimer(nullptr), - pWarnAboutMemoryLimit(false), - pWarnAboutDiskLimit(false), - pAlreadyWarnedNoMCYear(false), - pFeatureIndex(0) +Run::Run(wxWindow* parent, bool preproOnly): + wxDialog(parent, wxID_ANY, wxString(wxT("Simulation"))), + pSimulationName(nullptr), + pSimulationComments(nullptr), + pIgnoreWarnings(nullptr), + pTimer(nullptr), + pWarnAboutMemoryLimit(false), + pWarnAboutDiskLimit(false), + pAlreadyWarnedNoMCYear(false), + pFeatureIndex(0) { assert(parent); @@ -223,8 +240,8 @@ Run::Run(wxWindow* parent, bool preproOnly) : // Informations about the INPUT { - auto* lblMode - = Antares::Component::CreateLabel(pBigDaddy, wxStringFromUTF8(study.header.caption)); + auto* lblMode = Antares::Component::CreateLabel(pBigDaddy, + wxStringFromUTF8(study.header.caption)); wxFont f = lblMode->GetFont(); f.SetWeight(wxFONTWEIGHT_BOLD); lblMode->SetFont(f); @@ -232,7 +249,8 @@ Run::Run(wxWindow* parent, bool preproOnly) : } { auto* lblMode = Antares::Component::CreateLabel( - pBigDaddy, wxStringFromUTF8(Data::SimulationModeToCString(study.parameters.mode))); + pBigDaddy, + wxStringFromUTF8(Data::SimulationModeToCString(study.parameters.mode))); wxFont f = lblMode->GetFont(); f.SetWeight(wxFONTWEIGHT_BOLD); lblMode->SetFont(f); @@ -278,8 +296,9 @@ Run::Run(wxWindow* parent, bool preproOnly) : s->AddSpacer(2); #endif - pPreproOnly = new wxCheckBox( - pBigDaddy, wxID_ANY, wxString(wxT(" Run the time-series generators only "))); + pPreproOnly = new wxCheckBox(pBigDaddy, + wxID_ANY, + wxString(wxT(" Run the time-series generators only "))); pPreproOnly->SetValue(preproOnly); pPreproOnly->Connect(pPreproOnly->GetId(), wxEVT_COMMAND_CHECKBOX_CLICKED, @@ -310,12 +329,12 @@ Run::Run(wxWindow* parent, bool preproOnly) : // Ortools use { // Ortools solver selection - pTitleOrtoolsSolverCombox - = Antares::Component::CreateLabel(pBigDaddy, wxT("Ortools solver : ")); + pTitleOrtoolsSolverCombox = Antares::Component::CreateLabel(pBigDaddy, + wxT("Ortools solver : ")); pOrtoolsSolverCombox = new wxComboBox(pBigDaddy, wxID_ANY, "sirius"); - std::list solverList = getAvailableOrtoolsSolverName(); - for (const std::string& solverName : solverList) + std::list solverList = getAvailableSolverNames(SolverClass::LINEAR); + for (const std::string& solverName: solverList) { pOrtoolsSolverCombox->Append(solverName); } @@ -352,8 +371,10 @@ Run::Run(wxWindow* parent, bool preproOnly) : sizer = new wxBoxSizer(wxHORIZONTAL); title = Antares::Component::CreateLabel(pBigDaddy, wxT("Memory (estimation) : ")); pLblEstimation = Antares::Component::CreateLabel(pBigDaddy, wxEmptyString); - pLblEstimationAvailable - = Antares::Component::CreateLabel(pBigDaddy, wxEmptyString, false, true); + pLblEstimationAvailable = Antares::Component::CreateLabel(pBigDaddy, + wxEmptyString, + false, + true); sizer->Add(pLblEstimation, 0, wxALL | wxEXPAND); sizer->AddSpacer(10); sizer->Add(pLblEstimationAvailable, 0, wxALL | wxEXPAND); @@ -362,8 +383,10 @@ Run::Run(wxWindow* parent, bool preproOnly) : sizer = new wxBoxSizer(wxHORIZONTAL); title = Antares::Component::CreateLabel(pBigDaddy, wxT("Disk (estimation) : ")); pLblDiskEstimation = Antares::Component::CreateLabel(pBigDaddy, wxEmptyString); - pLblDiskEstimationAvailable - = Antares::Component::CreateLabel(pBigDaddy, wxEmptyString, false, true); + pLblDiskEstimationAvailable = Antares::Component::CreateLabel(pBigDaddy, + wxEmptyString, + false, + true); sizer->Add(pLblDiskEstimation, 0, wxALL | wxEXPAND); sizer->AddSpacer(10); sizer->Add(pLblDiskEstimationAvailable, 0, wxALL | wxEXPAND); @@ -413,9 +436,13 @@ Run::Run(wxWindow* parent, bool preproOnly) : Run::~Run() { if (pNbCores) + { delete pNbCores; + } if (pTitleSimCores) + { delete pTitleSimCores; + } if (pTimer) { @@ -456,11 +483,15 @@ bool Run::createCommentsFile(String& filename) const filename.clear(); if (not pSimulationComments) + { return false; + } wxString cmt = pSimulationComments->GetValue(); if (cmt.empty()) + { return false; + } String content; wxStringToString(cmt, content); @@ -472,7 +503,9 @@ bool Run::createCommentsFile(String& filename) const String temporary; if (not IO::Directory::System::Temporary(temporary)) + { return false; + } auto processID = ProcessID(); @@ -500,7 +533,9 @@ int Run::checkForLowResources() #endif if (not pWarnAboutMemoryLimit and not pWarnAboutDiskLimit) + { return 0; + } auto& mainFrm = *Forms::ApplWnd::Instance(); @@ -508,16 +543,25 @@ int Run::checkForLowResources() if (pWarnAboutMemoryLimit) { if (pWarnAboutDiskLimit) + { msg = wxT("Memory and Disk"); + } else + { msg = wxT("Memory"); + } } else + { msg = wxT("Disk"); + } msg << wxT(" almost full"); - Window::Message message( - &mainFrm, wxT("Simulation"), msg, wxT("Try anyway ?"), "images/misc/warning.png"); + Window::Message message(&mainFrm, + wxT("Simulation"), + msg, + wxT("Try anyway ?"), + "images/misc/warning.png"); message.add(Window::Message::btnContinue); message.add(Window::Message::btnCancel, true); if (message.showModal() != Window::Message::btnContinue) @@ -531,7 +575,9 @@ int Run::checkForLowResources() void Run::onRun(void*) { if (not CurrentStudyIsValid()) + { return; + } bool canNotifyUserForLowResources = true; switch (checkForLowResources()) @@ -568,13 +614,19 @@ void Run::onRun(void*) } if (canNotifyUserForLowResources and 1 == checkForLowResources()) + { return; + } // Updating the display if (pTimer) + { pTimer->Stop(); + } if (!(!pThread)) + { pThread->stop(); + } this->Enable(false); Refresh(); @@ -605,9 +657,13 @@ void Run::onRun(void*) String commentFile; if (not createCommentsFile(commentFile)) + { commentFile.clear(); + } else + { logs.debug() << "using comment file: " << commentFile; + } Hide(); @@ -701,7 +757,9 @@ void Run::updateNbCores() wxString s = wxT(""); s << nbCoresRaw; if (minNbCores < nbCoresRaw) + { s << L" (smallest batch size : " << minNbCores << L")"; + } pNbCores->SetLabel(s); pBtnRun->Enable(true); @@ -734,12 +792,12 @@ void Run::prepareMenuSolverMode(Antares::Component::Button&, wxMenu& menu, void* for (uint i = 0; i != featuresCount; ++i) { const wxMenuItem* it = Menu::CreateItem(&menu, - wxID_ANY, - featuresNames[i], - "images/16x16/empty.png", - wxEmptyString, - wxITEM_NORMAL, - (i == 0)); + wxID_ANY, + featuresNames[i], + "images/16x16/empty.png", + wxEmptyString, + wxITEM_NORMAL, + (i == 0)); pMappingSolverMode[it->GetId()] = i; menu.Connect(it->GetId(), @@ -754,7 +812,9 @@ void Run::onSelectMode(wxCommandEvent& evt) { pFeatureIndex = pMappingSolverMode[evt.GetId()]; if (pFeatureIndex >= featuresCount) + { pFeatureIndex = 0; + } // In case of either default mode, MC years parallel computation is disabled (nb // of cores is set to 1) @@ -796,5 +856,3 @@ void Run::onInternalMotion(wxMouseEvent&) } } // namespace Antares::Window::Simulation - -