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",