diff --git a/src/expressions/include/antares/expressions/iterators/pre-order.h b/src/expressions/include/antares/expressions/iterators/pre-order.h index e2f03f0814f..731c7b3216e 100644 --- a/src/expressions/include/antares/expressions/iterators/pre-order.h +++ b/src/expressions/include/antares/expressions/iterators/pre-order.h @@ -54,4 +54,51 @@ class AST // End iterator (indicating traversal is complete) ASTPreOrderIterator end(); }; + +// PreOrder Iterator for AST const version +class ASTPreOrderIteratorConst +{ + std::stack nodeStack; + +public: + // Iterator type aliases + using iterator_category = std::forward_iterator_tag; + using value_type = Node; + using difference_type = std::ptrdiff_t; + using pointer = const Node*; + using reference = const Node&; + + // Constructor + explicit ASTPreOrderIteratorConst(const Node* root = nullptr); + + // Dereference operator + reference operator*() const; + + // Pointer access operator + pointer operator->() const; + + // Increment operator (pre-order traversal) + ASTPreOrderIteratorConst& operator++(); + + // Equality comparison + bool operator==(const ASTPreOrderIteratorConst& other) const; + + // Inequality comparison + bool operator!=(const ASTPreOrderIteratorConst& other) const; +}; + +// AST container class to expose begin/end iterators +class ASTconst +{ + const Node* root; + +public: + explicit ASTconst(const Node* rootNode); + + // Begin iterator + ASTPreOrderIteratorConst begin(); + + // End iterator (indicating traversal is complete) + ASTPreOrderIteratorConst end(); +}; } // namespace Antares::Expressions::Nodes diff --git a/src/expressions/include/antares/expressions/nodes/ParentNode.h b/src/expressions/include/antares/expressions/nodes/ParentNode.h index 69df35d2add..fe34b142e22 100644 --- a/src/expressions/include/antares/expressions/nodes/ParentNode.h +++ b/src/expressions/include/antares/expressions/nodes/ParentNode.h @@ -72,6 +72,7 @@ class ParentNode: public Node * @return A vector of pointers to the operands of the parent node. */ const std::vector& getOperands() const; + const std::vector getConstOperands() const; Node* operator[](std::size_t idx) const; diff --git a/src/expressions/iterators/pre-order.cpp b/src/expressions/iterators/pre-order.cpp index e9530e6695e..1c52e6b515e 100644 --- a/src/expressions/iterators/pre-order.cpp +++ b/src/expressions/iterators/pre-order.cpp @@ -24,6 +24,24 @@ std::vector childrenLeftToRight(Node* node) } return {}; } + +// const version +std::vector childrenLeftToRight(const Node* node) +{ + if (auto* bin = dynamic_cast(node)) + { + return {bin->left(), bin->right()}; + } + else if (auto* unary = dynamic_cast(node)) + { + return {unary->child()}; + } + else if (auto* sum = dynamic_cast(node)) + { + return sum->getConstOperands(); + } + return {}; +} } // namespace // Constructor @@ -104,4 +122,87 @@ ASTPreOrderIterator AST::end() { return ASTPreOrderIterator(nullptr); } + +// +// CONST VERSION +// + +// Constructor +ASTPreOrderIteratorConst::ASTPreOrderIteratorConst(const Node* root) +{ + if (root) + { + nodeStack.push(root); + } +} + +// Dereference operator +ASTPreOrderIteratorConst::reference ASTPreOrderIteratorConst::operator*() const +{ + return *nodeStack.top(); +} + +// Pointer access operator +ASTPreOrderIteratorConst::pointer ASTPreOrderIteratorConst::operator->() const +{ + return nodeStack.top(); +} + +// Increment operator (pre-order traversal) +ASTPreOrderIteratorConst& ASTPreOrderIteratorConst::operator++() +{ + if (nodeStack.empty()) + { + return *this; + } + + const Node* current = nodeStack.top(); + nodeStack.pop(); + + const auto children = childrenLeftToRight(current); + // Push children in reverse order to process them in left-to-right order + for (auto* it: children | std::views::reverse) + { + nodeStack.push(it); + } + + return *this; +} + +// Equality comparison +bool ASTPreOrderIteratorConst::operator==(const ASTPreOrderIteratorConst& other) const +{ + if (nodeStack.empty() && other.nodeStack.empty()) + { + return true; + } + if (nodeStack.empty() || other.nodeStack.empty()) + { + return false; + } + return nodeStack.top() == other.nodeStack.top(); +} + +// Inequality comparison +bool ASTPreOrderIteratorConst::operator!=(const ASTPreOrderIteratorConst& other) const +{ + return !(*this == other); +} + +ASTconst::ASTconst(const Node* rootNode): + root(rootNode) +{ +} + +// Begin iterator +ASTPreOrderIteratorConst ASTconst::begin() +{ + return ASTPreOrderIteratorConst(root); +} + +// End iterator (indicating traversal is complete) +ASTPreOrderIteratorConst ASTconst::end() +{ + return ASTPreOrderIteratorConst(nullptr); +} } // namespace Antares::Expressions::Nodes diff --git a/src/expressions/nodes/ParentNode.cpp b/src/expressions/nodes/ParentNode.cpp index f919ac3ed26..fecb410b1c8 100644 --- a/src/expressions/nodes/ParentNode.cpp +++ b/src/expressions/nodes/ParentNode.cpp @@ -33,6 +33,12 @@ const std::vector& ParentNode::getOperands() const return operands_; } +const std::vector ParentNode::getConstOperands() const +{ + std::vector constOperands(operands_.begin(), operands_.end()); + return constOperands; +} + size_t ParentNode::size() const { return operands_.size(); diff --git a/src/io/inputs/model-converter/convertorVisitor.cpp b/src/io/inputs/model-converter/convertorVisitor.cpp index 69df40ec3df..0d5264582c1 100644 --- a/src/io/inputs/model-converter/convertorVisitor.cpp +++ b/src/io/inputs/model-converter/convertorVisitor.cpp @@ -424,7 +424,7 @@ std::any ConvertorVisitor::handleDual(ExprParser::ArgListContext* context) return node; } - throw NoConstraintWithThisName(model_.id, constraint_id); + throw DualNoConstraintWithThisName(model_.id, constraint_id); } std::any ConvertorVisitor::handleReducedCost(ExprParser::ArgListContext* context) @@ -446,18 +446,18 @@ std::any ConvertorVisitor::handleReducedCost(ExprParser::ArgListContext* context + params); } - std::vector nodes; - try - { - nodes = std::any_cast>(context->accept(this)); - } - catch (const NoParameterOrVariableWithThisName&) // to print accurate message + unsigned index = 0; + for (const auto& var: model_.variables) { - throw NoVariableWithThisName(model_.id, context->expr(0)->getText()); + if (var.id == variableId.at(0)->getText()) + { + auto* varNode = registry_.create(var.id, index); + return static_cast( + registry_.create(FunctionNodeType::reduced_cost, varNode)); + } + ++index; } - - return static_cast( - registry_.create(FunctionNodeType::reduced_cost, nodes.at(0))); + throw ReducedCostNoVariableWithThisName(model_.id, variableId.at(0)->getText()); } template diff --git a/src/io/inputs/model-converter/include/antares/io/inputs/model-converter/convertorVisitor.h b/src/io/inputs/model-converter/include/antares/io/inputs/model-converter/convertorVisitor.h index e391522529b..cefcfa9acb0 100644 --- a/src/io/inputs/model-converter/include/antares/io/inputs/model-converter/convertorVisitor.h +++ b/src/io/inputs/model-converter/include/antares/io/inputs/model-converter/convertorVisitor.h @@ -43,21 +43,22 @@ class NoParameterOrVariableWithThisName final: public std::runtime_error } }; -class NoVariableWithThisName final: public std::runtime_error +class ReducedCostNoVariableWithThisName final: public std::runtime_error { public: - explicit NoVariableWithThisName(const std::string& modelName, const std::string& varName): + explicit ReducedCostNoVariableWithThisName(const std::string& modelName, + const std::string& varName): runtime_error("reduced_cost called with unknown variable '" + varName + "' in model '" + modelName + "'") { } }; -class NoConstraintWithThisName final: public std::runtime_error +class DualNoConstraintWithThisName final: public std::runtime_error { public: - explicit NoConstraintWithThisName(const std::string& modelName, - const std::string& constraintName): + explicit DualNoConstraintWithThisName(const std::string& modelName, + const std::string& constraintName): runtime_error("dual called with unknown constraint '" + constraintName + "' in model '" + modelName + "'") { diff --git a/src/modeler-optimisation-container/OptimEntityContainer.cpp b/src/modeler-optimisation-container/OptimEntityContainer.cpp index 92bb0d8eb43..c4d7b76813b 100644 --- a/src/modeler-optimisation-container/OptimEntityContainer.cpp +++ b/src/modeler-optimisation-container/OptimEntityContainer.cpp @@ -52,7 +52,7 @@ void OptimEntityContainer::addFromSystemComponents(const std::vector& modelVariableGlobalIndices.reserve(variables.size()); for (const auto& variable: variables) { - if (AreLocationsCompatible(variable.location(), targetLocation)) + if (AreLocationsCompatibleForFillers(variable.location(), targetLocation)) { modelVariableGlobalIndices.push_back(variableGlobalIndex); ++variableGlobalIndex; diff --git a/src/modeler/CMakeLists.txt b/src/modeler/CMakeLists.txt index 50c600d86b0..fbac869246c 100644 --- a/src/modeler/CMakeLists.txt +++ b/src/modeler/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory(loadFiles) add_subdirectory(modeler) add_subdirectory(parameters) +add_subdirectory(checks) OMESSAGE(" :: modeler") diff --git a/src/modeler/checks/CMakeLists.txt b/src/modeler/checks/CMakeLists.txt new file mode 100644 index 00000000000..a3a1bbb3eff --- /dev/null +++ b/src/modeler/checks/CMakeLists.txt @@ -0,0 +1,29 @@ +set(SOURCES + checkLocation.cpp + + include/antares/solver/modeler/checks/checkLocation.h +) + +# Create the library +add_library(modeler-checks ${SOURCES}) +add_library(Antares::modeler-checks ALIAS modeler-checks) + +# Specify include directories +target_include_directories(modeler-checks + PUBLIC + $ +) + +find_package(fmt REQUIRED) +# Link dependencies (if any) +target_link_libraries(modeler-checks + PUBLIC + Antares::antares-study-system-model + PRIVATE + modeler-lib + fmt::fmt +) + +install(DIRECTORY include/antares + DESTINATION "include" +) diff --git a/src/modeler/checks/checkLocation.cpp b/src/modeler/checks/checkLocation.cpp new file mode 100644 index 00000000000..6ad35c75873 --- /dev/null +++ b/src/modeler/checks/checkLocation.cpp @@ -0,0 +1,223 @@ +/* + * Copyright 2007-2025, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#include + +#include +#include +#include +#include +#include + +using namespace Antares::Expressions::Nodes; +using namespace Antares::ModelerStudy::SystemModel; +using namespace Antares::Modeler::Config; + +namespace Antares::Modeler::Checks +{ + +void checkModel(const Model& model, const System& system) +{ + for (const auto& constraint: model.Constraints()) + { + checkExpression(constraint.expression().RootNode(), + constraint.location(), + model, + system, + constraint.expression().Value()); + } + + for (const auto& objective: model.Objectives()) + { + checkExpression(objective.expression().RootNode(), + objective.location(), + model, + system, + objective.expression().Value()); + } + + // Extra outputs must be evaluated, they need to contain only subproblem objects + for (const auto& [_, extraOutput]: model.ExtraOutputs()) + { + checkExpression(extraOutput.expression().RootNode(), + Location::SUBPROBLEMS, + model, + system, + extraOutput.expression().Value()); + } +} + +void checkLocations(const Modeler::Data& data) +{ + for (const auto& lib: data.libraries) + { + for (const auto& [_, model]: lib.Models()) + { + checkModel(model, *data.system); + } + } +} + +void checkFunctionNode(const Node& node, const Model& model, const std::string& exprStr) +{ + // dual and reduced_cost can only be used in subproblems + if (auto* functionNode = dynamic_cast(&node); functionNode) + { + if (functionNode->type() == FunctionNodeType::reduced_cost) + { + const auto* varNode = dynamic_cast( + functionNode->getOperands().at(0)); + for (const auto& variable: model.Variables()) + { + if (variable.Id() == varNode->value() + && variable.location() != Location::SUBPROBLEMS) + { + throw LocationError(fmt::format( + "Model '{}': In expression '{}': Error for variable '{}': reduced_cost can " + "only be used on variables located in subproblems", + model.Id(), + exprStr, + varNode->value())); + } + } + } + + if (functionNode->type() == FunctionNodeType::dual) + { + // This node contains the constraint name + const auto* n = dynamic_cast(functionNode->getOperands().at(0)); + for (const auto& constraint: model.Constraints()) + { + if (constraint.Id() == n->value() && constraint.location() != Location::SUBPROBLEMS) + { + throw LocationError( + fmt::format("Model '{}': In expression '{}': Error for constraint '{}': dual " + "can only be used on constraints located in subproblems", + model.Id(), + exprStr, + n->value())); + } + } + } + } +} + +void checkExpression(const Node* expression, + const Location& location, + const Model& model, + const System& system, + const std::string& exprStr, // used for error msgs + const std::string& errorMsgForPortField) // used for error msgs +{ + for (const auto& node: ASTconst(expression)) + { + // base variable + if (const auto* varNode = dynamic_cast(&node); varNode) + { + for (const auto& variable: model.Variables()) + { + if (variable.Id() == varNode->value() + && !AreLocationsCompatibleForExpressions(variable.location(), location)) + { + throw LocationError( + fmt::format("{}Model '{}': In expression '{}': Error for variable '{}': " + "Location doesn't match the expression location (variable " + "location: {}, expression location: {})", + errorMsgForPortField, + model.Id(), + exprStr, + varNode->value(), + LocationToStr(variable.location()), + LocationToStr(location))); + } + } + continue; + } + + // Portfields can contains variables, we recursively check their expressions + if (const auto* portFieldNode = dynamic_cast(&node); portFieldNode) + { + // This code allows to have a clear message if one of the referenced expression contains + // bad locations + std::string msgInCaseOfError = fmt::format( + "In model '{}': In expression '{}': this port field definition '{}.{}': is " + "referencing an expression containing an incorrect location: ", + model.Id(), + exprStr, + portFieldNode->getPortName(), + portFieldNode->getFieldName()); + + PortFieldKey key(portFieldNode->getPortName(), portFieldNode->getFieldName()); + auto& expr = model.PortFieldDefinitions().at(key).Definition(); + checkExpression(model.PortFieldDefinitions().at(key).Definition().RootNode(), + location, + model, + system, + expr.Value(), + msgInCaseOfError); + continue; + } + + if (const auto* portFieldSumNode = dynamic_cast(&node); + portFieldSumNode) + { + // This code allows to have a clear message if one of the referenced expression contains + // bad locations + std::string msgInCaseOfError = fmt::format( + "In model '{}': In expression '{}': this 'sum_connections({}.{})' is referencing an " + "expression containing an incorrect location: ", + model.Id(), + exprStr, + portFieldSumNode->getPortName(), + portFieldSumNode->getFieldName()); + + for (const auto& component: system.Components()) + { + if (component.getModel()->Id() != model.Id()) + { + continue; + } + + for (const auto& connection: + component.componentConnectionsViaPort(portFieldSumNode->getPortName())) + { + auto* component = connection.component(); + auto* port = connection.port(); + const auto& expr = component->expressionAtPortField( + port->Id(), + portFieldSumNode->getFieldName()); + + checkExpression(expr.RootNode(), + location, + *connection.component()->getModel(), + system, + expr.Value(), + msgInCaseOfError); + } + } + continue; + } + + // handle dual and reduced_cost + checkFunctionNode(node, model, exprStr); + } +} +} // namespace Antares::Modeler::Checks diff --git a/src/modeler/checks/include/antares/solver/modeler/checks/checkLocation.h b/src/modeler/checks/include/antares/solver/modeler/checks/checkLocation.h new file mode 100644 index 00000000000..8fe07c22b0b --- /dev/null +++ b/src/modeler/checks/include/antares/solver/modeler/checks/checkLocation.h @@ -0,0 +1,46 @@ +/* + * Copyright 2007-2025, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#pragma once + +#include +#include + +namespace Antares::Modeler::Checks +{ + +/// Check location validity for all modeler data +void checkLocations(const Antares::Modeler::Data& data); + +// shouldn't be used outside, here for unit tests +void checkExpression(const Antares::Expressions::Nodes::Node* expression, + const Antares::Modeler::Config::Location& location, + const Antares::ModelerStudy::SystemModel::Model& model, + const Antares::ModelerStudy::SystemModel::System& system, + const std::string& exprStr, + const std::string& errorMsgForPortFieldSum = ""); + +class LocationError final: public std::invalid_argument +{ + using std::invalid_argument::invalid_argument; +}; + +} // namespace Antares::Modeler::Checks diff --git a/src/modeler/loadFiles/CMakeLists.txt b/src/modeler/loadFiles/CMakeLists.txt index 073e242ed6b..e0f41209807 100644 --- a/src/modeler/loadFiles/CMakeLists.txt +++ b/src/modeler/loadFiles/CMakeLists.txt @@ -44,6 +44,7 @@ target_link_libraries(load-modeler-files Antares::modeler-lib Antares::scenario-group-parser Antares::utils + Antares::modeler-checks fmt::fmt ) diff --git a/src/modeler/loadFiles/loadAll.cpp b/src/modeler/loadFiles/loadAll.cpp index 39cf488edd7..4c4fabd11e1 100644 --- a/src/modeler/loadFiles/loadAll.cpp +++ b/src/modeler/loadFiles/loadAll.cpp @@ -23,6 +23,7 @@ #include #include +#include "antares/solver/modeler/checks/checkLocation.h" #include "antares/solver/modeler/loadFiles/loadFiles.h" #include "antares/utils/utils.h" @@ -50,6 +51,10 @@ Modeler::Data loadAll(const std::filesystem::path& studyPath) measure.tick(); logs.info() << "Scenario groups loaded"; logs.info() << "Modeler loaded in " << measure.toStringInSeconds(); + + Modeler::Checks::checkLocations(data); + logs.info() << "Locations validity OK"; + return data; } diff --git a/src/modeler/modeler/Modeler.cpp b/src/modeler/modeler/Modeler.cpp index bab0330aaa8..e33484c397b 100644 --- a/src/modeler/modeler/Modeler.cpp +++ b/src/modeler/modeler/Modeler.cpp @@ -1,4 +1,3 @@ - /* * Copyright 2007-2025, RTE (https://www.rte-france.com) * See AUTHORS.txt @@ -22,7 +21,6 @@ #include "antares/solver/modeler/Modeler.h" -#include #include #include @@ -40,7 +38,6 @@ using namespace Antares::Optimisation::LinearProblemMpsolverImpl; using namespace Antares; using namespace Antares::Optimization; using namespace Antares::Optimisation; -using namespace Antares::Solver; using namespace Antares::Optimisation::LinearProblemApi; namespace Antares::Solver diff --git a/src/packaging/CMakeLists.txt b/src/packaging/CMakeLists.txt index 483e36346d1..710b89e731a 100644 --- a/src/packaging/CMakeLists.txt +++ b/src/packaging/CMakeLists.txt @@ -111,6 +111,7 @@ set(TARGET_LIBS #No alias scenario-group-parser yml-optim-config enums + modeler-checks ) install(TARGETS ${TARGET_LIBS} 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..0705930ffde 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 @@ -114,7 +114,7 @@ class ComponentFiller: public LinearProblemApi::LinearProblemFiller { return std::views::filter( [this](const auto& item) - { return AreLocationsCompatible(item.location(), targetLocation_); }); + { return AreLocationsCompatibleForFillers(item.location(), targetLocation_); }); } }; } // namespace Antares::Optimisation diff --git a/src/study/system-model/CMakeLists.txt b/src/study/system-model/CMakeLists.txt index 8a49fffee3e..d9f834ba80d 100644 --- a/src/study/system-model/CMakeLists.txt +++ b/src/study/system-model/CMakeLists.txt @@ -6,6 +6,7 @@ set(SRC_model component.cpp system.cpp connection.cpp + optimConfig.cpp include/antares/study/system-model/library.h include/antares/study/system-model/model.h include/antares/study/system-model/parameter.h diff --git a/src/study/system-model/component.cpp b/src/study/system-model/component.cpp index 448b8695cc9..ce24478476f 100644 --- a/src/study/system-model/component.cpp +++ b/src/study/system-model/component.cpp @@ -111,7 +111,7 @@ std::vector Component::componentConnectionsViaPort(const std::str return {}; } -const Node* Component::nodeAtPortField(const std::string& portId, const std::string& fieldId) const +Node* Component::nodeAtPortField(const std::string& portId, const std::string& fieldId) const { try { @@ -125,6 +125,21 @@ const Node* Component::nodeAtPortField(const std::string& portId, const std::str } } +const Expression& Component::expressionAtPortField(const std::string& portId, + const std::string& fieldId) const +{ + try + { + PortFieldKey key(portId, fieldId); + return getModel()->PortFieldDefinitions().at(key).Definition(); + } + catch (const std::out_of_range&) + { + throw std::invalid_argument("Port field '" + portId + "." + fieldId + + "' not found in component '" + data_.id + "'"); + } +} + void Component::addAreaConnection(const std::string& localPortId, const std::string& areaId) { std::string exceptionPrefix = "Cannot connect area \"" + areaId + "\" to port \"" + localPortId diff --git a/src/study/system-model/include/antares/study/system-model/component.h b/src/study/system-model/include/antares/study/system-model/component.h index 9d37e41525d..9572a5f4d3d 100644 --- a/src/study/system-model/include/antares/study/system-model/component.h +++ b/src/study/system-model/include/antares/study/system-model/component.h @@ -104,8 +104,11 @@ class Component final void addComponentConnection(const std::string localPortId, ConnectionEnd&& connection); std::vector componentConnectionsViaPort(const std::string& portId) const; - const Expressions::Nodes::Node* nodeAtPortField(const std::string& portId, - const std::string& fieldId) const; + Expressions::Nodes::Node* nodeAtPortField(const std::string& portId, + const std::string& fieldId) const; + + const Expression& expressionAtPortField(const std::string& portId, + const std::string& fieldId) const; void addAreaConnection(const std::string& localPortId, const std::string& areaId); diff --git a/src/study/system-model/include/antares/study/system-model/model.h b/src/study/system-model/include/antares/study/system-model/model.h index 9f549968209..4bb7c2a8f42 100644 --- a/src/study/system-model/include/antares/study/system-model/model.h +++ b/src/study/system-model/include/antares/study/system-model/model.h @@ -26,6 +26,7 @@ #include +#include "connection.h" #include "constraint.h" #include "extraOutput.h" #include "objective.h" diff --git a/src/study/system-model/include/antares/study/system-model/optimConfig.h b/src/study/system-model/include/antares/study/system-model/optimConfig.h index ed0fdb2b4da..4a3ecedd363 100644 --- a/src/study/system-model/include/antares/study/system-model/optimConfig.h +++ b/src/study/system-model/include/antares/study/system-model/optimConfig.h @@ -30,19 +30,10 @@ enum class Location SUBPROBLEMS }; -constexpr bool AreLocationsCompatible(Location lhs, Location rhs) -{ - switch (rhs) - { - case Location::MASTER: - return lhs == Location::MASTER || lhs == Location::MASTER_AND_SUBPROBLEMS; - case Location::SUBPROBLEMS: - return lhs == Location::SUBPROBLEMS || lhs == Location::MASTER_AND_SUBPROBLEMS; - case Location::MASTER_AND_SUBPROBLEMS: - return true; - default: - return false; - } -} +const char* LocationToStr(Location loc); + +bool AreLocationsCompatibleForFillers(Location lhs, Location rhs); +// stricter for MASTER_AND_SUBPROLEMS +bool AreLocationsCompatibleForExpressions(Location lhs, Location rhs); } // namespace Antares::Modeler::Config diff --git a/src/study/system-model/optimConfig.cpp b/src/study/system-model/optimConfig.cpp new file mode 100644 index 00000000000..4dd1c9be1b6 --- /dev/null +++ b/src/study/system-model/optimConfig.cpp @@ -0,0 +1,73 @@ +/* +** Copyright 2007-2025, RTE (https://www.rte-france.com) +** See AUTHORS.txt +** SPDX-License-Identifier: MPL-2.0 +** This file is part of Antares-Simulator, +** Adequacy and Performance assessment for interconnected energy networks. +** +** Antares_Simulator is free software: you can redistribute it and/or modify +** it under the terms of the Mozilla Public Licence 2.0 as published by +** the Mozilla Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** Antares_Simulator is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** Mozilla Public Licence 2.0 for more details. +** +** You should have received a copy of the Mozilla Public Licence 2.0 +** along with Antares_Simulator. If not, see . +*/ + +#include "antares/study/system-model/optimConfig.h" + +namespace Antares::Modeler::Config +{ + +const char* LocationToStr(Location loc) +{ + switch (loc) + { + case Location::MASTER: + return "master"; + case Location::MASTER_AND_SUBPROBLEMS: + return "master-and-subproblems"; + case Location::SUBPROBLEMS: + return "subproblems"; + default: + return "Unknown"; + } +} + +bool AreLocationsCompatibleForFillers(Location lhs, Location rhs) +{ + switch (rhs) + { + case Location::MASTER: + return lhs == Location::MASTER || lhs == Location::MASTER_AND_SUBPROBLEMS; + case Location::SUBPROBLEMS: + return lhs == Location::SUBPROBLEMS || lhs == Location::MASTER_AND_SUBPROBLEMS; + case Location::MASTER_AND_SUBPROBLEMS: + return true; + default: + return false; + } +} + +// stricter for MASTER_AND_SUBPROLEMS +bool AreLocationsCompatibleForExpressions(Location lhs, Location rhs) +{ + switch (rhs) + { + case Location::MASTER: + return lhs == Location::MASTER || lhs == Location::MASTER_AND_SUBPROBLEMS; + case Location::SUBPROBLEMS: + return lhs == Location::SUBPROBLEMS || lhs == Location::MASTER_AND_SUBPROBLEMS; + case Location::MASTER_AND_SUBPROBLEMS: + return lhs == Location::MASTER_AND_SUBPROBLEMS; + default: + return false; + } +} + +} // namespace Antares::Modeler::Config diff --git a/src/tests/src/modeler/CMakeLists.txt b/src/tests/src/modeler/CMakeLists.txt index c6bd02c9a4f..00f067f716b 100644 --- a/src/tests/src/modeler/CMakeLists.txt +++ b/src/tests/src/modeler/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(loadFiles) +add_subdirectory(checks) add_library(mock-modeler-objects mockModelerObjects.cpp diff --git a/src/tests/src/modeler/checks/CMakeLists.txt b/src/tests/src/modeler/checks/CMakeLists.txt new file mode 100644 index 00000000000..bc61dc1438f --- /dev/null +++ b/src/tests/src/modeler/checks/CMakeLists.txt @@ -0,0 +1,10 @@ +include(${CMAKE_SOURCE_DIR}/tests/macros.cmake) + +add_boost_test(test-modeler-checks + SRC + testCheckLocation.cpp + LIBS + Antares::modeler-checks + Antares::modeler-lib + test_utils_unit +) diff --git a/src/tests/src/modeler/checks/testCheckLocation.cpp b/src/tests/src/modeler/checks/testCheckLocation.cpp new file mode 100644 index 00000000000..8d715747602 --- /dev/null +++ b/src/tests/src/modeler/checks/testCheckLocation.cpp @@ -0,0 +1,260 @@ +/* + * Copyright 2007-2025, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ +#define WIN32_LEAN_AND_MEAN + +#define BOOST_TEST_MODULE modeler checks + +#include + +#include +#include "antares/study/system-model/model.h" + +// If we don't turn clang-format off here, some antlr4 header does not compile : +// it collides with a #include somewhere in Yuni +// clang-format off +#include +// clang-format on + +using namespace Antares::ModelerStudy::SystemModel; +using namespace Antares::Modeler::Checks; +using namespace Antares::Expressions::Nodes; +using namespace Antares::Modeler::Config; + +struct Fixture +{ + ModelBuilder modelBuilder; + SystemBuilder systemBuilder; + ComponentBuilder componentBuilder; + + Antares::Expressions::Registry registry; +}; + +BOOST_AUTO_TEST_SUITE(check_location) + +BOOST_FIXTURE_TEST_CASE(one_var_good_one_var_throw, Fixture) +{ + Node* goodLocVar = registry.create("var1", 0); + Node* badLocVar = registry.create("var2", 0); + Node* root = registry.create(goodLocVar, badLocVar); + + std::vector variables; + variables.push_back({"var1", {}, {}, ValueType::FLOAT, {}, {}, Location::SUBPROBLEMS}); + variables.push_back({"var2", {}, {}, ValueType::BOOL, {}, {}, Location::MASTER}); + + auto model = modelBuilder.withVariables(std::move(variables)).withId("base model").build(); + auto system = systemBuilder + .withComponents( + {componentBuilder.withModel(&model).withId("component").build()}) + .withId("system") + .build(); + + BOOST_CHECK_EXCEPTION( + checkExpression(root, Location::SUBPROBLEMS, model, system, "var1 + var2"), + LocationError, + checkMessage("Model 'base model': In expression 'var1 + var2': Error for variable " + "'var2': Location doesn't match the expression location (variable " + "location: master, expression location: subproblems)")); +} + +BOOST_FIXTURE_TEST_CASE(reduced_cost_throw, Fixture) +{ + Node* root = registry.create(FunctionNodeType::reduced_cost, + registry.create("var", 0)); + + std::vector variables; + variables.push_back( + {"var", {}, {}, ValueType::FLOAT, {}, {}, Location::MASTER_AND_SUBPROBLEMS}); + + auto model = modelBuilder.withVariables(std::move(variables)).withId("base model").build(); + auto system = systemBuilder + .withComponents( + {componentBuilder.withModel(&model).withId("component").build()}) + .withId("system") + .build(); + + BOOST_CHECK_EXCEPTION( + checkExpression(root, Location::SUBPROBLEMS, model, system, "reduced_cost(var)"), + LocationError, + checkMessage("Model 'base model': In expression 'reduced_cost(var)': Error for variable " + "'var': reduced_cost can only be used on variables located in subproblems")); +} + +BOOST_FIXTURE_TEST_CASE(dual_throw, Fixture) +{ + Node* root = registry.create(FunctionNodeType::dual, + registry.create("constraint"), + registry.create(0)); + + std::vector constraints; + constraints.push_back({"constraint", {}, Location::MASTER_AND_SUBPROBLEMS}); + auto model = modelBuilder.withConstraints(std::move(constraints)).withId("base model").build(); + auto system = systemBuilder + .withComponents( + {componentBuilder.withModel(&model).withId("component").build()}) + .withId("system") + .build(); + + BOOST_CHECK_EXCEPTION( + checkExpression(root, Location::SUBPROBLEMS, model, system, "dual(constraint)"), + LocationError, + checkMessage("Model 'base model': In expression 'dual(constraint)': Error for constraint " + "'constraint': dual can only be used on constraints located in subproblems")); +} + +BOOST_FIXTURE_TEST_CASE(portfield_throw, Fixture) +{ + // expressions setup + Node* pfNode = registry.create("port", "field"); + Antares::Expressions::Registry registry2; + Node* varNode = registry2.create("var1", 0); + Antares::Expressions::NodeRegistry node_registry(varNode, std::move(registry2)); + Expression pfExpression("var1", std::move(node_registry)); + + // var setup + std::vector variables; + variables.push_back({"var1", {}, {}, ValueType::FLOAT, {}, {}, Location::SUBPROBLEMS}); + + // ports setup + PortField portfield("field"); + std::vector portFields = {portfield}; + PortType portType("port", std::move(portFields), "field"); + Port port("port", portType); + std::vector portFieldDefs; + portFieldDefs.emplace_back(port, portfield, std::move(pfExpression)); + + auto model = modelBuilder.withVariables(std::move(variables)) + .withPortFieldDefinitions(std::move(portFieldDefs)) + .withId("base model") + .build(); + auto system = systemBuilder + .withComponents( + {componentBuilder.withModel(&model).withId("component").build()}) + .withId("system") + .build(); + + BOOST_CHECK_EXCEPTION( + checkExpression(pfNode, Location::MASTER, model, system, "port.field"), + LocationError, + checkMessage( + "In model 'base model': In expression 'port.field': this port field definition " + "'port.field': is referencing an expression containing an incorrect location: Model 'base " + "model': In expression 'var1': Error for variable 'var1': Location doesn't match the " + "expression location (variable location: subproblems, expression location: master)")); +} + +BOOST_FIXTURE_TEST_CASE(portfield_ok_var_throw, Fixture) +{ + // expressions setup + Node* pfNode = registry.create("port", "field"); + Node* varNodeBad = registry.create("var2", 0); + Node* root = registry.create(pfNode, varNodeBad); + + Antares::Expressions::Registry registry2; + Node* varNode = registry2.create("var1", 0); + Antares::Expressions::NodeRegistry node_registry(varNode, std::move(registry2)); + Expression pfExpression("var1", std::move(node_registry)); + + // var setup + std::vector variables; + variables.push_back( + {"var1", {}, {}, ValueType::FLOAT, {}, {}, Location::MASTER_AND_SUBPROBLEMS}); + variables.push_back({"var2", {}, {}, ValueType::FLOAT, {}, {}, Location::MASTER}); + + // ports setup + PortField portfield("field"); + std::vector portFields = {portfield}; + PortType portType("port", std::move(portFields), "field"); + Port port("port", portType); + std::vector portFieldDefs; + portFieldDefs.emplace_back(port, portfield, std::move(pfExpression)); + + auto model = modelBuilder.withVariables(std::move(variables)) + .withPortFieldDefinitions(std::move(portFieldDefs)) + .withId("base model") + .build(); + auto system = systemBuilder + .withComponents( + {componentBuilder.withModel(&model).withId("component").build()}) + .withId("system") + .build(); + + BOOST_CHECK_EXCEPTION( + checkExpression(root, Location::MASTER_AND_SUBPROBLEMS, model, system, "port.field + var2"), + LocationError, + checkMessage("Model 'base model': In expression 'port.field + var2': Error for variable " + "'var2': Location doesn't match the expression location (variable " + "location: master, expression location: master-and-subproblems)")); +} + +BOOST_FIXTURE_TEST_CASE(porfieldsum_throw, Fixture) +{ + // expressions setup + Node* pfNode = registry.create("port", "field"); + Node* pfSumNode = registry.create("port", "field"); + + Antares::Expressions::Registry registry2; + Node* varNode = registry2.create("var1", 0); + Antares::Expressions::NodeRegistry node_registry(varNode, std::move(registry2)); + Expression pfExpression("var1", std::move(node_registry)); + + // var setup + std::vector variables; + variables.push_back({"var1", {}, {}, ValueType::FLOAT, {}, {}, Location::MASTER}); + + // ports setup + PortField portfield("field"); + std::vector portFields = {portfield}; + PortType portType("port", std::move(portFields), "field"); + Port port("port", portType); + std::vector portFieldDefs; + portFieldDefs.emplace_back(port, portfield, std::move(pfExpression)); + + auto model1 = modelBuilder.withPorts({port}).withId("model1").build(); + + auto model2 = modelBuilder.withVariables(std::move(variables)) + .withId("model2") + .withPorts({port}) + .withPortFieldDefinitions(std::move(portFieldDefs)) + .build(); + + auto component1 = componentBuilder.withModel(&model1).withId("comp1").build(); + auto component2 = componentBuilder.withModel(&model2).withId("comp2").build(); + + component1.addComponentConnection("port", + ConnectionEnd(&component2, &model2.Ports().at("port"))); + + auto system = systemBuilder.withComponents({component1, component2}).withId("system").build(); + + BOOST_CHECK_EXCEPTION( + checkExpression(pfSumNode, + Location::SUBPROBLEMS, + model1, + system, + "sum_connections(port.field)"), + LocationError, + checkMessage( + "In model 'model1': In expression 'sum_connections(port.field)': this " + "'sum_connections(port.field)' is referencing an expression containing an incorrect " + "location: Model " + "'model2': In expression 'var1': Error for variable 'var1': Location doesn't match the " + "expression location (variable location: master, expression location: subproblems)")); +} +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/tests/src/modeler/loadFiles/test-load-optim-config.cpp b/src/tests/src/modeler/loadFiles/test-load-optim-config.cpp index a38c356eebb..adaf1390fb3 100644 --- a/src/tests/src/modeler/loadFiles/test-load-optim-config.cpp +++ b/src/tests/src/modeler/loadFiles/test-load-optim-config.cpp @@ -329,3 +329,38 @@ BOOST_FIXTURE_TEST_CASE(constraint_does_not_exist_in_model___exception_raised, std::runtime_error, checkMessage("No constraint 'some_constraint' found.")); } + +BOOST_AUTO_TEST_CASE(locationToString) +{ + BOOST_CHECK_EQUAL(LocationToStr(Location::MASTER), "master"); + BOOST_CHECK_EQUAL(LocationToStr(Location::MASTER_AND_SUBPROBLEMS), "master-and-subproblems"); + BOOST_CHECK_EQUAL(LocationToStr(Location::SUBPROBLEMS), "subproblems"); +} + +BOOST_AUTO_TEST_CASE(locationsCompatibleForFillers) +{ + BOOST_CHECK(AreLocationsCompatibleForFillers(Location::MASTER, Location::MASTER)); + BOOST_CHECK(AreLocationsCompatibleForFillers(Location::SUBPROBLEMS, Location::SUBPROBLEMS)); + BOOST_CHECK(!AreLocationsCompatibleForFillers(Location::SUBPROBLEMS, Location::MASTER)); + BOOST_CHECK(!AreLocationsCompatibleForFillers(Location::MASTER, Location::SUBPROBLEMS)); + BOOST_CHECK( + AreLocationsCompatibleForFillers(Location::MASTER_AND_SUBPROBLEMS, Location::MASTER)); + BOOST_CHECK( + AreLocationsCompatibleForFillers(Location::MASTER_AND_SUBPROBLEMS, Location::SUBPROBLEMS)); + BOOST_CHECK(AreLocationsCompatibleForFillers(Location::MASTER_AND_SUBPROBLEMS, + Location::MASTER_AND_SUBPROBLEMS)); +} + +BOOST_AUTO_TEST_CASE(locationsCompatibleForExpressions) +{ + BOOST_CHECK(AreLocationsCompatibleForExpressions(Location::MASTER, Location::MASTER)); + BOOST_CHECK(AreLocationsCompatibleForExpressions(Location::SUBPROBLEMS, Location::SUBPROBLEMS)); + BOOST_CHECK(AreLocationsCompatibleForExpressions(Location::MASTER_AND_SUBPROBLEMS, + Location::MASTER_AND_SUBPROBLEMS)); + BOOST_CHECK( + !AreLocationsCompatibleForExpressions(Location::MASTER, Location::MASTER_AND_SUBPROBLEMS)); + BOOST_CHECK(!AreLocationsCompatibleForExpressions(Location::SUBPROBLEMS, + Location::MASTER_AND_SUBPROBLEMS)); + BOOST_CHECK(!AreLocationsCompatibleForExpressions(Location::SUBPROBLEMS, Location::MASTER)); + BOOST_CHECK(!AreLocationsCompatibleForExpressions(Location::MASTER, Location::SUBPROBLEMS)); +}