diff --git a/src/optimisation/linear-problem-api/include/antares/optimisation/linear-problem-api/linearProblem.h b/src/optimisation/linear-problem-api/include/antares/optimisation/linear-problem-api/linearProblem.h index 9934f010622..7a72cb39e8f 100644 --- a/src/optimisation/linear-problem-api/include/antares/optimisation/linear-problem-api/linearProblem.h +++ b/src/optimisation/linear-problem-api/include/antares/optimisation/linear-problem-api/linearProblem.h @@ -92,6 +92,8 @@ class ILinearProblem // Definition of infinity [[nodiscard]] virtual double infinity() const = 0; virtual bool isLP() const = 0; + + virtual double objectiveValue() const = 0; }; } // namespace Antares::Optimisation::LinearProblemApi diff --git a/src/optimisation/linear-problem-mpsolver-impl/include/antares/optimisation/linear-problem-mpsolver-impl/linearProblem.h b/src/optimisation/linear-problem-mpsolver-impl/include/antares/optimisation/linear-problem-mpsolver-impl/linearProblem.h index a945966e259..c20ddb0e379 100644 --- a/src/optimisation/linear-problem-mpsolver-impl/include/antares/optimisation/linear-problem-mpsolver-impl/linearProblem.h +++ b/src/optimisation/linear-problem-mpsolver-impl/include/antares/optimisation/linear-problem-mpsolver-impl/linearProblem.h @@ -105,6 +105,14 @@ class OrtoolsLinearProblem: public LinearProblemApi::ILinearProblem OrtoolsMipSolution* solve(bool verboseSolver) override; + /** + * Return the last solution or solve if no solution exist. + * @param verboseSolver + * @return The solution + */ + OrtoolsMipSolution* solution(bool verboseSolver); + double objectiveValue() const override; + [[nodiscard]] double infinity() const override; [[nodiscard]] bool isLP() const override; diff --git a/src/optimisation/linear-problem-mpsolver-impl/linearProblem.cpp b/src/optimisation/linear-problem-mpsolver-impl/linearProblem.cpp index f04d6654228..5c0a5c8debc 100644 --- a/src/optimisation/linear-problem-mpsolver-impl/linearProblem.cpp +++ b/src/optimisation/linear-problem-mpsolver-impl/linearProblem.cpp @@ -231,6 +231,20 @@ OrtoolsMipSolution* OrtoolsLinearProblem::solve(bool verboseSolver) return solution_.get(); } +OrtoolsMipSolution* OrtoolsLinearProblem::solution(bool verboseSolver) +{ + if (!solution_) + { + solve(verboseSolver); + } + return solution_.get(); +} + +double OrtoolsLinearProblem::objectiveValue() const +{ + return ::getObjectiveValue(mpSolver_); +} + double OrtoolsLinearProblem::infinity() const { return MPSolver::infinity(); diff --git a/src/optimisation/linear-problem-mpsolver-impl/mipSolution.cpp b/src/optimisation/linear-problem-mpsolver-impl/mipSolution.cpp index 1ad52d95d5c..4ac038f2313 100644 --- a/src/optimisation/linear-problem-mpsolver-impl/mipSolution.cpp +++ b/src/optimisation/linear-problem-mpsolver-impl/mipSolution.cpp @@ -26,8 +26,7 @@ namespace Antares::Optimisation::LinearProblemMpsolverImpl { -OrtoolsMipSolution::OrtoolsMipSolution(operations_research::MPSolver::ResultStatus& status, - operations_research::MPSolver* solver): +OrtoolsMipSolution::OrtoolsMipSolution(MPSolver::ResultStatus& status, MPSolver* solver): status_(status), mpSolver_(solver) { @@ -41,13 +40,13 @@ LinearProblemApi::MipStatus OrtoolsMipSolution::getStatus() const { switch (status_) { - case operations_research::MPSolver::ResultStatus::OPTIMAL: + case MPSolver::ResultStatus::OPTIMAL: return LinearProblemApi::MipStatus::OPTIMAL; - case operations_research::MPSolver::ResultStatus::FEASIBLE: + case MPSolver::ResultStatus::FEASIBLE: return LinearProblemApi::MipStatus::FEASIBLE; - case operations_research::MPSolver::ResultStatus::UNBOUNDED: + case MPSolver::ResultStatus::UNBOUNDED: return LinearProblemApi::MipStatus::UNBOUNDED; - case operations_research::MPSolver::ResultStatus::INFEASIBLE: + case MPSolver::ResultStatus::INFEASIBLE: return LinearProblemApi::MipStatus::INFEASIBLE; default: logs.warning() << "Solve returned an error status"; diff --git a/src/solver/optim-model-filler/ComponentFiller.cpp b/src/solver/optim-model-filler/ComponentFiller.cpp index c1760ee4928..33fdb5959fb 100644 --- a/src/solver/optim-model-filler/ComponentFiller.cpp +++ b/src/solver/optim-model-filler/ComponentFiller.cpp @@ -19,6 +19,7 @@ * along with Antares_Simulator. If not, see . */ +#include #include #include #include @@ -331,7 +332,7 @@ void ComponentFiller::addVariables(const LinearProblemApi::FillContext& ctx) } void ComponentFiller::addStaticConstraint(const LinearConstraint& linear_constraint, - const std::string& constraint_id) + const std::string& constraint_id) const { auto* ct = optimEntityContainer_.Problem().addConstraint(linear_constraint.lb[0], linear_constraint.ub[0], @@ -348,7 +349,7 @@ void ComponentFiller::addStaticConstraint(const LinearConstraint& linear_constra void ComponentFiller::addTimeDependentConstraints(const LinearConstraint& linear_constraints, const std::string& constraint_id, - const LinearProblemApi::FillContext& ctx) + const LinearProblemApi::FillContext& ctx) const { auto& pb = optimEntityContainer_.Problem(); const auto dims = getDimensions(ctx); @@ -396,13 +397,12 @@ void ComponentFiller::addConstraints(const LinearProblemApi::FillContext& ctx) } } -void ComponentFiller::addStaticObjective( - const Optimization::TimeDependentLinearExpression& expression) const +void ComponentFiller::addStaticObjective(const Optimization::LinearExpression& expression) const { auto& pb = optimEntityContainer_.Problem(); const auto& solverVariables = optimEntityContainer_.getVariables(); - for (const auto& [index, value]: expression[0]) + for (const auto& [index, value]: expression) { pb.setObjectiveCoefficient(solverVariables[index].get(), value); } @@ -413,18 +413,21 @@ void ComponentFiller::addObjectives(const LinearProblemApi::FillContext& ctx) auto* model = component_.getModel(); ReadLinearExpressionVisitor visitor(optimEntityContainer_, ctx, component_); + double objectiveOffset = 0.0; for (const auto& objective: model->Objectives() | locationFilter()) { const auto root_node = objective.expression().RootNode(); - const auto linearExpression = visitor.visitMergeDuplicates(root_node); - const auto variability = getVariability(root_node, component_); if (isTimeDependent(variability)) { throw Error::RuntimeError("Time dependent objectives are not supported in Antares."); } + const auto linearExpression = visitor.visitMergeDuplicates(root_node)[0]; addStaticObjective(linearExpression); + objectiveOffset += linearExpression.constant(); } + auto& pb = optimEntityContainer_.Problem(); + pb.setObjectiveOffset(objectiveOffset); } VariabilityType ComponentFiller::getVariability(const Node* node, const Component& component) const diff --git a/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/ComponentFiller.h b/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/ComponentFiller.h index 8cf71f02f4b..629c5bbbb9f 100644 --- a/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/ComponentFiller.h +++ b/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/ComponentFiller.h @@ -92,13 +92,13 @@ class ComponentFiller: public LinearProblemApi::LinearProblemFiller private: void addStaticConstraint(const LinearConstraint& linear_constraint, - const std::string& constraint_id); + const std::string& constraint_id) const; void addTimeDependentConstraints(const LinearConstraint& linear_constraints, const std::string& constraint_id, - const LinearProblemApi::FillContext& ctx); + const LinearProblemApi::FillContext& ctx) const; - void addStaticObjective(const Optimization::TimeDependentLinearExpression& expression) const; + void addStaticObjective(const Optimization::LinearExpression& expression) const; VariabilityType getVariability(const Nodes::Node* node, const ModelerStudy::SystemModel::Component& component) const; diff --git a/src/solver/optimisation/LegacyFiller.cpp b/src/solver/optimisation/LegacyFiller.cpp index d7c6b9c6c93..63283c2f233 100644 --- a/src/solver/optimisation/LegacyFiller.cpp +++ b/src/solver/optimisation/LegacyFiller.cpp @@ -91,6 +91,7 @@ void LegacyFiller::CreateVariable(unsigned idxVar) const isIntegerVariable, GetVariableName(idxVar)); linearProblem_.setObjectiveCoefficient(var, problemeAResoudre_->CoutLineaire[idxVar]); + // linearProblem_.setObjectiveOffset(problemeAResoudre_->) } void LegacyFiller::CopyVariables() const diff --git a/src/solver/optimisation/include/antares/solver/optimisation/LegacyOrtoolsLinearProblem.h b/src/solver/optimisation/include/antares/solver/optimisation/LegacyOrtoolsLinearProblem.h index 707bcb4fdaf..a9a3483df0f 100644 --- a/src/solver/optimisation/include/antares/solver/optimisation/LegacyOrtoolsLinearProblem.h +++ b/src/solver/optimisation/include/antares/solver/optimisation/LegacyOrtoolsLinearProblem.h @@ -27,7 +27,7 @@ namespace Antares::Optimization { class LegacyOrtoolsLinearProblem final - : public Antares::Optimisation::LinearProblemMpsolverImpl::OrtoolsLinearProblem + : public Optimisation::LinearProblemMpsolverImpl::OrtoolsLinearProblem { public: LegacyOrtoolsLinearProblem(bool isMip, const std::string& solverName): @@ -36,7 +36,7 @@ class LegacyOrtoolsLinearProblem final // nothing else to do } - operations_research::MPSolver* getMpSolver() + MPSolver* getMpSolver() { return MpSolver(); } diff --git a/src/solver/optimisation/opt_appel_solveur_lineaire.cpp b/src/solver/optimisation/opt_appel_solveur_lineaire.cpp index 1b46c49e6bd..a92fdc51d26 100644 --- a/src/solver/optimisation/opt_appel_solveur_lineaire.cpp +++ b/src/solver/optimisation/opt_appel_solveur_lineaire.cpp @@ -176,13 +176,12 @@ static SimplexResult OPT_TryToCallSimplex(const SingleOptimOptions& options, FillContext fillCtx = buildFillContext(problemeHebdo, NumIntervalle); const auto& modelerData = problemeHebdo->modelerData; bool hasModelerData = modelerData != nullptr; - const Optimisation::LinearProblemApi::ILinearProblemData* modelerDataSeries = hasModelerData - ? modelerData - ->dataSeries - .get() - : nullptr; - const Optimisation::ScenarioGroupRepository* modelerScenarioGroupRepository - = hasModelerData ? &modelerData->scenarioGroupRepository : nullptr; + const ILinearProblemData* modelerDataSeries = hasModelerData ? modelerData->dataSeries.get() + : nullptr; + const ScenarioGroupRepository* modelerScenarioGroupRepository = hasModelerData + ? &modelerData + ->scenarioGroupRepository + : nullptr; OptimEntityContainer optimEntityContainer(ortoolsProblem, modelerDataSeries, @@ -332,9 +331,9 @@ bool OPT_AppelDuSimplexe(const SingleOptimOptions& options, FillContext fillCtx = buildFillContext(problemeHebdo, NumIntervalle); const auto& modelerData = problemeHebdo->modelerData; bool hasModelerData = modelerData != nullptr; - const Optimisation::LinearProblemApi::ILinearProblemData* modelerDataSeries - = hasModelerData ? modelerData->dataSeries.get() : nullptr; - const Optimisation::ScenarioGroupRepository* modelerScenarioGroupRepository + const ILinearProblemData* modelerDataSeries = hasModelerData ? modelerData->dataSeries.get() + : nullptr; + const ScenarioGroupRepository* modelerScenarioGroupRepository = hasModelerData ? &modelerData->scenarioGroupRepository : nullptr; OptimEntityContainer optimEntityContainer(infeasibleProblem, diff --git a/src/tests/cucumber/features/solver-features/hybrid_studies.feature b/src/tests/cucumber/features/solver-features/hybrid_studies.feature index 9cfb7b125cb..f605d1ffe3d 100644 --- a/src/tests/cucumber/features/solver-features/hybrid_studies.feature +++ b/src/tests/cucumber/features/solver-features/hybrid_studies.feature @@ -85,4 +85,12 @@ Feature: hybrid (simulator+modeler) studies Given the solver study path is "Antares_Simulator_Tests_NR/invalid-studies/hybrid/Scenario-independent variable" When I run antares simulator Then the simulation fails - And the message "Scenario-independent variables are not supported in hybrid studies" is reported in the logs \ No newline at end of file + And the message "Scenario-independent variables are not supported in hybrid studies" is reported in the logs + + @fast @short + Scenario: Two studies with same structure should have the same objective value at each time step + Given the study path 1 is "Antares_Simulator_Tests_NR/hybrid/14_1/five_steps_hybrid_fixed_load" + And the study path 2 is "Antares_Simulator_Tests_NR/hybrid/14_1/five_steps_hybrid_flexible_load" + When I run antares simulator on all studies + Then all simulations succeed + And for each time step, all studies have the same objective value diff --git a/src/tests/cucumber/features/steps/common_steps/modeler_output_handler.py b/src/tests/cucumber/features/steps/common_steps/modeler_output_handler.py index 5f5ca9e9267..9c45ffa53cb 100644 --- a/src/tests/cucumber/features/steps/common_steps/modeler_output_handler.py +++ b/src/tests/cucumber/features/steps/common_steps/modeler_output_handler.py @@ -19,6 +19,7 @@ def __init__(self, master, subproblem, structure): self.subproblem = subproblem self.structure = structure + class modeler_output_handler: def __init__(self, simulation_table_location, output_location=None): self.simulation_table = modeler_output_handler.__read_simulation_table(simulation_table_location) @@ -54,7 +55,7 @@ def __read_simulation_table(absolute_path) -> pd.DataFrame: df[col] = df[col].astype(float) return df - def get_simulation_table_entry(self, component : str, output : str, block : int, timestep : int, scenario : int): + def get_simulation_table_entry(self, component: str, output: str, block: int, timestep: int, scenario: int): df = self.simulation_table[(self.simulation_table["component"] == component) & (self.simulation_table["output"] == output)] if not pd.isna(block): @@ -68,7 +69,8 @@ def get_simulation_table_entry(self, component : str, output : str, block : int, else: df = df[df["scenario_index"] == scenario] if len(df) != 1: - raise LookupError(f"Simulation table contains {len(df)} row(s) (expected 1) for component '{component}', output '{output}', block '{block}', timestep '{timestep}', scenario '{scenario}'") + raise LookupError( + f"Simulation table contains {len(df)} row(s) (expected 1) for component '{component}', output '{output}', block '{block}', timestep '{timestep}', scenario '{scenario}'") return df["value"].iloc[0] def get_objective_value(self): @@ -76,3 +78,19 @@ def get_objective_value(self): if len(df) != 1: raise LookupError(f"Simulation table contains no or multiple objective values") return df["value"].iloc[0] + + def get_objective_values_by_block(self): + """ + Returns a dictionary mapping block number to objective value. + Each block represents a time step in the optimization. + """ + df = self.simulation_table[(self.simulation_table["output"] == "OBJECTIVE_VALUE")] + if df.empty: + raise LookupError(f"Simulation table contains no objective values") + # Group by block and get the objective value for each block + result = {} + for _, row in df.iterrows(): + block = int(row["block"]) + value = row["value"] + result[block] = value + return result diff --git a/src/tests/cucumber/features/steps/common_steps/solver_steps.py b/src/tests/cucumber/features/steps/common_steps/solver_steps.py index 6dad478eb58..88a3fbaacc5 100644 --- a/src/tests/cucumber/features/steps/common_steps/solver_steps.py +++ b/src/tests/cucumber/features/steps/common_steps/solver_steps.py @@ -389,6 +389,144 @@ def ckeck_log_exists(context, log): raise AssertionError(f"Log '{log}' is not reported in the logs") +def _initialize_multi_study_context(context): + """Initialize context for multiple study simulations""" + if not hasattr(context, 'multi_studies'): + context.multi_studies = [] + + +def _store_simulation_result(context, study_index: int): + """Store simulation results for a given study index""" + result = { + 'index': study_index, + 'path': context.study_path, + 'return_code': context.return_code, + 'logs_out': context.logs_out, + 'logs_err': context.logs_err, + 'output_path': context.output_path, + 'soh': context.soh, + 'moh': context.moh if hasattr(context, 'moh') else None + } + context.multi_studies.append(result) + + +def _run_study_at_index(context, study_index: int, study_path: Path): + """Run a single study and store its results""" + context.study_path = study_path + init_simulation(context) + context.named_mps_problems = False + context.parallel = False + run_simulation(context) + _store_simulation_result(context, study_index) + + +@given('the study path {study_num:d} is "{string}"') +def nth_study_path_is(context, study_num, string): + """Generic step to define the Nth study path""" + _initialize_multi_study_context(context) + study_path = Path(context.config.userdata["resources-path"]) / Path(string.replace("/", os.sep)) + assert study_path.exists(), f"Study path {study_num} ({study_path}) does not exist" + + if not hasattr(context, 'study_paths'): + context.study_paths = [] + + # Extend list if necessary to accommodate the index + while len(context.study_paths) < study_num: + context.study_paths.append(None) + + # Store at index (1-based to 0-based conversion) + context.study_paths[study_num - 1] = study_path + + +@when('I run antares simulator on all studies') +def run_antares_on_all_studies(context): + """Run simulator on all defined studies""" + _initialize_multi_study_context(context) + + assert hasattr(context, 'study_paths'), "No study paths defined" + assert len(context.study_paths) > 0, "No study paths defined" + + # Run all studies + for idx, study_path in enumerate(context.study_paths): + assert study_path is not None, f"Study path at index {idx} is not defined" + _run_study_at_index(context, idx, study_path) + + +@then('all simulations succeed') +def all_simulations_succeed(context): + """Check that all simulations succeeded""" + assert hasattr(context, 'multi_studies'), "No simulations were run" + assert len(context.multi_studies) > 0, "No simulations were run" + + failures = [] + for study in context.multi_studies: + if study['return_code'] != 0: + failures.append( + f"Study {study['index'] + 1} (path: {study['path']}) failed with return code {study['return_code']}: " + f"\nSTDOUT: \n{study['logs_out']} \n STDERR: \n{study['logs_err']}" + ) + + if failures: + raise AssertionError("\n\n".join(failures)) + + +@then('for each time step, all studies have the same objective value') +def compare_objective_values_all_studies(context): + """Compare objective values across all studies to ensure they are identical""" + assert hasattr(context, 'multi_studies'), "No simulations were run" + assert len(context.multi_studies) > 1, f"Need at least 2 studies to compare, found {len(context.multi_studies)}" + + # Collect all objective values + all_objectives = [] + for study in context.multi_studies: + assert study['moh'] is not None, \ + f"Study {study['index'] + 1} (path: {study['path']}) does not have modeler outputs (simulation_table)" + objectives = study['moh'].get_objective_values_by_block() + all_objectives.append({ + 'index': study['index'], + 'path': study['path'], + 'objectives': objectives + }) + + # Use first study as reference + reference = all_objectives[0] + reference_blocks = set(reference['objectives'].keys()) + + # Compare each study against the reference + all_mismatches = [] + for study_data in all_objectives[1:]: + study_blocks = set(study_data['objectives'].keys()) + + # Check blocks match + if reference_blocks != study_blocks: + all_mismatches.append( + f"Study {study_data['index'] + 1} has different blocks. " + f"Reference: {sorted(reference_blocks)}, Study: {sorted(study_blocks)}" + ) + continue + + # Compare values for each block + mismatches = [] + for block in sorted(reference_blocks): + ref_value = reference['objectives'][block] + study_value = study_data['objectives'][block] + if not np.isclose(ref_value, study_value, rtol=1e-9, atol=1e-9): + mismatches.append( + f" Block {block}: reference={ref_value}, study {study_data['index'] + 1}={study_value}, " + f"diff={abs(ref_value - study_value)}" + ) + + if mismatches: + all_mismatches.append( + f"Study {study_data['index'] + 1} (path: {study_data['path']}) differs from reference:\n" + + "\n".join(mismatches) + ) + + if all_mismatches: + error_msg = "Objective values differ between studies:\n\n" + "\n\n".join(all_mismatches) + raise AssertionError(error_msg) + + @then( 'in area "{area}", during year {year:d}, hourly value of "{var_name}" for hour {hour:d} is equal to {expected_value:d}') def check_hourly_variable_value(context, area, year, var_name, hour, expected_value): diff --git a/src/tests/inmemory-modeler/include/inmemory-modeler.h b/src/tests/inmemory-modeler/include/inmemory-modeler.h index 52533965968..760a33037e6 100644 --- a/src/tests/inmemory-modeler/include/inmemory-modeler.h +++ b/src/tests/inmemory-modeler/include/inmemory-modeler.h @@ -83,6 +83,13 @@ struct LinearProblemBuildingFixture const std::vector& constraintsData, Antares::Expressions::Nodes::Node* objective = nullptr); + void createModelWithMultipleObjectives( + const std::string& modelId, + std::vector, + const std::vector& variablesData, + const std::vector& constraintsData, + std::vector objectives); + void createModelWithOneFloatVar(const std::string& modelId, const std::vector& parameterIds, const std::string& varId, @@ -112,10 +119,11 @@ struct LinearProblemBuildingFixture Antares::Expressions::Nodes::Node* multiply(Antares::Expressions::Nodes::Node* node1, Antares::Expressions::Nodes::Node* node2); - - Antares::Expressions::Nodes::Node* negate(Antares::Expressions::Nodes::Node* node); Antares::Expressions::Nodes::Node* add(Antares::Expressions::Nodes::Node* node1, Antares::Expressions::Nodes::Node* node2); + Antares::Expressions::Nodes::Node* Sum(Antares::Expressions::Nodes::Node* node); + + Antares::Expressions::Nodes::Node* negate(Antares::Expressions::Nodes::Node* node); void buildLinearProblem( Antares::Optimisation::LinearProblemApi::FillContext& time_scenario_ctx, diff --git a/src/tests/inmemory-modeler/inmemory-modeler.cpp b/src/tests/inmemory-modeler/inmemory-modeler.cpp index c3ff17ccc13..ed986965696 100644 --- a/src/tests/inmemory-modeler/inmemory-modeler.cpp +++ b/src/tests/inmemory-modeler/inmemory-modeler.cpp @@ -137,6 +137,11 @@ Nodes::Node* LinearProblemBuildingFixture::add(Nodes::Node* node1, Nodes::Node* return nodeRegistry.create(node1, node2); } +Nodes::Node* LinearProblemBuildingFixture::Sum(Nodes::Node* node) +{ + return nodeRegistry.create(node); +} + void LinearProblemBuildingFixture::createModel(const std::string& modelId, const std::vector& parameterIds, const std::vector& variablesData, @@ -199,6 +204,49 @@ void LinearProblemBuildingFixture::createModelWithSystemModelParameter( models[modelId] = std::move(model); } +void LinearProblemBuildingFixture::createModelWithMultipleObjectives( + const std::string& modelId, + std::vector parameters, + const std::vector& variablesData, + const std::vector& constraintsData, + std::vector objectiveNodes) +{ + std::vector variables; + for (const auto& [id, type, lb, ub, timeDependent, scenarioDependent]: variablesData) + { + variables.emplace_back(id, + createExpression(lb, nodeRegistry), + createExpression(ub, nodeRegistry), + type, + fromBool(timeDependent), + fromBool(scenarioDependent)); + } + std::vector constraints; + for (const auto& [id, expression]: constraintsData) + { + constraints.emplace_back(id, createExpression(expression, nodeRegistry)); + } + + std::vector objectives; + int objIndex = 0; + for (auto* objectiveNode: objectiveNodes) + { + objectives.emplace_back("objective_" + std::to_string(objIndex), + createExpression(objectiveNode, nodeRegistry)); + objIndex++; + } + + ModelBuilder model_builder; + model_builder.withId(modelId) + .withParameters(std::move(parameters)) + .withVariables(std::move(variables)) + .withConstraints(std::move(constraints)) + .withObjectives(std::move(objectives)); + + auto model = model_builder.build(); + models[modelId] = std::move(model); +} + void LinearProblemBuildingFixture::createModelWithOneFloatVar( const std::string& modelId, const std::vector& parameterIds, diff --git a/src/tests/src/modeler/UtilMocks.h b/src/tests/src/modeler/UtilMocks.h index 32433f9872d..7a09a8fe9b8 100644 --- a/src/tests/src/modeler/UtilMocks.h +++ b/src/tests/src/modeler/UtilMocks.h @@ -326,6 +326,11 @@ class MockLinearProblem: public Antares::Optimisation::LinearProblemApi::ILinear return !isMinimization(); } + [[nodiscard]] double objectiveValue() const override + { + return 0.0; + } + protected: bool isLP_; std::vector> variables_; diff --git a/src/tests/src/optimisation/linear-problem/testLinearProblemMpsolverImpl.cpp b/src/tests/src/optimisation/linear-problem/testLinearProblemMpsolverImpl.cpp index 10c5b16a1e3..2eebacfb740 100644 --- a/src/tests/src/optimisation/linear-problem/testLinearProblemMpsolverImpl.cpp +++ b/src/tests/src/optimisation/linear-problem/testLinearProblemMpsolverImpl.cpp @@ -259,4 +259,48 @@ BOOST_FIXTURE_TEST_CASE(solve_problem_then_add_new_var___new_var_optimal_value_i BOOST_CHECK_EQUAL(newVar->solutionValue(), 0); } +// New tests for objectiveValue() +BOOST_FIXTURE_TEST_CASE(objectiveValue_default_is_zero, FixtureEmptyProblem) +{ + BOOST_CHECK_EQUAL(pb->objectiveValue(), 0); +} + +BOOST_FIXTURE_TEST_CASE(objectiveValue_with_coeff_but_not_solved_is_zero, FixtureEmptyProblem) +{ + auto* var = pb->addNumVariable(0, 10, "var"); + pb->setObjectiveCoefficient(var, 5); + BOOST_CHECK_EQUAL(pb->objectiveValue(), 0); +} + +BOOST_FIXTURE_TEST_CASE(objectiveValue_after_solve_matches_solution_objective, + FixtureFeasibleProblem) +{ + auto* solution = pb->solve(false); + BOOST_CHECK_EQUAL(solution->getObjectiveValue(), 1); + BOOST_CHECK_EQUAL(pb->objectiveValue(), solution->getObjectiveValue()); +} + +BOOST_FIXTURE_TEST_CASE(objectiveOffset_default_and_value_is_zero, FixtureEmptyProblem) +{ + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 0); + BOOST_CHECK_EQUAL(pb->objectiveValue(), 0); +} + +BOOST_FIXTURE_TEST_CASE(setObjectiveOffset_changes_getter_and_objectiveValue_without_vars, + FixtureEmptyProblem) +{ + pb->setObjectiveOffset(2.5); + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 2.5); + BOOST_CHECK_EQUAL(pb->objectiveValue(), 2.5); +} + +BOOST_FIXTURE_TEST_CASE(setObjectiveOffset_before_solve_affects_solution_objective, + FixtureFeasibleProblem) +{ + pb->setObjectiveOffset(2.0); + auto* solution = pb->solve(false); + BOOST_CHECK_EQUAL(solution->getObjectiveValue(), 3); + BOOST_CHECK_EQUAL(pb->objectiveValue(), solution->getObjectiveValue()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/tests/src/solver/optim-model-filler/test_componentFiller_addObjective.cpp b/src/tests/src/solver/optim-model-filler/test_componentFiller_addObjective.cpp index a7eb2625f22..70807f949eb 100644 --- a/src/tests/src/solver/optim-model-filler/test_componentFiller_addObjective.cpp +++ b/src/tests/src/solver/optim-model-filler/test_componentFiller_addObjective.cpp @@ -23,6 +23,7 @@ #include +#include #include #include "antares/exception/RuntimeError.hpp" #include "antares/expressions/nodes/ExpressionsNodes.h" @@ -113,6 +114,7 @@ BOOST_AUTO_TEST_CASE(time_sum_var_with_objective) const auto var_name = "componentA.x_s0_t" + to_string(i); BOOST_CHECK_NO_THROW((void)pb->lookupVariable(var_name)); BOOST_CHECK_EQUAL(pb->getObjectiveCoefficient(pb->lookupVariable(var_name)), 2); + // Offset 9 } } @@ -146,3 +148,335 @@ BOOST_AUTO_TEST_CASE(one_var_with_param_objective) } BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(_ComponentFiller_getObjectiveOffset_, LinearProblemBuildingFixture) + +BOOST_AUTO_TEST_CASE(one_var_no_offset_expect_objective_offset_zero) +{ + // exp: ax + b, a = 1, b = 0 + auto objective = variable("x", 0); + + createModelWithOneFloatVar("model", {}, "x", literal(-50), literal(-40), {}, objective); + createComponent("model", "componentA", {}); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 0); +} + +BOOST_AUTO_TEST_CASE(one_var_with_param_no_offset_expect_objective_offset_zero) +{ + // exp: param * x, param = 5, no offset + auto objective = multiply(parameter("param"), variable("x", 0)); + createModelWithOneFloatVar("model", {"param"}, "x", literal(-50), literal(-40), {}, objective); + createComponent("model", "componentA", {build_context_parameter_with("param", "5")}); + buildLinearProblem(); + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 0); +} + +BOOST_AUTO_TEST_CASE(one_var_with_constant_offset_offset_expect_objective_offset_ten) +{ + // exp: ax + b, a = 1, b = 10 + const auto objective = add(variable("x", 0), literal(10)); + createModelWithOneFloatVar("model", {}, "x", literal(-50), literal(-40), {}, objective); + createComponent("model", "componentA", {}); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 10); +} + +BOOST_AUTO_TEST_CASE(one_param_offset_expect_objective_offset_five) +{ + // exp: x + param, param = 5 + auto objective = parameter("param"); + createModelWithOneFloatVar("model", {"param"}, "x", literal(-50), literal(-40), {}, objective); + createComponent("model", "componentA", {build_context_parameter_with("param", "5")}); + FillContext ctx{0, 10, 0, 10, 0}; + buildLinearProblem(ctx); + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 5); +} + +BOOST_AUTO_TEST_CASE( + one_time_dependent_var_ten_timesteps_with_constant_offset_ten_expected_objective_offset_ten_times_nb_timesteps) +{ + // exp: sum(ax + b), a: [t0,...tn], b = 10 + auto objective = nodeRegistry.create( + add(variable("x", 0, VariabilityType::VARYING_IN_TIME_ONLY), literal(10))); + + createModelWithOneFloatVar("model", {}, "x", literal(-50), literal(-40), {}, objective, true); + createComponent("model", "componentA", {}); + + constexpr unsigned int last_time_step = 9; + FillContext ctx{0, last_time_step, 0, last_time_step, 0}; + buildLinearProblem(ctx); + const auto nb_var = ctx.getLocalNumberOfTimeSteps(); // = 10 + + BOOST_CHECK_EQUAL(pb->variableCount(), nb_var); + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), nb_var * 10); // 10 timesteps each with offset 10 +} + +BOOST_AUTO_TEST_CASE( + one_var_with_time_dependent_offset_3_timesteps_expected_objective_offset_sum_of_param_values_at_time_steps) +{ + // exp: sum(x + param(t)), param = [10,11,12] + auto objective = nodeRegistry.create( + add(variable("x", 0), parameter("param", VariabilityType::VARYING_IN_TIME_ONLY))); + createModelWithSystemModelParameter( + "model", + {Parameter{"param", TimeDependent::YES, ScenarioDependent::NO}}, + {{"x", ValueType::FLOAT, literal(-5), literal(10), true, false}}, + {}, + objective); + createComponent( + "model", + "componentA", + {build_context_parameter_with("param", "bounds", VariabilityType::VARYING_IN_TIME_ONLY)}); + + FillContext ctx{0, 2, 0, 2, 0}; + auto bounds_time_series = std::make_unique("bounds", 3); + // setting 3 hours (including h 1 and 2) + bounds_time_series->add({10., 11., 12.}); + LinearProblemData data; + data.addDataSeries(std::move(bounds_time_series)); + + std::vector> scenarios; + buildLinearProblem(ctx, data, scenarios); + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 33); // 10 + 11 + 12 +} + +BOOST_AUTO_TEST_CASE( + one_var_with_scenario_dependent_offset_expected_objective_offset_value_for_scenario) +{ + // exp: x + param(s), param varies by scenario + auto objective = Sum( + add(variable("x", 0), parameter("param", VariabilityType::VARYING_IN_SCENARIO_ONLY))); + createModelWithSystemModelParameter( + "model", + {Parameter{"param", TimeDependent::NO, ScenarioDependent::YES}}, + {{"x", ValueType::FLOAT, literal(-5), literal(10), false, false}}, + {}, + objective); + createComponent( + "model", + "componentA", + {build_context_parameter_with("param", "bounds", VariabilityType::VARYING_IN_SCENARIO_ONLY)}, + "scenario_group"); + + FillContext ctx{0, 0, 0, 0, 0}; + auto bounds_time_series = std::make_unique("bounds", 1); + // Add 3 different scenarios with different offset values + bounds_time_series->add({15.}); // scenario 0 + bounds_time_series->add({25.}); // scenario 1 + bounds_time_series->add({35.}); // scenario 2 + LinearProblemData data; + data.addDataSeries(std::move(bounds_time_series)); + + std::vector> scenarios; + auto scenario0 = std::make_unique("scenario_group"); + scenario0->setTimeSerieNumber(0, 3); + scenarios.push_back(std::move(scenario0)); + + buildLinearProblem(ctx, data, scenarios); + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 35); // value for scenario_group ts number 3 +} + +BOOST_AUTO_TEST_CASE( + one_var_with_time_and_scenario_dependent_offset_expected_objective_offset_sum_for_all_time_and_scenarios) +{ + // exp: sum(x + param(t,s)), param varies by both time and scenario + auto objective = Sum( + add(variable("x", 0), parameter("param", VariabilityType::VARYING_IN_TIME_AND_SCENARIO))); + createModelWithSystemModelParameter( + "model", + {Parameter{"param", TimeDependent::YES, ScenarioDependent::YES}}, + {{"x", ValueType::FLOAT, literal(-5), literal(10), true, false}}, + {}, + objective); + createComponent( + "model", + "componentA", + {build_context_parameter_with("param", "bounds", VariabilityType::VARYING_IN_TIME_ONLY)}, + "scenario_group"); + + FillContext ctx{0, 2, 0, 2, 0}; // 3 time steps + auto bounds_time_series = std::make_unique("bounds", 3); + // Add 2 different scenarios with different offset values across time + // Scenario 0: time series [10, 20, 30] + bounds_time_series->add({10., 20., 30.}); + // Scenario 1: time series [15, 25, 35] + bounds_time_series->add({15., 25., 35.}); + LinearProblemData data; + data.addDataSeries(std::move(bounds_time_series)); + + std::vector> scenarios; + auto scenario0 = std::make_unique("scenario_group"); + scenario0->setTimeSerieNumber(0, 2); + scenarios.push_back(std::move(scenario0)); + + buildLinearProblem(ctx, data, scenarios); + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 75); +} + +BOOST_AUTO_TEST_CASE(var_and_param_both_varying_in_time_and_scenario_with_different_scenario_groups) +{ + // exp: sum(x(t,s_x) + param(t,s_y)) + // Variable x varies in time and scenario (uses scenarioX) + // Param varies in time and scenario (uses scenarioY) + auto objective = Sum(add(variable("x", 0, VariabilityType::VARYING_IN_TIME_AND_SCENARIO), + parameter("param", VariabilityType::VARYING_IN_TIME_AND_SCENARIO))); + createModelWithSystemModelParameter( + "model", + {Parameter{"param", TimeDependent::YES, ScenarioDependent::YES}}, + {{"x", ValueType::FLOAT, literal(-5), literal(100), true, true}}, + {}, + objective); + createComponent( + "model", + "componentA", + {build_context_parameter_with("param", "param_ts", VariabilityType::VARYING_IN_TIME_ONLY)}, + "scenarioX"); + + FillContext ctx{0, 2, 0, 2, 0}; // 3 time steps + + // Time series for variable x (scenarioX group) + auto x_time_series = std::make_unique("x", 3); + x_time_series->add({1., 2., 3.}); // time series 0 + x_time_series->add({11., 22., 33.}); // time series 1 + + // Time series for parameter (scenarioY group) + auto param_time_series = std::make_unique("param_ts", 3); + param_time_series->add({3., 6., 9.}); // time series 0 + + LinearProblemData data; + data.addDataSeries(std::move(x_time_series)); + data.addDataSeries(std::move(param_time_series)); + + std::vector> scenarios; + // Scenario for variable x: select second time series [11, 22, 33] + auto scenarioX = std::make_unique("scenarioX"); + scenarioX->setTimeSerieNumber(0, 1); // Select time series 1 + scenarios.push_back(std::move(scenarioX)); + + // Scenario for parameter: select first time series [3, 6, 9] + auto scenarioY = std::make_unique("scenarioY"); + scenarioY->setTimeSerieNumber(0, 0); // Select time series 0 + scenarios.push_back(std::move(scenarioY)); + + buildLinearProblem(ctx, data, scenarios); + + // Variable x contributes: 11 + 22 + 33 = 66 + // Parameter contributes: 3 + 6 + 9 = 18 + // But only the parameter is in the objective offset (variable coefficients don't contribute to + // offset) Expected: 18 (only from parameter offset) + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 18); +} + +BOOST_AUTO_TEST_CASE(two_expressions_one_with_time_varying_param_one_with_constant_offset) +{ + // Expression 1: sum(x + param(t), param = [1, 2, 3]) + // Expression 2: sum(x + 20) + auto expr1 = Sum(add(variable("x", 0, VariabilityType::VARYING_IN_TIME_ONLY), + parameter("param", VariabilityType::VARYING_IN_TIME_ONLY))); + auto expr2 = Sum(add(variable("x", 0, VariabilityType::VARYING_IN_TIME_ONLY), literal(20))); + auto objective = add(expr1, expr2); + + createModelWithSystemModelParameter( + "model", + {Parameter{"param", TimeDependent::YES, ScenarioDependent::NO}}, + {{"x", ValueType::FLOAT, literal(-5), literal(100), true, false}}, + {}, + objective); + createComponent( + "model", + "componentA", + {build_context_parameter_with("param", "param_ts", VariabilityType::VARYING_IN_TIME_ONLY)}); + + FillContext ctx{0, 2, 0, 2, 0}; // 3 time steps + + // Time series for parameter: [1, 2, 3] + auto param_time_series = std::make_unique("param_ts", 3); + param_time_series->add({1., 2., 3.}); + + LinearProblemData data; + data.addDataSeries(std::move(param_time_series)); + + std::vector> scenarios; + buildLinearProblem(ctx, data, scenarios); + + // Expression 1 offset: 1 + 2 + 3 = 6 + // Expression 2 offset: 20 + 20 + 20 = 60 + // Total offset: 6 + 60 = 66 + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 66); +} + +BOOST_AUTO_TEST_CASE(multiple_objectives_in_model) +{ + // Model with two objectives: + // Objective 1: x + 10 + // Objective 2: x + param(t), param = [5, 10, 15] + // Expected offset over 3 time steps: (10+10+10) + (5+10+15) = 30 + 30 = 60 + + auto objective1 = Sum( + add(variable("x", 0, VariabilityType::VARYING_IN_TIME_ONLY), literal(10))); + auto objective2 = Sum(add(variable("x", 0, VariabilityType::VARYING_IN_TIME_ONLY), + parameter("param", VariabilityType::VARYING_IN_TIME_ONLY))); + + std::vector objectives = {objective1, objective2}; + + createModelWithMultipleObjectives( + "model", + {Parameter{"param", TimeDependent::YES, ScenarioDependent::NO}}, + {{"x", ValueType::FLOAT, literal(-5), literal(100), true, false}}, + {}, + objectives); + createComponent( + "model", + "componentA", + {build_context_parameter_with("param", "param_ts", VariabilityType::VARYING_IN_TIME_ONLY)}); + + FillContext ctx{0, 2, 0, 2, 0}; // 3 time steps + + // Time series for parameter: [5, 10, 15] + auto param_time_series = std::make_unique("param_ts", 3); + param_time_series->add({5., 10., 15.}); + + LinearProblemData data; + data.addDataSeries(std::move(param_time_series)); + + std::vector> scenarios; + buildLinearProblem(ctx, data, scenarios); + + // Objective 1 offset: 10 + 10 + 10 = 30 + // Objective 2 offset: 5 + 10 + 15 = 30 + // Total offset: 30 + 30 = 60 + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 60); +} + +BOOST_AUTO_TEST_CASE(time_sum_var_with_objective) +{ + // Objective: sum(2*x + 3) over time steps 0 to 2 + // This should give coefficient 2 for each variable x_t0, x_t1, x_t2 + auto from = literal(0); + auto to = literal(2); + auto expression = add(multiply(literal(2), + variable("x", 0, VariabilityType::VARYING_IN_TIME_ONLY)), + literal(3)); + auto timeSum = nodeRegistry.create(expression); + + createModelWithOneFloatVar("model", {}, "x", literal(-50), literal(-40), {}, timeSum, true); + createComponent("model", "componentA", {}); + + constexpr unsigned int last_time_step = 2; + FillContext ctx{0, last_time_step, 0, last_time_step, 0}; + buildLinearProblem(ctx); + const auto nb_var = ctx.getLocalNumberOfTimeSteps(); // = 3 + + BOOST_CHECK_EQUAL(pb->variableCount(), nb_var); + for (unsigned i = 0; i < nb_var; i++) + { + { + BOOST_CHECK_EQUAL(pb->getObjectiveOffset(), 9); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/vcpkg-configuration.json b/src/vcpkg-configuration.json index a91bca70274..0a9d98f4fe3 100644 --- a/src/vcpkg-configuration.json +++ b/src/vcpkg-configuration.json @@ -11,7 +11,7 @@ { "kind": "git", "repository": "https://github.com/AntaresSimulatorTeam/antares-vcpkg-registry", - "baseline": "9b4a8e17c594af44915fdd75b9affb5ff3be4cc8", + "baseline": "6c324ae538979f4e79eb692466ad092ab48bd8fc", "packages": [ "sirius-solver" ] diff --git a/src/vcpkg.json b/src/vcpkg.json index fa643cbe300..c7d7f163fa1 100644 --- a/src/vcpkg.json +++ b/src/vcpkg.json @@ -4,7 +4,7 @@ "dependencies": [ { "name": "sirius-solver", - "version>=": "1.8" + "version>=": "1.9" }, { "name": "wxwidgets",