diff --git a/src/solver/CMakeLists.txt b/src/solver/CMakeLists.txt index adf9825864..5ae822209b 100644 --- a/src/solver/CMakeLists.txt +++ b/src/solver/CMakeLists.txt @@ -23,6 +23,7 @@ add_subdirectory(simulation) add_subdirectory(ts-generator) add_subdirectory(utils) add_subdirectory(variable) +add_subdirectory(optim-model-filler) # # Resource file for Windows diff --git a/src/solver/expressions/include/antares/solver/expressions/NodeRegistry.h b/src/solver/expressions/include/antares/solver/expressions/NodeRegistry.h index 994b8f6886..22b40bf011 100644 --- a/src/solver/expressions/include/antares/solver/expressions/NodeRegistry.h +++ b/src/solver/expressions/include/antares/solver/expressions/NodeRegistry.h @@ -9,14 +9,13 @@ class NodeRegistry { public: NodeRegistry() = default; - NodeRegistry(Antares::Solver::Nodes::Node* node, - Antares::Solver::Registry registry); + NodeRegistry(Nodes::Node* node, Registry registry); // Shallow copy NodeRegistry(NodeRegistry&&) = default; NodeRegistry& operator=(NodeRegistry&&) = default; - Antares::Solver::Nodes::Node* node; - Antares::Solver::Registry registry; + Nodes::Node* node; + Registry registry; }; } // namespace Antares::Solver diff --git a/src/solver/modeler/api/include/antares/solver/modeler/api/linearProblem.h b/src/solver/modeler/api/include/antares/solver/modeler/api/linearProblem.h index 94c95e4e31..6885d81ac1 100644 --- a/src/solver/modeler/api/include/antares/solver/modeler/api/linearProblem.h +++ b/src/solver/modeler/api/include/antares/solver/modeler/api/linearProblem.h @@ -35,7 +35,7 @@ namespace Antares::Solver::Modeler::Api /** * Linear Problem * This class is aimed at creating and manipulating variables/constraints - * Also used to to control the objective, maximization or minimization, and to solve the problem + * Also used to control the objective, maximization or minimization, and to solve the problem */ class ILinearProblem { diff --git a/src/solver/modeler/api/include/antares/solver/modeler/api/mipVariable.h b/src/solver/modeler/api/include/antares/solver/modeler/api/mipVariable.h index 1caac002bf..c8e93acb53 100644 --- a/src/solver/modeler/api/include/antares/solver/modeler/api/mipVariable.h +++ b/src/solver/modeler/api/include/antares/solver/modeler/api/mipVariable.h @@ -29,6 +29,8 @@ namespace Antares::Solver::Modeler::Api class IMipVariable: public IHasBounds, public IHasName { +public: + virtual bool isInteger() const = 0; }; } // namespace Antares::Solver::Modeler::Api diff --git a/src/solver/modeler/ortoolsImpl/include/antares/solver/modeler/ortoolsImpl/mipVariable.h b/src/solver/modeler/ortoolsImpl/include/antares/solver/modeler/ortoolsImpl/mipVariable.h index 528ee0cd9e..a1c0a3fa91 100644 --- a/src/solver/modeler/ortoolsImpl/include/antares/solver/modeler/ortoolsImpl/mipVariable.h +++ b/src/solver/modeler/ortoolsImpl/include/antares/solver/modeler/ortoolsImpl/mipVariable.h @@ -44,6 +44,8 @@ class OrtoolsMipVariable final: public Api::IMipVariable const std::string& getName() const override; + bool isInteger() const override; + const operations_research::MPVariable* getMpVar() const; ~OrtoolsMipVariable() override = default; diff --git a/src/solver/modeler/ortoolsImpl/mipVariable.cpp b/src/solver/modeler/ortoolsImpl/mipVariable.cpp index 5e21924a8f..b29d0000a3 100644 --- a/src/solver/modeler/ortoolsImpl/mipVariable.cpp +++ b/src/solver/modeler/ortoolsImpl/mipVariable.cpp @@ -66,4 +66,9 @@ const std::string& OrtoolsMipVariable::getName() const return mpVar_->name(); } +bool OrtoolsMipVariable::isInteger() const +{ + return mpVar_->integer(); +} + } // namespace Antares::Solver::Modeler::OrtoolsImpl diff --git a/src/solver/optim-model-filler/CMakeLists.txt b/src/solver/optim-model-filler/CMakeLists.txt new file mode 100644 index 0000000000..8fb6bdc0e0 --- /dev/null +++ b/src/solver/optim-model-filler/CMakeLists.txt @@ -0,0 +1,29 @@ +set(PROJ optim-model-filler) + +set(SRC_optim_model_filler + include/antares/solver/optim-model-filler/ComponentFiller.h + include/antares/solver/optim-model-filler/LinearExpression.h + include/antares/solver/optim-model-filler/ReadLinearConstraintVisitor.h + include/antares/solver/optim-model-filler/ReadLinearExpressionVisitor.h + ComponentFiller.cpp + LinearExpression.cpp + ReadLinearConstraintVisitor.cpp + ReadLinearExpressionVisitor.cpp +) + +add_library(${PROJ} ${SRC_optim_model_filler}) +add_library(Antares::${PROJ} ALIAS ${PROJ}) + +set_target_properties(${PROJ} PROPERTIES LINKER_LANGUAGE CXX) + +target_link_libraries(${PROJ} + PRIVATE + Antares::solver-expressions + Antares::antares-study-system-model + Antares::modeler_api +) + +target_include_directories(${PROJ} + PUBLIC + $ +) diff --git a/src/solver/optim-model-filler/ComponentFiller.cpp b/src/solver/optim-model-filler/ComponentFiller.cpp new file mode 100644 index 0000000000..49a66cda11 --- /dev/null +++ b/src/solver/optim-model-filler/ComponentFiller.cpp @@ -0,0 +1,95 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#include + +#include +#include +#include +#include +#include + +namespace Antares::Optimization +{ + +ComponentFiller::ComponentFiller(const Study::SystemModel::Component& component): + component_(component), + evaluationContext_(component_.getParameterValues(), {}) +{ +} + +void ComponentFiller::addVariables(Solver::Modeler::Api::ILinearProblem& pb, + Solver::Modeler::Api::LinearProblemData& data, + Solver::Modeler::Api::FillContext& ctx) +{ + auto evaluator = std::make_unique(evaluationContext_); + for (const auto& variable: component_.getModel()->Variables() | std::views::values) + { + pb.addVariable(evaluator->dispatch(variable.LowerBound().RootNode()), + evaluator->dispatch(variable.UpperBound().RootNode()), + variable.Type() != Study::SystemModel::ValueType::FLOAT, + component_.Id() + "." + variable.Id()); + } +} + +void ComponentFiller::addConstraints(Solver::Modeler::Api::ILinearProblem& pb, + Solver::Modeler::Api::LinearProblemData& data, + Solver::Modeler::Api::FillContext& ctx) +{ + ReadLinearConstraintVisitor visitor(evaluationContext_); + for (const auto& constraint: component_.getModel()->getConstraints() | std::views::values) + { + auto linear_constraint = visitor.dispatch(constraint.expression().RootNode()); + auto* ct = pb.addConstraint(linear_constraint.lb, + linear_constraint.ub, + component_.Id() + "." + constraint.Id()); + for (auto [var_id, coef]: linear_constraint.coef_per_var) + { + auto* variable = pb.getVariable(component_.Id() + "." + var_id); + ct->setCoefficient(variable, coef); + } + } +} + +void ComponentFiller::addObjective(Solver::Modeler::Api::ILinearProblem& pb, + Solver::Modeler::Api::LinearProblemData& data, + Solver::Modeler::Api::FillContext& ctx) +{ + if (component_.getModel()->Objective().Empty()) + { + return; + } + ReadLinearExpressionVisitor visitor(evaluationContext_); + auto linear_expression = visitor.dispatch(component_.getModel()->Objective().RootNode()); + if (abs(linear_expression.offset()) > 1e-10) + { + throw std::invalid_argument("Antares does not support objective offsets (found in model '" + + component_.getModel()->Id() + "' of component '" + + component_.Id() + "')."); + } + for (auto [var_id, coef]: linear_expression.coefPerVar()) + { + auto* variable = pb.getVariable(component_.Id() + "." + var_id); + pb.setObjectiveCoefficient(variable, coef); + } +} + +} // namespace Antares::Optimization diff --git a/src/solver/optim-model-filler/LinearExpression.cpp b/src/solver/optim-model-filler/LinearExpression.cpp new file mode 100644 index 0000000000..dd3d961215 --- /dev/null +++ b/src/solver/optim-model-filler/LinearExpression.cpp @@ -0,0 +1,120 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#include + +#include + +namespace Antares::Optimization +{ + +/** + * Element-wise sum of two [string, double] maps, preceded an element-wise multiplication of the + * right-hand-side map. Keys that do not exist in one of the two maps are considered to have a zero + * value. For every key: value = left_value + rhs_multiplier * right_value + * @param left The left hand side map + * @param right The right hand side map + * @param rhs_multiplier The multiplier to apply to the right hand side map + * @return The map resulting from the operation + */ +static std::map add_maps(const std::map& left, + const std::map& right, + double rhs_multiplier) +{ + std::map result(left); + for (auto [key, value]: right) + { + if (result.contains(key)) + { + result[key] += rhs_multiplier * value; + } + else + { + result[key] = rhs_multiplier * value; + } + } + return result; +} + +/** + * Element-wise multiplication of a map by a scale. + * For every key: final_value = scale * initial_value + * @param map The [string, double] map to scale + * @param scale The scale + * @return The scaled map + */ +static std::map scale_map(const std::map& map, + double scale) +{ + std::map result; + for (auto [key, value]: map) + { + result[key] = scale * value; + } + return result; +} + +LinearExpression::LinearExpression(double offset, std::map coef_per_var): + offset_(offset), + coef_per_var_(std::move(coef_per_var)) +{ +} + +LinearExpression LinearExpression::operator+(const LinearExpression& other) const +{ + return {offset_ + other.offset_, add_maps(coef_per_var_, other.coef_per_var_, 1)}; +} + +LinearExpression LinearExpression::operator-(const LinearExpression& other) const +{ + return {offset_ - other.offset_, add_maps(coef_per_var_, other.coef_per_var_, -1)}; +} + +LinearExpression LinearExpression::operator*(const LinearExpression& other) const +{ + if (coef_per_var_.empty()) + { + return {offset_ * other.offset_, scale_map(other.coef_per_var_, offset_)}; + } + else if (other.coef_per_var_.empty()) + { + return {offset_ * other.offset_, scale_map(coef_per_var_, other.offset_)}; + } + else + { + throw std::invalid_argument("A linear expression can't have quadratic terms."); + } +} + +LinearExpression LinearExpression::operator/(const LinearExpression& other) const +{ + if (!other.coef_per_var_.empty()) + { + throw std::invalid_argument("A linear expression can't have a variable as a dividend."); + } + return LinearExpression(offset_ / other.offset_, scale_map(coef_per_var_, 1 / other.offset_)); +} + +LinearExpression LinearExpression::negate() const +{ + return {-offset_, scale_map(coef_per_var_, -1)}; +} +} // namespace Antares::Optimization diff --git a/src/solver/optim-model-filler/ReadLinearConstraintVisitor.cpp b/src/solver/optim-model-filler/ReadLinearConstraintVisitor.cpp new file mode 100644 index 0000000000..9e33b5868e --- /dev/null +++ b/src/solver/optim-model-filler/ReadLinearConstraintVisitor.cpp @@ -0,0 +1,131 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#include +#include +#include + +using namespace Antares::Solver::Nodes; + +namespace Antares::Optimization +{ + +ReadLinearConstraintVisitor::ReadLinearConstraintVisitor( + Solver::Visitors::EvaluationContext context): + linear_expression_visitor_(std::move(context)) +{ +} + +std::string ReadLinearConstraintVisitor::name() const +{ + return "ReadLinearConstraintVisitor"; +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const EqualNode* node) +{ + auto leftMinusRight = linear_expression_visitor_.dispatch(node->left()) + - linear_expression_visitor_.dispatch(node->right()); + return LinearConstraint{.coef_per_var = leftMinusRight.coefPerVar(), + .lb = -leftMinusRight.offset(), + .ub = -leftMinusRight.offset()}; +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const LessThanOrEqualNode* node) +{ + auto leftMinusRight = linear_expression_visitor_.dispatch(node->left()) + - linear_expression_visitor_.dispatch(node->right()); + return LinearConstraint{.coef_per_var = leftMinusRight.coefPerVar(), + .ub = -leftMinusRight.offset()}; +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const GreaterThanOrEqualNode* node) +{ + auto leftMinusRight = linear_expression_visitor_.dispatch(node->left()) + - linear_expression_visitor_.dispatch(node->right()); + return LinearConstraint{.coef_per_var = leftMinusRight.coefPerVar(), + .lb = -leftMinusRight.offset()}; +} + +static std::invalid_argument IllegalNodeException() +{ + return std::invalid_argument("Root node of a constraint must be a comparator."); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const SumNode* sum_node) +{ + throw IllegalNodeException(); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const SubtractionNode* node) +{ + throw IllegalNodeException(); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const MultiplicationNode* node) +{ + throw IllegalNodeException(); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const DivisionNode* node) +{ + throw IllegalNodeException(); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const NegationNode* node) +{ + throw IllegalNodeException(); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const VariableNode* node) +{ + throw IllegalNodeException(); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const ParameterNode* node) +{ + throw IllegalNodeException(); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const LiteralNode* node) +{ + throw IllegalNodeException(); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const PortFieldNode* node) +{ + throw IllegalNodeException(); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const PortFieldSumNode* node) +{ + throw IllegalNodeException(); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const ComponentVariableNode* node) +{ + throw IllegalNodeException(); +} + +LinearConstraint ReadLinearConstraintVisitor::visit(const ComponentParameterNode* node) +{ + throw IllegalNodeException(); +} +} // namespace Antares::Optimization diff --git a/src/solver/optim-model-filler/ReadLinearExpressionVisitor.cpp b/src/solver/optim-model-filler/ReadLinearExpressionVisitor.cpp new file mode 100644 index 0000000000..ccc722bdc0 --- /dev/null +++ b/src/solver/optim-model-filler/ReadLinearExpressionVisitor.cpp @@ -0,0 +1,124 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#include + +#include +#include +#include +#include + +using namespace Antares::Solver::Nodes; + +namespace Antares::Optimization +{ + +ReadLinearExpressionVisitor::ReadLinearExpressionVisitor( + Solver::Visitors::EvaluationContext context): + context_(std::move(context)) +{ +} + +std::string ReadLinearExpressionVisitor::name() const +{ + return "ReadLinearExpressionVisitor"; +} + +LinearExpression ReadLinearExpressionVisitor::visit(const SumNode* node) +{ + auto operands = node->getOperands(); + return std::accumulate(std::begin(operands), + std::end(operands), + LinearExpression(), + [this](LinearExpression sum, Node* operand) + { return sum + dispatch(operand); }); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const SubtractionNode* node) +{ + return dispatch(node->left()) - dispatch(node->right()); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const MultiplicationNode* node) +{ + return dispatch(node->left()) * dispatch(node->right()); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const DivisionNode* node) +{ + return dispatch(node->left()) / dispatch(node->right()); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const EqualNode* node) +{ + throw std::invalid_argument("A linear expression can't contain comparison operators."); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const LessThanOrEqualNode* node) +{ + throw std::invalid_argument("A linear expression can't contain comparison operators."); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const GreaterThanOrEqualNode* node) +{ + throw std::invalid_argument("A linear expression can't contain comparison operators."); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const NegationNode* node) +{ + return dispatch(node->child()).negate(); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const VariableNode* node) +{ + return LinearExpression(0, {{node->value(), 1}}); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const ParameterNode* node) +{ + return {context_.getParameterValue(node->value()), {}}; +} + +LinearExpression ReadLinearExpressionVisitor::visit(const LiteralNode* node) +{ + return {node->value(), {}}; +} + +LinearExpression ReadLinearExpressionVisitor::visit(const PortFieldNode* node) +{ + throw std::invalid_argument("ReadLinearExpressionVisitor cannot visit PortFieldNodes"); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const PortFieldSumNode* node) +{ + throw std::invalid_argument("ReadLinearExpressionVisitor cannot visit PortFieldSumNodes"); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const ComponentVariableNode* node) +{ + throw std::invalid_argument("ReadLinearExpressionVisitor cannot visit ComponentVariableNodes"); +} + +LinearExpression ReadLinearExpressionVisitor::visit(const ComponentParameterNode* node) +{ + throw std::invalid_argument("ReadLinearExpressionVisitor cannot visit ComponentParameterNodes"); +} +} // namespace Antares::Optimization 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 new file mode 100644 index 0000000000..db75c170b5 --- /dev/null +++ b/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/ComponentFiller.h @@ -0,0 +1,62 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#pragma once + +#include +#include +#include "antares/solver/expressions/visitors/EvaluationContext.h" + +namespace Antares::Study::SystemModel +{ +class Component; +} + +namespace Antares::Optimization +{ +/** + * Component filler + * Implements LinearProbleFiller interface. + * Fills a LinearProblem with variables, constraints, and objective coefficients of a Component + */ +class ComponentFiller: public Solver::Modeler::Api::LinearProblemFiller +{ +public: + ComponentFiller() = delete; + ComponentFiller(ComponentFiller& other) = delete; + /// Create a ComponentFiller for a Component + explicit ComponentFiller(const Study::SystemModel::Component& component); + + void addVariables(Solver::Modeler::Api::ILinearProblem& pb, + Solver::Modeler::Api::LinearProblemData& data, + Solver::Modeler::Api::FillContext& ctx) override; + void addConstraints(Solver::Modeler::Api::ILinearProblem& pb, + Solver::Modeler::Api::LinearProblemData& data, + Solver::Modeler::Api::FillContext& ctx) override; + void addObjective(Solver::Modeler::Api::ILinearProblem& pb, + Solver::Modeler::Api::LinearProblemData& data, + Solver::Modeler::Api::FillContext& ctx) override; + +private: + const Study::SystemModel::Component& component_; + Solver::Visitors::EvaluationContext evaluationContext_; +}; +} // namespace Antares::Optimization diff --git a/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/LinearExpression.h b/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/LinearExpression.h new file mode 100644 index 0000000000..e53877b774 --- /dev/null +++ b/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/LinearExpression.h @@ -0,0 +1,73 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#pragma once + +#include +#include + +namespace Antares::Optimization +{ +/** + * Linear Expression + * Represents an expression that is linear in regard to an optimization problem's variables. + * It can be fully defined by: + * - the non-zero coefficients of the variables + * - a scalar offset + */ +class LinearExpression +{ +public: + /// Build a linear expression with zero offset and zero coefficients + LinearExpression() = default; + /// Build a linear expression with a given offset and a given map of non-zero coefficients per + /// variable ID + LinearExpression(double offset, std::map coef_per_var); + /// Sum two linear expressions + LinearExpression operator+(const LinearExpression& other) const; + /// Subtract two linear expressions + LinearExpression operator-(const LinearExpression& other) const; + /// Multiply two linear expressions + /// Only one can have non-zero coefficients, otherwise the result cannot be linear + LinearExpression operator*(const LinearExpression& other) const; + /// Divide two linear expressions + /// Only first expression can have non-zero coefficients, otherwise the result cannot be linear + LinearExpression operator/(const LinearExpression& other) const; + /// Multiply linear expression by -1 + LinearExpression negate() const; + + /// Get the offset + double offset() const + { + return offset_; + } + + /// Get the non-zero coefficients per variable ID + std::map coefPerVar() const + { + return coef_per_var_; + } + +private: + double offset_ = 0; + std::map coef_per_var_; +}; +} // namespace Antares::Optimization diff --git a/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/ReadLinearConstraintVisitor.h b/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/ReadLinearConstraintVisitor.h new file mode 100644 index 0000000000..dfddc5db2f --- /dev/null +++ b/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/ReadLinearConstraintVisitor.h @@ -0,0 +1,76 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#pragma once + +#include +#include + +#include "ReadLinearExpressionVisitor.h" + +/** + * Read Linear Constraint Visitor + * Visits a Node and produces a Linear Constraint (defined by its Linear Expression and bounds). + * The root node is expected to be a comparison node. + */ +namespace Antares::Optimization +{ + +/** + * Linear Constraint + * Represents a linear constraint in an optimization problem. + * It is fully defined by: + * - a Linear Expression (defined by an offset and non-zero variable coefficients) + * - a lower and an upper bounds + */ +struct LinearConstraint +{ + std::map coef_per_var; + double lb = -std::numeric_limits::infinity(); + double ub = std::numeric_limits::infinity(); +}; + +class ReadLinearConstraintVisitor: public Solver::Visitors::NodeVisitor +{ +public: + ReadLinearConstraintVisitor() = default; + explicit ReadLinearConstraintVisitor(Solver::Visitors::EvaluationContext context); + std::string name() const override; + +private: + ReadLinearExpressionVisitor linear_expression_visitor_; + LinearConstraint visit(const Solver::Nodes::SumNode* node) override; + LinearConstraint visit(const Solver::Nodes::SubtractionNode* node) override; + LinearConstraint visit(const Solver::Nodes::MultiplicationNode* node) override; + LinearConstraint visit(const Solver::Nodes::DivisionNode* node) override; + LinearConstraint visit(const Solver::Nodes::EqualNode* node) override; + LinearConstraint visit(const Solver::Nodes::LessThanOrEqualNode* node) override; + LinearConstraint visit(const Solver::Nodes::GreaterThanOrEqualNode* node) override; + LinearConstraint visit(const Solver::Nodes::NegationNode* node) override; + LinearConstraint visit(const Solver::Nodes::VariableNode* node) override; + LinearConstraint visit(const Solver::Nodes::ParameterNode* node) override; + LinearConstraint visit(const Solver::Nodes::LiteralNode* node) override; + LinearConstraint visit(const Solver::Nodes::PortFieldNode* node) override; + LinearConstraint visit(const Solver::Nodes::PortFieldSumNode* node) override; + LinearConstraint visit(const Solver::Nodes::ComponentVariableNode* node) override; + LinearConstraint visit(const Solver::Nodes::ComponentParameterNode* node) override; +}; +} // namespace Antares::Optimization diff --git a/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/ReadLinearExpressionVisitor.h b/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/ReadLinearExpressionVisitor.h new file mode 100644 index 0000000000..ed86bf9617 --- /dev/null +++ b/src/solver/optim-model-filler/include/antares/solver/optim-model-filler/ReadLinearExpressionVisitor.h @@ -0,0 +1,62 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#pragma once + +#include +#include +#include + +/** + * Read Linear Expression Visitor + * Visits a Node and produces a Linear Expression (defined by an offset and non-zero + * coefficients of variables) + * Comparison Nodes are not allowed + */ +namespace Antares::Optimization +{ + +class ReadLinearExpressionVisitor: public Solver::Visitors::NodeVisitor +{ +public: + ReadLinearExpressionVisitor() = default; + explicit ReadLinearExpressionVisitor(Solver::Visitors::EvaluationContext context); + std::string name() const override; + +private: + const Solver::Visitors::EvaluationContext context_; + LinearExpression visit(const Solver::Nodes::SumNode* node) override; + LinearExpression visit(const Solver::Nodes::SubtractionNode* node) override; + LinearExpression visit(const Solver::Nodes::MultiplicationNode* node) override; + LinearExpression visit(const Solver::Nodes::DivisionNode* node) override; + LinearExpression visit(const Solver::Nodes::EqualNode* node) override; + LinearExpression visit(const Solver::Nodes::LessThanOrEqualNode* node) override; + LinearExpression visit(const Solver::Nodes::GreaterThanOrEqualNode* node) override; + LinearExpression visit(const Solver::Nodes::NegationNode* node) override; + LinearExpression visit(const Solver::Nodes::VariableNode* node) override; + LinearExpression visit(const Solver::Nodes::ParameterNode* node) override; + LinearExpression visit(const Solver::Nodes::LiteralNode* node) override; + LinearExpression visit(const Solver::Nodes::PortFieldNode* node) override; + LinearExpression visit(const Solver::Nodes::PortFieldSumNode* node) override; + LinearExpression visit(const Solver::Nodes::ComponentVariableNode* node) override; + LinearExpression visit(const Solver::Nodes::ComponentParameterNode* node) override; +}; +} // namespace Antares::Optimization diff --git a/src/study/system-model/CMakeLists.txt b/src/study/system-model/CMakeLists.txt index 19cbfa7abb..c7e2208c41 100644 --- a/src/study/system-model/CMakeLists.txt +++ b/src/study/system-model/CMakeLists.txt @@ -31,7 +31,7 @@ target_include_directories(antares-study-system-model ) target_link_libraries(antares-study-system-model PUBLIC - Antares::solver-expressions + Antares::solver-expressions ) install(DIRECTORY include/antares DESTINATION "include" diff --git a/src/study/system-model/component.cpp b/src/study/system-model/component.cpp index b9b7c6a03b..1c3d360e64 100644 --- a/src/study/system-model/component.cpp +++ b/src/study/system-model/component.cpp @@ -81,7 +81,7 @@ ComponentBuilder& ComponentBuilder::withId(const std::string_view id) * \param model The model to set. * \return Reference to the ComponentBuilder object. */ -ComponentBuilder& ComponentBuilder::withModel(Model* model) +ComponentBuilder& ComponentBuilder::withModel(const Model* model) { data_.model = model; return *this; @@ -118,9 +118,11 @@ ComponentBuilder& ComponentBuilder::withScenarioGroupId(const std::string& scena * * \return The constructed Component object. */ -Component ComponentBuilder::build() const +Component ComponentBuilder::build() { - return Component(data_); + Component component(data_); + data_.reset(); // makes the ComponentBuilder re-usable + return component; } } // namespace Antares::Study::SystemModel 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 d3be08ba80..6195a6496c 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 @@ -31,12 +31,21 @@ namespace Antares::Study::SystemModel * Defines the attributes of the Component class * Made into a struct to avoid duplication in ComponentBuilder */ -struct ComponentData +class ComponentData { +public: std::string id; - Model* model = nullptr; + const Model* model = nullptr; std::map parameter_values; std::string scenario_group_id; + + void reset() + { + id.clear(); + model = nullptr; + parameter_values.clear(); + scenario_group_id.clear(); + } }; /** @@ -53,11 +62,16 @@ class Component return data_.id; } - Model* getModel() const + const Model* getModel() const { return data_.model; } + const std::map& getParameterValues() const + { + return data_.parameter_values; + } + double getParameterValue(const std::string& parameter_id) const { if (!data_.parameter_values.contains(parameter_id)) @@ -84,10 +98,10 @@ class ComponentBuilder { public: ComponentBuilder& withId(std::string_view id); - ComponentBuilder& withModel(Model* model); + ComponentBuilder& withModel(const Model* model); ComponentBuilder& withParameterValues(std::map parameter_values); ComponentBuilder& withScenarioGroupId(const std::string& scenario_group_id); - Component build() const; + Component build(); private: ComponentData data_; diff --git a/src/study/system-model/include/antares/study/system-model/expression.h b/src/study/system-model/include/antares/study/system-model/expression.h index 0b52d82f91..cf8ea30a61 100644 --- a/src/study/system-model/include/antares/study/system-model/expression.h +++ b/src/study/system-model/include/antares/study/system-model/expression.h @@ -32,14 +32,16 @@ class Node; namespace Antares::Study::SystemModel { +// TODO: add unit tests for this class class Expression { public: Expression() = default; - explicit Expression(const std::string& value, Antares::Solver::NodeRegistry root): + explicit Expression(const std::string& value, Solver::NodeRegistry root): value_(value), - root_(std::move(root)) + root_(std::move(root)), + empty_(false) { } @@ -48,9 +50,20 @@ class Expression return value_; } + Solver::Nodes::Node* RootNode() const + { + return root_.node; + } + + bool Empty() const + { + return empty_; + } + private: std::string value_; - Antares::Solver::NodeRegistry root_; + Solver::NodeRegistry root_; + bool empty_ = true; }; } // namespace Antares::Study::SystemModel 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 a050424131..a878838315 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 @@ -36,6 +36,7 @@ namespace Antares::Study::SystemModel * Defines a model that can be referenced by actual components. * A model defines the behaviour of those components. */ +// TODO: add unit tests for this class class Model { public: @@ -69,6 +70,7 @@ class Model const std::map& Variables() const { + // TODO : convert to vector? return variables_; } diff --git a/src/study/system-model/model.cpp b/src/study/system-model/model.cpp index fd77cb3cf2..18d6b15e8c 100644 --- a/src/study/system-model/model.cpp +++ b/src/study/system-model/model.cpp @@ -36,7 +36,9 @@ namespace Antares::Study::SystemModel */ Model ModelBuilder::build() { - return std::move(model_); + Model model = std::move(model_); + model_ = Model(); // makes ModelBuilder re-usable + return std::move(model); } /** diff --git a/src/tests/src/solver/CMakeLists.txt b/src/tests/src/solver/CMakeLists.txt index 2152a8e1bf..9cf4843bd8 100644 --- a/src/tests/src/solver/CMakeLists.txt +++ b/src/tests/src/solver/CMakeLists.txt @@ -4,5 +4,6 @@ add_subdirectory(lps) add_subdirectory(modelParser) add_subdirectory(modeler) add_subdirectory(optimisation) +add_subdirectory(optim-model-filler) add_subdirectory(simulation) add_subdirectory(utils) diff --git a/src/tests/src/solver/modeler/api/testModelerLinearProblemWithOrtools.cpp b/src/tests/src/solver/modeler/api/testModelerLinearProblemWithOrtools.cpp index 7c1f8f84e3..efad36d77c 100644 --- a/src/tests/src/solver/modeler/api/testModelerLinearProblemWithOrtools.cpp +++ b/src/tests/src/solver/modeler/api/testModelerLinearProblemWithOrtools.cpp @@ -65,6 +65,7 @@ BOOST_FIXTURE_TEST_CASE(add_int_variable_to_problem___check_var_exists, FixtureE pb->addIntVariable(5, 15, "var"); auto* var = pb->getVariable("var"); BOOST_CHECK(var); + BOOST_CHECK(var->isInteger()); BOOST_CHECK_EQUAL(var->getLb(), 5); BOOST_CHECK_EQUAL(var->getUb(), 15); } @@ -74,6 +75,7 @@ BOOST_FIXTURE_TEST_CASE(add_num_variable_to_problem___check_var_exists, FixtureE pb->addNumVariable(2., 7., "var"); auto* var = pb->getVariable("var"); BOOST_CHECK(var); + BOOST_CHECK(!var->isInteger()); BOOST_CHECK_EQUAL(var->getLb(), 2.); BOOST_CHECK_EQUAL(var->getUb(), 7.); } diff --git a/src/tests/src/solver/optim-model-filler/CMakeLists.txt b/src/tests/src/solver/optim-model-filler/CMakeLists.txt new file mode 100644 index 0000000000..fa81c819b7 --- /dev/null +++ b/src/tests/src/solver/optim-model-filler/CMakeLists.txt @@ -0,0 +1,29 @@ +set(EXECUTABLE_NAME unit-tests-for-component-filler) +add_executable(${EXECUTABLE_NAME}) + +target_sources(${EXECUTABLE_NAME} + PRIVATE + test_main.cpp + test_componentFiller.cpp + test_linearExpression.cpp + test_readLinearExpressionVisitor.cpp + test_readLinearConstraintVisitor.cpp +) +target_include_directories(${EXECUTABLE_NAME} + PRIVATE + "${src_solver_optimisation}" +) + +target_link_libraries(${EXECUTABLE_NAME} + PRIVATE + Boost::unit_test_framework + antares-study-system-model + Antares::modeler-ortools-impl + Antares::optim-model-filler + test_utils_unit +) + +set_target_properties(${EXECUTABLE_NAME} PROPERTIES FOLDER Unit-tests) +add_test(NAME ${EXECUTABLE_NAME} COMMAND ${EXECUTABLE_NAME}) +set_property(TEST ${EXECUTABLE_NAME} PROPERTY LABELS unit) + diff --git a/src/tests/src/solver/optim-model-filler/test_componentFiller.cpp b/src/tests/src/solver/optim-model-filler/test_componentFiller.cpp new file mode 100644 index 0000000000..c1a5a52ec9 --- /dev/null +++ b/src/tests/src/solver/optim-model-filler/test_componentFiller.cpp @@ -0,0 +1,514 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#define WIN32_LEAN_AND_MEAN + +#include + +#include "antares/solver/expressions/nodes/ExpressionsNodes.h" +#include "antares/solver/modeler/api/linearProblemBuilder.h" +#include "antares/solver/modeler/ortoolsImpl/linearProblem.h" +#include "antares/solver/optim-model-filler/ComponentFiller.h" +#include "antares/study/system-model/component.h" +#include "antares/study/system-model/parameter.h" + +#include "unit_test_utils.h" + +using namespace Antares::Solver::Modeler::Api; +using namespace Antares::Study::SystemModel; +using namespace Antares::Optimization; +using namespace Antares::Solver::Nodes; +using namespace std; + +struct VariableData +{ + string id; + ValueType type; + Node* lb; + Node* ub; +}; + +struct ConstraintData +{ + string id; + Node* expression; +}; + +struct LinearProblemBuildingFixture +{ + map models; + Antares::Solver::Registry nodes; + vector components; + unique_ptr pb; + + void createModel(string modelId, + vector parameterIds, + vector variablesData, + vector constraintsData, + Node* objective = nullptr); + + void createModelWithOneFloatVar(const string& modelId, + const vector& parameterIds, + const string& varId, + Node* lb, + Node* ub, + const vector& constraintsData, + Node* objective = nullptr) + { + createModel(modelId, + parameterIds, + {{varId, ValueType::FLOAT, lb, ub}}, + constraintsData, + objective); + } + + void createComponent(const string& modelId, + const string& componentId, + map parameterValues = {}); + + Node* literal(double value) + { + return nodes.create(value); + } + + Node* parameter(const string& paramId) + { + return nodes.create(paramId); + } + + Node* variable(const string& varId) + { + return nodes.create(varId); + } + + Node* multiply(Node* node1, Node* node2) + { + return nodes.create(node1, node2); + } + + Node* negate(Node* node) + { + return nodes.create(node); + } + + void buildLinearProblem(); +}; + +void LinearProblemBuildingFixture::createModel(string modelId, + vector parameterIds, + vector variablesData, + vector constraintsData, + Node* objective) +{ + auto createExpression = [this](Node* node) + { + Antares::Solver::NodeRegistry node_registry(node, move(nodes)); + Expression expression("expression", move(node_registry)); + return move(expression); + }; + vector parameters; + for (auto parameter_id: parameterIds) + { + parameters.push_back( + Parameter(parameter_id, Parameter::TimeDependent::NO, Parameter::ScenarioDependent::NO)); + } + vector variables; + for (auto [id, type, lb, ub]: variablesData) + { + variables.push_back(move(Variable(id, createExpression(lb), createExpression(ub), type))); + } + vector constraints; + for (auto [id, expression]: constraintsData) + { + constraints.push_back(move(Constraint(id, createExpression(expression)))); + } + ModelBuilder model_builder; + model_builder.withId(modelId) + .withParameters(move(parameters)) + .withVariables(move(variables)) + .withConstraints(move(constraints)); + if (objective) + { + model_builder.withObjective(createExpression(objective)); + } + auto model = model_builder.build(); + models[modelId] = move(model); +} + +void LinearProblemBuildingFixture::createComponent(const string& modelId, + const string& componentId, + map parameterValues) +{ + BOOST_CHECK_NO_THROW(models.at(modelId)); + ComponentBuilder component_builder; + auto component = component_builder.withId(componentId) + .withModel(&models.at(modelId)) + .withScenarioGroupId("scenario_group") + .withParameterValues(move(parameterValues)) + .build(); + components.push_back(move(component)); +} + +void LinearProblemBuildingFixture::buildLinearProblem() +{ + vector> fillers; + vector fillers_ptr; + for (auto& component: components) + { + auto cf = make_unique(component); + fillers.push_back(move(cf)); + } + for (auto& component_filler: fillers) + { + fillers_ptr.push_back(component_filler.get()); + } + pb = make_unique(false, "scip"); + LinearProblemBuilder linear_problem_builder(fillers_ptr); + LinearProblemData dummy_data; + FillContext dummy_time_scenario_ctx = {0, 0}; + linear_problem_builder.build(*pb.get(), dummy_data, dummy_time_scenario_ctx); +} + +BOOST_FIXTURE_TEST_SUITE(_ComponentFiller_addVariables_, LinearProblemBuildingFixture) + +BOOST_AUTO_TEST_CASE(var_with_literal_bounds_to_filler__problem_contains_one_var) +{ + createModelWithOneFloatVar("some_model", {}, "var1", literal(-5), literal(10), {}); + createComponent("some_model", "some_component"); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->variableCount(), 1); + BOOST_CHECK_EQUAL(pb->constraintCount(), 0); + auto* var = pb->getVariable("some_component.var1"); + BOOST_CHECK(var); + BOOST_CHECK_EQUAL(var->getLb(), -5); + BOOST_CHECK_EQUAL(var->getUb(), 10); + BOOST_CHECK(!var->isInteger()); + BOOST_CHECK_EQUAL(pb->getObjectiveCoefficient(var), 0); +} + +BOOST_AUTO_TEST_CASE(var_with_wrong_parameter_lb__exception_is_raised) +{ + createModel("my-model", + {}, + {{"variable", ValueType::FLOAT, parameter("parameter-not-in-model"), literal(10)}}, + {}); + createComponent("my-model", "my-component"); + // TODO : improve exception message in eval visitor + BOOST_CHECK_THROW(buildLinearProblem(), out_of_range); +} + +BOOST_AUTO_TEST_CASE(var_with_wrong_variable_ub__exception_is_raised) +{ + createModel("my-model", + {}, + {{"variable", ValueType::FLOAT, literal(10), variable("variable")}}, + {}); + createComponent("my-model", "my-component"); + // TODO : improve exception message in eval visitor + BOOST_CHECK_THROW(buildLinearProblem(), out_of_range); +} + +BOOST_AUTO_TEST_CASE(two_variables_given_to_different_fillers__LP_contains_the_two_variables) +{ + createModelWithOneFloatVar("m1", {}, "var1", literal(-1), literal(6), {}); + createModelWithOneFloatVar("m2", {}, "var2", literal(-3), literal(2), {}); + createComponent("m1", "component_1"); + createComponent("m2", "component_2"); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->variableCount(), 2); + + auto* var1 = pb->getVariable("component_1.var1"); + BOOST_CHECK(var1); + BOOST_CHECK(!var1->isInteger()); + BOOST_CHECK_EQUAL(var1->getLb(), -1.); + BOOST_CHECK_EQUAL(var1->getUb(), 6.); + + auto* var2 = pb->getVariable("component_2.var2"); + BOOST_CHECK(var2); + BOOST_CHECK(!var2->isInteger()); + BOOST_CHECK_EQUAL(var2->getLb(), -3.); + BOOST_CHECK_EQUAL(var2->getUb(), 2.); +} + +BOOST_AUTO_TEST_CASE(var_whose_bounds_are_parameters_given_to_component__problem_contains_this_var) +{ + createModel("model", + {"pmin", "pmax"}, + {{"var1", ValueType::INTEGER, parameter("pmin"), parameter("pmax")}}, + {}); + createComponent("model", "componentToto", {{"pmin", -3.}, {"pmax", 4.}}); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->variableCount(), 1); + BOOST_CHECK_EQUAL(pb->constraintCount(), 0); + auto* var = pb->getVariable("componentToto.var1"); + BOOST_CHECK(var); + BOOST_CHECK(var->isInteger()); + BOOST_CHECK_EQUAL(var->getLb(), -3.); + BOOST_CHECK_EQUAL(var->getUb(), 4.); +} + +BOOST_AUTO_TEST_CASE(three_different_vars__exist) +{ + VariableData var1 = {"is_cluster_on", ValueType::BOOL, literal(0), literal(1)}; + VariableData var2 = {"n_started_units", ValueType::INTEGER, literal(0), parameter("nUnits")}; + VariableData var3 = {"p_per_unit", ValueType::FLOAT, parameter("pmin"), parameter("pmax")}; + createModel("thermalClusterModel", {"pmin", "pmax", "nUnits"}, {var1, var2, var3}, {}); + createComponent("thermalClusterModel", + "thermalCluster1", + {{"pmin", 100.248}, {"pmax", 950.6784}, {"nUnits", 17.}}); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->variableCount(), 3); + BOOST_CHECK_EQUAL(pb->constraintCount(), 0); + auto* is_cluster_on = pb->getVariable("thermalCluster1.is_cluster_on"); + BOOST_CHECK(is_cluster_on); + BOOST_CHECK(is_cluster_on->isInteger()); + BOOST_CHECK_EQUAL(is_cluster_on->getLb(), 0); + BOOST_CHECK_EQUAL(is_cluster_on->getUb(), 1); + auto* n_started_units = pb->getVariable("thermalCluster1.n_started_units"); + BOOST_CHECK(n_started_units); + BOOST_CHECK(n_started_units->isInteger()); + BOOST_CHECK_EQUAL(n_started_units->getLb(), 0); + BOOST_CHECK_EQUAL(n_started_units->getUb(), 17); + auto* p_per_unit = pb->getVariable("thermalCluster1.p_per_unit"); + BOOST_CHECK(p_per_unit); + BOOST_CHECK(!p_per_unit->isInteger()); + BOOST_CHECK_EQUAL(p_per_unit->getLb(), 100.248); + BOOST_CHECK_EQUAL(p_per_unit->getUb(), 950.6784); +} + +BOOST_AUTO_TEST_CASE(one_model_two_components__dont_clash) +{ + createModelWithOneFloatVar("m1", {"ub"}, "var1", literal(-100), parameter("ub"), {}); + createComponent("m1", "component_1", {{"ub", 15}}); + createComponent("m1", "component_2", {{"ub", 48}}); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->variableCount(), 2); + BOOST_CHECK_EQUAL(pb->constraintCount(), 0); + auto* c1_var1 = pb->getVariable("component_1.var1"); + BOOST_CHECK(c1_var1); + BOOST_CHECK(!c1_var1->isInteger()); + BOOST_CHECK_EQUAL(c1_var1->getLb(), -100); + BOOST_CHECK_EQUAL(c1_var1->getUb(), 15); + auto* c2_var1 = pb->getVariable("component_2.var1"); + BOOST_CHECK(c2_var1); + BOOST_CHECK(!c2_var1->isInteger()); + BOOST_CHECK_EQUAL(c2_var1->getLb(), -100); + BOOST_CHECK_EQUAL(c2_var1->getUb(), 48); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(_ComponentFiller_addConstraints_, LinearProblemBuildingFixture) + +BOOST_AUTO_TEST_CASE(ct_one_var__pb_contains_the_ct) +{ + // var1 <= 3 + auto var_node = variable("var1"); + auto three = literal(3); + auto ct_node = nodes.create(var_node, three); + createModel("model", + {}, + {{"var1", ValueType::BOOL, literal(-5), literal(10)}}, + {{"ct1", ct_node}}); + createComponent("model", "componentToto"); + buildLinearProblem(); + + auto var = pb->getVariable("componentToto.var1"); + BOOST_CHECK(var); + BOOST_CHECK(var->isInteger()); + BOOST_CHECK_EQUAL(pb->variableCount(), 1); + BOOST_CHECK_EQUAL(pb->constraintCount(), 1); + auto ct = pb->getConstraint("componentToto.ct1"); + BOOST_CHECK(ct); + BOOST_CHECK_EQUAL(ct->getLb(), -pb->infinity()); + BOOST_CHECK_EQUAL(ct->getUb(), 3); + BOOST_CHECK_EQUAL(ct->getCoefficient(var), 1); +} + +BOOST_AUTO_TEST_CASE(ct_one_var_with_coef__pb_contains_the_ct) +{ + // 3 * var1 >= 5 * var1 + 5 + // simplified to : -2 * var1 >= 5 + auto var_node = variable("var__1"); + auto five = literal(5); + auto coef_node_left = multiply(literal(3), var_node); + auto coef_node_right = multiply(var_node, five); + auto sum_node_right = nodes.create(coef_node_right, five); + auto ct_node = nodes.create(coef_node_left, sum_node_right); + + createModelWithOneFloatVar("model", + {}, + "var__1", + literal(-5), + literal(10), + {{"ct_1", ct_node}}); + createComponent("model", "componentTata"); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->variableCount(), 1); + BOOST_CHECK_NO_THROW(pb->getVariable("componentTata.var__1")); + auto var = pb->getVariable("componentTata.var__1"); + BOOST_CHECK_EQUAL(pb->constraintCount(), 1); + BOOST_CHECK_NO_THROW(pb->getConstraint("componentTata.ct_1")); + auto ct = pb->getConstraint("componentTata.ct_1"); + BOOST_CHECK(ct); + BOOST_CHECK_EQUAL(ct->getLb(), 5); + BOOST_CHECK_EQUAL(ct->getUb(), pb->infinity()); + BOOST_CHECK_EQUAL(ct->getCoefficient(var), -2); +} + +BOOST_AUTO_TEST_CASE(ct_with_two_vars) +{ + // param1(-16) * v1 + 8 * v2 + 5 - param2(8) = 7 * v1 + param3(5) * v2 + 89 + 5 * param4(-3) + // simplifies to: -23 * v1 + 3 * v2 = 77 + vector params = {"param1", "param2", "param3", "param4"}; + VariableData var1Data = {"v1", ValueType::FLOAT, literal(-50.), literal(300.)}; + VariableData var2Data = {"v2", ValueType::FLOAT, literal(60.), literal(75.)}; + + auto sum_node_left = nodes.create(multiply(variable("v1"), parameter("param1")), + multiply(literal(8), variable("v2")), + literal(5), + negate(parameter("param2"))); + auto sum_node_right = nodes.create(multiply(variable("v1"), literal(7)), + multiply(parameter("param3"), variable("v2")), + literal(89), + multiply(literal(5), parameter("param4"))); + auto ct_node = nodes.create(sum_node_left, sum_node_right); + + createModel("my_new_model", params, {var1Data, var2Data}, {{"constraint1", ct_node}}); + createComponent("my_new_model", + "my_component", + {{"param1", -16}, {"param2", 8}, {"param3", 5}, {"param4", -3}}); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->variableCount(), 2); + BOOST_CHECK_EQUAL(pb->constraintCount(), 1); + BOOST_CHECK_NO_THROW(pb->getConstraint("my_component.constraint1")); + auto ct = pb->getConstraint("my_component.constraint1"); + BOOST_CHECK(ct); + BOOST_CHECK_EQUAL(ct->getLb(), 77); + BOOST_CHECK_EQUAL(ct->getUb(), 77); + BOOST_CHECK_EQUAL(ct->getCoefficient(pb->getVariable("my_component.v1")), -23); + BOOST_CHECK_EQUAL(ct->getCoefficient(pb->getVariable("my_component.v2")), 3); +} + +BOOST_AUTO_TEST_CASE(two_constraints__they_are_created) +{ + // 3 * v1 -2 <= v2 (simplifies to : 3 * v1 - 2 * v2 <= 2) + // v2 <= v1 / 2 (simplifies to : -0.5 * v1 + v2 <= 0) + VariableData var1Data = {"v1", ValueType::FLOAT, literal(-50.), literal(300.)}; + VariableData var2Data = {"v2", ValueType::FLOAT, literal(60.), literal(75.)}; + + auto ct1_node = nodes.create( + nodes.create(multiply(literal(3), variable("v1")), literal(2)), + variable("v2")); + auto ct2_node = nodes.create(variable("v2"), + nodes.create(variable("v1"), + literal(2))); + + createModel("my_new_model", {}, {var1Data, var2Data}, {{"ct1", ct1_node}, {"ct2", ct2_node}}); + createComponent("my_new_model", "my_component", {}); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->variableCount(), 2); + BOOST_CHECK_EQUAL(pb->constraintCount(), 2); + + BOOST_CHECK_NO_THROW(pb->getConstraint("my_component.ct1")); + auto ct1 = pb->getConstraint("my_component.ct1"); + BOOST_CHECK(ct1); + BOOST_CHECK_EQUAL(ct1->getLb(), -numeric_limits::infinity()); + BOOST_CHECK_EQUAL(ct1->getUb(), 2); + BOOST_CHECK_EQUAL(ct1->getCoefficient(pb->getVariable("my_component.v1")), 3); + BOOST_CHECK_EQUAL(ct1->getCoefficient(pb->getVariable("my_component.v2")), -1); + + BOOST_CHECK_NO_THROW(pb->getConstraint("my_component.ct2")); + auto ct2 = pb->getConstraint("my_component.ct2"); + BOOST_CHECK(ct2); + BOOST_CHECK_EQUAL(ct2->getLb(), -numeric_limits::infinity()); + BOOST_CHECK_EQUAL(ct2->getUb(), 0); + BOOST_CHECK_EQUAL(ct2->getCoefficient(pb->getVariable("my_component.v1")), -0.5); + BOOST_CHECK_EQUAL(ct2->getCoefficient(pb->getVariable("my_component.v2")), 1); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(_ComponentFiller_addObjective_, LinearProblemBuildingFixture) + +BOOST_AUTO_TEST_CASE(one_var_with_objective) +{ + auto objective = variable("x"); + createModelWithOneFloatVar("model", {}, "x", literal(-50), literal(-40), {}, objective); + createComponent("model", "componentA", {}); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->variableCount(), 1); + BOOST_CHECK_NO_THROW(pb->getVariable("componentA.x")); + BOOST_CHECK_EQUAL(pb->getObjectiveCoefficient(pb->getVariable("componentA.x")), 1); +} + +BOOST_AUTO_TEST_CASE(two_vars_but_only_one_in_objective) +{ + VariableData var1Data = {"v1", ValueType::FLOAT, literal(-50.), literal(300.)}; + VariableData var2Data = {"v2", ValueType::FLOAT, literal(60.), literal(75.)}; + auto objective = multiply(variable("v2"), literal(37)); + + createModel("model", {}, {var1Data, var2Data}, {}, objective); + createComponent("model", "componentA", {}); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->variableCount(), 2); + BOOST_CHECK_NO_THROW(pb->getVariable("componentA.v1")); + BOOST_CHECK_NO_THROW(pb->getVariable("componentA.v2")); + BOOST_CHECK_EQUAL(pb->getObjectiveCoefficient(pb->getVariable("componentA.v1")), 0); + BOOST_CHECK_EQUAL(pb->getObjectiveCoefficient(pb->getVariable("componentA.v2")), 37); +} + +BOOST_AUTO_TEST_CASE(one_var_with_param_objective) +{ + // -param(5)*param(5) * x + auto objective = multiply(negate(multiply(parameter("param"), parameter("param"))), + variable("x")); + createModelWithOneFloatVar("model", {"param"}, "x", literal(-50), literal(-40), {}, objective); + createComponent("model", "componentA", {{"param", 5}}); + buildLinearProblem(); + + BOOST_CHECK_EQUAL(pb->variableCount(), 1); + BOOST_CHECK_NO_THROW(pb->getVariable("componentA.x")); + BOOST_CHECK_EQUAL(pb->getObjectiveCoefficient(pb->getVariable("componentA.x")), -25); +} + +BOOST_AUTO_TEST_CASE(offset_in_objective__throws_exception) +{ + auto objective = literal(6); + createModelWithOneFloatVar("model", {}, "x", literal(-50), literal(-40), {}, objective); + createComponent("model", "componentA", {}); + BOOST_CHECK_EXCEPTION(buildLinearProblem(), + invalid_argument, + checkMessage("Antares does not support objective offsets (found in model " + "'model' of component 'componentA').")); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/tests/src/solver/optim-model-filler/test_linearExpression.cpp b/src/tests/src/solver/optim-model-filler/test_linearExpression.cpp new file mode 100644 index 0000000000..b8e7ed0441 --- /dev/null +++ b/src/tests/src/solver/optim-model-filler/test_linearExpression.cpp @@ -0,0 +1,150 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#define WIN32_LEAN_AND_MEAN + +#include + +#include + +#include + +using namespace Antares::Optimization; + +BOOST_AUTO_TEST_SUITE(_linear_expressions_) + +BOOST_AUTO_TEST_CASE(default_linear_expression) +{ + LinearExpression linearExpression; + + BOOST_CHECK_EQUAL(linearExpression.offset(), 0.); + BOOST_CHECK(linearExpression.coefPerVar().empty()); +} + +BOOST_AUTO_TEST_CASE(linear_expression_explicit_construction) +{ + LinearExpression linearExpression(4., {{"some key", -5.}}); + + BOOST_CHECK_EQUAL(linearExpression.offset(), 4.); + BOOST_CHECK_EQUAL(linearExpression.coefPerVar().size(), 1); + BOOST_CHECK_EQUAL(linearExpression.coefPerVar()["some key"], -5.); +} + +BOOST_AUTO_TEST_CASE(sum_two_linear_expressions) +{ + LinearExpression linearExpression1(4., {{"var1", -5.}, {"var2", 6.}}); + LinearExpression linearExpression2(-1., {{"var3", 20.}, {"var2", -4.}}); + + auto sum = linearExpression1 + linearExpression2; + + BOOST_CHECK_EQUAL(sum.offset(), 3.); + BOOST_CHECK_EQUAL(sum.coefPerVar().size(), 3); + BOOST_CHECK_EQUAL(sum.coefPerVar()["var1"], -5.); + BOOST_CHECK_EQUAL(sum.coefPerVar()["var2"], 2.); + BOOST_CHECK_EQUAL(sum.coefPerVar()["var3"], 20.); +} + +BOOST_AUTO_TEST_CASE(subtract_two_linear_expressions) +{ + LinearExpression linearExpression1(4., {{"var1", -5.}, {"var2", 6.}}); + LinearExpression linearExpression2(-1., {{"var2", -4.}, {"var3", 20.}}); + + auto subtract = linearExpression1 - linearExpression2; + + BOOST_CHECK_EQUAL(subtract.offset(), 5.); + BOOST_CHECK_EQUAL(subtract.coefPerVar().size(), 3); + BOOST_CHECK_EQUAL(subtract.coefPerVar()["var1"], -5.); + BOOST_CHECK_EQUAL(subtract.coefPerVar()["var2"], 10.); + BOOST_CHECK_EQUAL(subtract.coefPerVar()["var3"], -20.); +} + +BOOST_AUTO_TEST_CASE(multiply_linear_expression_by_scalar) +{ + LinearExpression linearExpression(4., {{"var1", -5.}, {"var2", 6.}}); + LinearExpression someScalar(-2., {}); + + auto product = linearExpression * someScalar; + + BOOST_CHECK_EQUAL(product.offset(), -8.); + BOOST_CHECK_EQUAL(product.coefPerVar().size(), 2); + BOOST_CHECK_EQUAL(product.coefPerVar()["var1"], 10.); + BOOST_CHECK_EQUAL(product.coefPerVar()["var2"], -12.); +} + +BOOST_AUTO_TEST_CASE(multiply_scalar_by_linear_expression) +{ + LinearExpression linearExpression(4., {{"var1", -5.}, {"var2", 6.}}); + LinearExpression someScalar(-2., {}); + + auto product = someScalar * linearExpression; + + BOOST_CHECK_EQUAL(product.offset(), -8.); + BOOST_CHECK_EQUAL(product.coefPerVar().size(), 2); + BOOST_CHECK_EQUAL(product.coefPerVar()["var1"], 10.); + BOOST_CHECK_EQUAL(product.coefPerVar()["var2"], -12.); +} + +BOOST_AUTO_TEST_CASE(multiply_two_linear_expressions_containing_variables__exception_raised) +{ + LinearExpression linearExpression1(4., {{"var1", -5.}, {"var2", 6.}}); + LinearExpression linearExpression2(-1., {{"var2", -4.}, {"var3", 20.}}); + + BOOST_CHECK_EXCEPTION(linearExpression1 * linearExpression2, + std::invalid_argument, + checkMessage("A linear expression can't have quadratic terms.")); +} + +BOOST_AUTO_TEST_CASE(divide_linear_expression_by_scalar) +{ + LinearExpression linearExpression(4., {{"var1", -5.}, {"var2", 6.}}); + LinearExpression someScalar(-2., {}); + + auto product = linearExpression / someScalar; + + BOOST_CHECK_EQUAL(product.offset(), -2.); + BOOST_CHECK_EQUAL(product.coefPerVar().size(), 2); + BOOST_CHECK_EQUAL(product.coefPerVar()["var1"], 2.5); + BOOST_CHECK_EQUAL(product.coefPerVar()["var2"], -3.); +} + +BOOST_AUTO_TEST_CASE(divide_scalar_by_linear_expression__exception_raised) +{ + LinearExpression linearExpression(4., {{"var1", -5.}, {"var2", 6.}}); + LinearExpression someScalar(-2., {}); + + BOOST_CHECK_EXCEPTION(someScalar / linearExpression, + std::invalid_argument, + checkMessage("A linear expression can't have a variable as a dividend.")); +} + +BOOST_AUTO_TEST_CASE(negate_linear_expression) +{ + LinearExpression linearExpression(4., {{"var1", -5.}, {"var2", 6.}}); + + auto negative = linearExpression.negate(); + + BOOST_CHECK_EQUAL(negative.offset(), -4.); + BOOST_CHECK_EQUAL(negative.coefPerVar().size(), 2); + BOOST_CHECK_EQUAL(negative.coefPerVar()["var1"], 5.); + BOOST_CHECK_EQUAL(negative.coefPerVar()["var2"], -6.); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/tests/src/solver/optim-model-filler/test_main.cpp b/src/tests/src/solver/optim-model-filler/test_main.cpp new file mode 100644 index 0000000000..6f0c4178e7 --- /dev/null +++ b/src/tests/src/solver/optim-model-filler/test_main.cpp @@ -0,0 +1,26 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#define BOOST_TEST_MODULE test modeler impl + +#define WIN32_LEAN_AND_MEAN + +#include diff --git a/src/tests/src/solver/optim-model-filler/test_readLinearConstraintVisitor.cpp b/src/tests/src/solver/optim-model-filler/test_readLinearConstraintVisitor.cpp new file mode 100644 index 0000000000..da7b16eb86 --- /dev/null +++ b/src/tests/src/solver/optim-model-filler/test_readLinearConstraintVisitor.cpp @@ -0,0 +1,129 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#define WIN32_LEAN_AND_MEAN + +#include + +#include + +#include +#include +#include + +using namespace Antares::Solver; +using namespace Antares::Solver::Nodes; +using namespace Antares::Solver::Visitors; + +using namespace Antares::Optimization; + +BOOST_AUTO_TEST_SUITE(_read_linear_constraint_visitor_) + +BOOST_AUTO_TEST_CASE(test_name) +{ + ReadLinearConstraintVisitor visitor; + BOOST_CHECK_EQUAL(visitor.name(), "ReadLinearConstraintVisitor"); +} + +BOOST_FIXTURE_TEST_CASE(test_visit_equal_node, Registry) +{ + // 5 + var1 = var2 + 3 * var1 - param1(9) ==> -2 * var1 - var2 = -14 + Node* lhs = create(create(5.), create("var1")); + Node* rhs = create(create("var2"), + create(create(3.), + create("var1")), + create(create("param1"))); + Node* node = create(lhs, rhs); + EvaluationContext context({{"param1", 9.}}, {}); + ReadLinearConstraintVisitor visitor(context); + auto constraint = visitor.dispatch(node); + BOOST_CHECK_EQUAL(constraint.lb, -14.); + BOOST_CHECK_EQUAL(constraint.ub, -14.); + BOOST_CHECK_EQUAL(constraint.coef_per_var.size(), 2); + BOOST_CHECK_EQUAL(constraint.coef_per_var["var1"], -2); + BOOST_CHECK_EQUAL(constraint.coef_per_var["var2"], -1); +} + +BOOST_FIXTURE_TEST_CASE(test_visit_less_than_or_equal_node, Registry) +{ + // -9 + var3 <= var1 + 5 * var2 - param1(10) ==> - var1 - 5 * var2 + var3 <= -1 + Node* lhs = create(create(-9.), create("var3")); + Node* rhs = create(create("var1"), + create(create(5.), + create("var2")), + create(create("param1"))); + Node* node = create(lhs, rhs); + EvaluationContext context({{"param1", 10.}}, {}); + ReadLinearConstraintVisitor visitor(context); + auto constraint = visitor.dispatch(node); + BOOST_CHECK_EQUAL(constraint.lb, -std::numeric_limits::infinity()); + BOOST_CHECK_EQUAL(constraint.ub, -1.); + BOOST_CHECK_EQUAL(constraint.coef_per_var.size(), 3); + BOOST_CHECK_EQUAL(constraint.coef_per_var["var1"], -1); + BOOST_CHECK_EQUAL(constraint.coef_per_var["var2"], -5); + BOOST_CHECK_EQUAL(constraint.coef_per_var["var3"], 1); +} + +BOOST_FIXTURE_TEST_CASE(test_visit_greater_than_or_equal_node, Registry) +{ + // 5 + var1 >= var2 + 3 * var1 - param1(9) ==> -2 * var1 - var2 >= -14 + Node* lhs = create(create(5.), create("var1")); + Node* rhs = create(create("var2"), + create(create(3.), + create("var1")), + create(create("param1"))); + Node* node = create(lhs, rhs); + EvaluationContext context({{"param1", 9.}}, {}); + ReadLinearConstraintVisitor visitor(context); + auto constraint = visitor.dispatch(node); + BOOST_CHECK_EQUAL(constraint.lb, -14); + BOOST_CHECK_EQUAL(constraint.ub, std::numeric_limits::infinity()); + BOOST_CHECK_EQUAL(constraint.coef_per_var.size(), 2); + BOOST_CHECK_EQUAL(constraint.coef_per_var["var1"], -2); + BOOST_CHECK_EQUAL(constraint.coef_per_var["var2"], -1); +} + +BOOST_FIXTURE_TEST_CASE(test_visit_illegal_node, Registry) +{ + auto lit = create(5.); + std::vector illegal_nodes = {create(), + create(lit, lit), + create(lit, lit), + create(lit, lit), + create(lit), + create("var"), + create("param"), + create(5.), + create("port", "field"), + create("port", "field"), + create("x", "y"), + create("x", "y")}; + + for (Node* node: illegal_nodes) + { + ReadLinearConstraintVisitor visitor; + BOOST_CHECK_EXCEPTION(visitor.dispatch(node), + std::invalid_argument, + checkMessage("Root node of a constraint must be a comparator.")); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/tests/src/solver/optim-model-filler/test_readLinearExpressionVisitor.cpp b/src/tests/src/solver/optim-model-filler/test_readLinearExpressionVisitor.cpp new file mode 100644 index 0000000000..e9ce35c934 --- /dev/null +++ b/src/tests/src/solver/optim-model-filler/test_readLinearExpressionVisitor.cpp @@ -0,0 +1,179 @@ +/* + * Copyright 2007-2024, RTE (https://www.rte-france.com) + * See AUTHORS.txt + * SPDX-License-Identifier: MPL-2.0 + * This file is part of Antares-Simulator, + * Adequacy and Performance assessment for interconnected energy networks. + * + * Antares_Simulator is free software: you can redistribute it and/or modify + * it under the terms of the Mozilla Public Licence 2.0 as published by + * the Mozilla Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Antares_Simulator is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Mozilla Public Licence 2.0 for more details. + * + * You should have received a copy of the Mozilla Public Licence 2.0 + * along with Antares_Simulator. If not, see . + */ + +#define WIN32_LEAN_AND_MEAN + +#include + +#include + +#include +#include +#include + +using namespace Antares::Solver; +using namespace Antares::Solver::Nodes; +using namespace Antares::Solver::Visitors; + +using namespace Antares::Optimization; + +BOOST_AUTO_TEST_SUITE(_read_linear_expression_visitor_) + +BOOST_AUTO_TEST_CASE(name) +{ + ReadLinearExpressionVisitor visitor; + BOOST_CHECK_EQUAL(visitor.name(), "ReadLinearExpressionVisitor"); +} + +BOOST_FIXTURE_TEST_CASE(visit_literal, Registry) +{ + Node* node = create(5.); + ReadLinearExpressionVisitor visitor; + auto linear_expression = visitor.dispatch(node); + BOOST_CHECK_EQUAL(linear_expression.offset(), 5.); + BOOST_CHECK(linear_expression.coefPerVar().empty()); +} + +BOOST_FIXTURE_TEST_CASE(visit_literal_plus_param, Registry) +{ + // 5 + param(3) = 8 + Node* sum = create(create(5.), create("param")); + EvaluationContext evaluation_context({{"param", 3.}}, {}); + ReadLinearExpressionVisitor visitor(evaluation_context); + auto linear_expression = visitor.dispatch(sum); + BOOST_CHECK_EQUAL(linear_expression.offset(), 8.); + BOOST_CHECK(linear_expression.coefPerVar().empty()); +} + +BOOST_FIXTURE_TEST_CASE(visit_literal_plus_param_plus_var, Registry) +{ + // 60 + param(-5) + 7 * var = { 55, {var : 7} } + Node* product = create(create(7.), + create("var")); + Node* sum = create(create(60.), create("param"), product); + EvaluationContext evaluation_context({{"param", -5.}}, {}); + ReadLinearExpressionVisitor visitor(evaluation_context); + auto linear_expression = visitor.dispatch(sum); + BOOST_CHECK_EQUAL(linear_expression.offset(), 55.); + BOOST_CHECK_EQUAL(linear_expression.coefPerVar().size(), 1); + BOOST_CHECK_EQUAL(linear_expression.coefPerVar()["var"], 7.); +} + +BOOST_FIXTURE_TEST_CASE(visit_negate_literal_plus_var, Registry) +{ + // -(60 + 7 * var) = { -60, {var : -7} } + Node* product = create(create(7.), + create("var")); + Node* sum = create(create(60.), product); + Node* neg = create(sum); + ReadLinearExpressionVisitor visitor; + auto linear_expression = visitor.dispatch(neg); + BOOST_CHECK_EQUAL(linear_expression.offset(), -60.); + BOOST_CHECK_EQUAL(linear_expression.coefPerVar().size(), 1); + BOOST_CHECK_EQUAL(linear_expression.coefPerVar()["var"], -7.); +} + +BOOST_FIXTURE_TEST_CASE(visit_literal_minus_var, Registry) +{ + // 60 - 7 * var = { 60, {var : -7} } + Node* product = create(create(7.), + create("var")); + Node* sub = create(create(60.), product); + ReadLinearExpressionVisitor visitor; + auto linear_expression = visitor.dispatch(sub); + BOOST_CHECK_EQUAL(linear_expression.offset(), 60.); + BOOST_CHECK_EQUAL(linear_expression.coefPerVar().size(), 1); + BOOST_CHECK_EQUAL(linear_expression.coefPerVar()["var"], -7.); +} + +BOOST_FIXTURE_TEST_CASE(visit_complex_expression, Registry) +{ + // 2 * (13 + 3 * param1(-2) + 14 * var1) / 7 + param2(8) + 6 * var2 = {10 ; {var1:4, var2:6}} + + // small_sum = 13 + 3 * param1(-2) + 14 * var1 + Node* small_sum = create(create(13.), + create(create(3), + create("param1")), + create(create(14), + create("var1"))); + + // big_sum = 2 * small_sum / 7 + param2(8) + 6 * var2 + Node* big_sum = create( + create(create(create(2.), small_sum), + create(7.)), // 2 * small_sum / 7 + create("param2"), // param2 + create(create(6.), create("var2")) // 6 * var2 + ); + + EvaluationContext evaluation_context({{"param1", -2.}, {"param2", 8.}}, {}); + ReadLinearExpressionVisitor visitor(evaluation_context); + auto linear_expression = visitor.dispatch(big_sum); + BOOST_CHECK_EQUAL(linear_expression.offset(), 10.); + BOOST_CHECK_EQUAL(linear_expression.coefPerVar().size(), 2); + BOOST_CHECK_EQUAL(linear_expression.coefPerVar()["var1"], 4.); + BOOST_CHECK_EQUAL(linear_expression.coefPerVar()["var2"], 6.); +} + +BOOST_FIXTURE_TEST_CASE(comparison_nodes__exception_thrown, Registry) +{ + Node* literal = create(5.); + ReadLinearExpressionVisitor visitor; + auto predicate = checkMessage("A linear expression can't contain comparison operators."); + + Node* node = create(literal, literal); + BOOST_CHECK_EXCEPTION(visitor.dispatch(node), std::invalid_argument, predicate); + + node = create(literal, literal); + BOOST_CHECK_EXCEPTION(visitor.dispatch(node), std::invalid_argument, predicate); + + node = create(literal, literal); + BOOST_CHECK_EXCEPTION(visitor.dispatch(node), std::invalid_argument, predicate); +} + +BOOST_FIXTURE_TEST_CASE(not_implemented_nodes__exception_thrown, Registry) +{ + ReadLinearExpressionVisitor visitor; + + Node* node = create("port", "field"); + BOOST_CHECK_EXCEPTION(visitor.dispatch(node), + std::invalid_argument, + checkMessage("ReadLinearExpressionVisitor cannot visit PortFieldNodes")); + + node = create("port", "field"); + BOOST_CHECK_EXCEPTION(visitor.dispatch(node), + std::invalid_argument, + checkMessage( + "ReadLinearExpressionVisitor cannot visit PortFieldSumNodes")); + + node = create("id", "y"); + BOOST_CHECK_EXCEPTION(visitor.dispatch(node), + std::invalid_argument, + checkMessage( + "ReadLinearExpressionVisitor cannot visit ComponentVariableNodes")); + + node = create("id", "y"); + BOOST_CHECK_EXCEPTION(visitor.dispatch(node), + std::invalid_argument, + checkMessage( + "ReadLinearExpressionVisitor cannot visit ComponentParameterNodes")); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/tests/src/study/system-model/CMakeLists.txt b/src/tests/src/study/system-model/CMakeLists.txt index 0f5f956ac8..727635be3e 100644 --- a/src/tests/src/study/system-model/CMakeLists.txt +++ b/src/tests/src/study/system-model/CMakeLists.txt @@ -12,7 +12,7 @@ target_link_libraries(${EXECUTABLE_NAME} PRIVATE Boost::unit_test_framework antares-study-system-model - test_utils_unit + test_utils_unit ) set_target_properties(${EXECUTABLE_NAME} PROPERTIES FOLDER Unit-tests) diff --git a/src/tests/src/study/system-model/test_component.cpp b/src/tests/src/study/system-model/test_component.cpp index e8c69007a9..3f52b33880 100644 --- a/src/tests/src/study/system-model/test_component.cpp +++ b/src/tests/src/study/system-model/test_component.cpp @@ -21,12 +21,12 @@ #define WIN32_LEAN_AND_MEAN +#include + #include #include "antares/study/system-model/component.h" -#include "../../utils/unit_test_utils.h" - using namespace Antares::Study::SystemModel; struct ComponentBuilderCreationFixture @@ -98,6 +98,33 @@ BOOST_AUTO_TEST_CASE(nominal_build_without_parameters2) BOOST_CHECK_EQUAL(component.getScenarioGroupId(), "scenario_group3"); } +BOOST_AUTO_TEST_CASE(reuse_builder) +{ + Model model1 = createModelWithoutParameters(); + auto component1 = component_builder.withId("component1") + .withModel(&model1) + .withScenarioGroupId("scenario_group1") + .build(); + Model model2 = createModelWithParameters(); + auto component2 = component_builder.withId("component2") + .withModel(&model2) + .withParameterValues({{"param1", 5}, {"param2", 3}}) + .withScenarioGroupId("scenario_group2") + .build(); + + BOOST_CHECK_EQUAL(component1.Id(), "component1"); + BOOST_CHECK_EQUAL(component1.getModel(), &model1); + BOOST_CHECK_EQUAL(component1.getScenarioGroupId(), "scenario_group1"); + BOOST_CHECK(component1.getParameterValues().empty()); + + BOOST_CHECK_EQUAL(component2.Id(), "component2"); + BOOST_CHECK_EQUAL(component2.getModel(), &model2); + BOOST_CHECK_EQUAL(component2.getScenarioGroupId(), "scenario_group2"); + BOOST_CHECK_EQUAL(component2.getParameterValues().size(), 2); + BOOST_CHECK_EQUAL(component2.getParameterValues().at("param1"), 5); + BOOST_CHECK_EQUAL(component2.getParameterValues().at("param2"), 3); +} + BOOST_AUTO_TEST_CASE(fail_on_no_id) { Model model = createModelWithoutParameters(); diff --git a/src/tests/src/study/system-model/test_system.cpp b/src/tests/src/study/system-model/test_system.cpp index 9500b6118b..550ca59693 100644 --- a/src/tests/src/study/system-model/test_system.cpp +++ b/src/tests/src/study/system-model/test_system.cpp @@ -21,12 +21,12 @@ #define WIN32_LEAN_AND_MEAN +#include + #include #include "antares/study/system-model/system.h" -#include "../../utils/unit_test_utils.h" - using namespace Antares::Study::SystemModel; struct SystemBuilderCreationFixture