Skip to content
Merged
Show file tree
Hide file tree
Changes from 87 commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
17fdbb2
Add setObjectiveOffset
JasonMarechal25 Oct 30, 2025
6ce6190
Offset fail
JasonMarechal25 Oct 30, 2025
ef1d0b6
Not implemented in sirius
JasonMarechal25 Oct 30, 2025
7ac83b7
Only param ok
JasonMarechal25 Oct 31, 2025
c22bec2
Var in time OK
JasonMarechal25 Oct 31, 2025
5dd8b88
Time dependent offset fail
JasonMarechal25 Oct 31, 2025
3d802d0
Time dependent offset unsupported
JasonMarechal25 Oct 31, 2025
44e4152
Time dependent offset unsupported
JasonMarechal25 Oct 31, 2025
040366b
Refacto
JasonMarechal25 Oct 31, 2025
c48aeaf
Delta double comparaison
JasonMarechal25 Nov 3, 2025
0f188c0
Default highs for cucumber
JasonMarechal25 Nov 3, 2025
579c9cb
Merge branch 'develop' into feature/set_offset
JasonMarechal25 Nov 4, 2025
1090878
Use highs by default in tests
JasonMarechal25 Nov 10, 2025
fa98604
Merge remote-tracking branch 'origin/develop' into feature/set_offset
JasonMarechal25 Nov 10, 2025
691baf2
Fix merge
JasonMarechal25 Nov 10, 2025
e5df6ac
Fix merge
JasonMarechal25 Nov 10, 2025
3c47266
Merge branch 'develop' into feature/set_offset
JasonMarechal25 Nov 10, 2025
3dd1b98
Highs default
JasonMarechal25 Nov 10, 2025
8246d0e
Fix update offset
JasonMarechal25 Nov 12, 2025
1926e89
Slight improvement
JasonMarechal25 Nov 12, 2025
eee5d5f
Fix unit tests
JasonMarechal25 Nov 12, 2025
5a63ba5
Add message to error
JasonMarechal25 Nov 12, 2025
a167d65
Improve log when simulation tables don't match
JasonMarechal25 Nov 12, 2025
6596e2d
Improve log
JasonMarechal25 Nov 12, 2025
af51a70
Proper offset computation
JasonMarechal25 Nov 17, 2025
01577d2
Test description
JasonMarechal25 Nov 17, 2025
746626c
Test pass
JasonMarechal25 Nov 17, 2025
3786a08
Handle N studies
JasonMarechal25 Nov 17, 2025
cc34597
Split test suites
JasonMarechal25 Nov 17, 2025
0ef6b40
style: More explicit tests
JasonMarechal25 Nov 17, 2025
89c51e5
style: Remove intermediate variable
JasonMarechal25 Nov 17, 2025
fc7d6db
style: Declare reference outside loop
JasonMarechal25 Nov 17, 2025
cd277d4
Merge branch 'develop' into feature/set_offset
JasonMarechal25 Nov 17, 2025
7edd830
style: Format
JasonMarechal25 Nov 17, 2025
a5f5db5
test: Objective offset varying in scenario
JasonMarechal25 Nov 17, 2025
8c4feb6
test: Objective offset varying in time and scenario
JasonMarechal25 Nov 17, 2025
9c34d75
test: Objective offset and variable varying in time and scenario
JasonMarechal25 Nov 17, 2025
86e30cc
Objective offset : proposal for small improvement (#3241)
guilpier-code Nov 18, 2025
7a7276f
test: Proper objective offset for several expression
JasonMarechal25 Nov 18, 2025
14dc9cc
test: Proper objective offset for several objectives
JasonMarechal25 Nov 18, 2025
f50ad73
fix: Proper objective offset for several objectives
JasonMarechal25 Nov 18, 2025
7385d07
chore: Split large test suite
JasonMarechal25 Nov 25, 2025
9396db9
feat: Time dependent objective is not supported
JasonMarechal25 Nov 25, 2025
cfeedb7
test: add-node helper
JasonMarechal25 Nov 25, 2025
477cbd5
doc: Update expression documentation
JasonMarechal25 Nov 25, 2025
256e535
Merge branch 'develop' into feature/time_dep_obj
JasonMarechal25 Nov 25, 2025
bd9a060
feat: Time dependent objective is not supported
JasonMarechal25 Nov 25, 2025
4aafeaa
feat: Objective offset
JasonMarechal25 Nov 25, 2025
946b95d
Merge remote-tracking branch 'origin/feature/time_dep_obj' into featu…
JasonMarechal25 Nov 26, 2025
aa95227
fix: build post rebase
JasonMarechal25 Nov 26, 2025
8368e33
wip
JasonMarechal25 Nov 26, 2025
3c7715b
fix: proper offset for multiple time steps
JasonMarechal25 Nov 27, 2025
480a4f5
Merge remote-tracking branch 'origin/develop' into feature/set_offset
JasonMarechal25 Nov 27, 2025
d83a1ff
fix: build after merge
JasonMarechal25 Nov 27, 2025
be9e7d7
feat: setObjectiveOffset API
JasonMarechal25 Oct 30, 2025
f0e9b7f
feat: Don't throw error when using setObjectiveOffset with Sirius
JasonMarechal25 Nov 18, 2025
4932b40
refactor: improve design setObjectiveOffset
JasonMarechal25 Nov 21, 2025
549192b
feat: Don't throw error when using setObjectiveOffset with Sirius
JasonMarechal25 Nov 18, 2025
086e6df
refactor: improve design setObjectiveOffset
JasonMarechal25 Nov 21, 2025
d43e9ec
Merge branch 'feature/non_blocking_offset' into feature/set_offset
JasonMarechal25 Nov 27, 2025
83136e0
fix: Remove unused import
JasonMarechal25 Nov 27, 2025
49b1e19
refactor: more descriptive parameter name
JasonMarechal25 Nov 28, 2025
4a5d655
refactor: more descriptive class name
JasonMarechal25 Nov 28, 2025
a07e8ba
doc: method doc
JasonMarechal25 Nov 28, 2025
36b00a6
Merge branch 'feature/non_blocking_offset' into feature/set_offset
JasonMarechal25 Nov 28, 2025
cd56ace
refactor: use Objective() value
JasonMarechal25 Dec 1, 2025
295c8af
wip
JasonMarechal25 Dec 1, 2025
05a1def
wip
JasonMarechal25 Dec 1, 2025
bcb1979
Merge remote-tracking branch 'origin/develop' into feature/set_offset
JasonMarechal25 Dec 1, 2025
6b2def5
fix: build after merge
JasonMarechal25 Dec 1, 2025
d49902c
fix: properly handle objective offset for value
JasonMarechal25 Dec 1, 2025
01489fb
chore: update ortools to 9.14
JasonMarechal25 Dec 1, 2025
e9638d5
chore: update ortools to 9.13-rte1.2
JasonMarechal25 Dec 1, 2025
ae56b7f
chore: update ortools to 9.13-rte1.2
JasonMarechal25 Dec 1, 2025
2e4f083
Revert "refactor: use Objective() value"
JasonMarechal25 Dec 3, 2025
9c994dc
chore: update ortools to 9.13-rte1.3
JasonMarechal25 Dec 4, 2025
1f642a1
chore: bump registry
JasonMarechal25 Dec 4, 2025
9597205
fix
JasonMarechal25 Dec 4, 2025
9c89bf8
Merge remote-tracking branch 'origin/develop' into feature/set_offset
JasonMarechal25 Dec 5, 2025
40e2cd3
fix: build
JasonMarechal25 Dec 5, 2025
335a8c9
fix: build TIMESERIE -> TIMESERIES
JasonMarechal25 Dec 5, 2025
55b03dc
Revert "Revert "refactor: use Objective() value""
JasonMarechal25 Dec 5, 2025
9f38df6
Revert "Revert "Revert "refactor: use Objective() value"""
JasonMarechal25 Dec 5, 2025
07bfb75
Merge branch 'develop' into feature/set_offset
JasonMarechal25 Dec 8, 2025
b373401
fix: return proper objective value
JasonMarechal25 Dec 8, 2025
f098b60
tests: test on getObjectiveValue
JasonMarechal25 Dec 8, 2025
fc2003d
fix: remove unused header
JasonMarechal25 Dec 8, 2025
d6e0b98
refactor: cleaner
JasonMarechal25 Dec 10, 2025
25e22e8
Merge branch 'develop' into feature/set_offset
JasonMarechal25 Dec 10, 2025
7fdc23f
fix: unit tests after rebase
JasonMarechal25 Dec 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
14 changes: 14 additions & 0 deletions src/optimisation/linear-problem-mpsolver-impl/linearProblem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
11 changes: 5 additions & 6 deletions src/optimisation/linear-problem-mpsolver-impl/mipSolution.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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";
Expand Down
9 changes: 7 additions & 2 deletions src/solver/optim-model-filler/ComponentFiller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* along with Antares_Simulator. If not, see <https://opensource.org/license/mpl-2-0/>.
*/

#include <numeric>
#include <ranges>
#include <stdexcept>
#include <variant>
Expand Down Expand Up @@ -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],
Expand All @@ -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);
Expand Down Expand Up @@ -413,6 +414,7 @@ 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();
Expand All @@ -424,7 +426,10 @@ void ComponentFiller::addObjectives(const LinearProblemApi::FillContext& ctx)
throw Error::RuntimeError("Time dependent objectives are not supported in Antares.");
}
addStaticObjective(linearExpression);
objectiveOffset += linearExpression.constant()[0];
}
auto& pb = optimEntityContainer_.Problem();
pb.setObjectiveOffset(objectiveOffset);
}

VariabilityType ComponentFiller::getVariability(const Node* node, const Component& component) const
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ 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;

Expand Down
1 change: 1 addition & 0 deletions src/solver/optimisation/LegacyFiller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -36,7 +36,7 @@ class LegacyOrtoolsLinearProblem final
// nothing else to do
}

operations_research::MPSolver* getMpSolver()
MPSolver* getMpSolver()
{
return MpSolver();
}
Expand Down
19 changes: 9 additions & 10 deletions src/solver/optimisation/opt_appel_solveur_lineaire.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -68,11 +69,28 @@ 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):
df = self.simulation_table[(self.simulation_table["output"] == "OBJECTIVE_VALUE")]
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
138 changes: 138 additions & 0 deletions src/tests/cucumber/features/steps/common_steps/solver_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading
Loading