diff --git a/CMakeLists_files.cmake b/CMakeLists_files.cmake
index 5b684b52fb9..a807d61a325 100644
--- a/CMakeLists_files.cmake
+++ b/CMakeLists_files.cmake
@@ -264,6 +264,7 @@ list(APPEND MAIN_SOURCE_FILES
opm/input/eclipse/Schedule/Network/Branch.cpp
opm/input/eclipse/Schedule/Network/ExtNetwork.cpp
opm/input/eclipse/Schedule/Network/NetworkKeywordHandlers.cpp
+ opm/input/eclipse/Schedule/Network/NetworkValidation.cpp
opm/input/eclipse/Schedule/Network/Node.cpp
opm/input/eclipse/Schedule/ResCoup/ReservoirCouplingInfo.cpp
opm/input/eclipse/Schedule/ResCoup/ReservoirCouplingKeywordHandlers.cpp
@@ -1644,6 +1645,7 @@ list(APPEND PRIVATE_HEADER_FILES
external/resinsight/cafPdmCore/cafAssert.h
external/resinsight/cafPdmCore/cafSignal.h
opm/input/eclipse/Schedule/HandlerContext.hpp
+ opm/input/eclipse/Schedule/Network/NetworkValidation.hpp
opm/input/eclipse/Schedule/Well/WellTrajInfo.hpp
opm/input/eclipse/Schedule/WellTraj/RigEclipseWellLogExtractor.hpp
)
diff --git a/opm/input/eclipse/Parser/ParseContext.cpp b/opm/input/eclipse/Parser/ParseContext.cpp
index 4672d92eb76..9310fd451a6 100644
--- a/opm/input/eclipse/Parser/ParseContext.cpp
+++ b/opm/input/eclipse/Parser/ParseContext.cpp
@@ -160,6 +160,8 @@ namespace Opm {
this->addKey(SCHEDULE_MSW_KEYWORD_ON_NON_MSW_WELL, InputErrorAction::THROW_EXCEPTION);
this->addKey(SCHEDULE_ICD_INCOMPATIBLE_PDROP_MODEL, InputErrorAction::THROW_EXCEPTION);
+ this->addKey(SCHEDULE_NETWORK_INVALID, InputErrorAction::DELAYED_EXIT1);
+
addKey(SCHEDULE_INVALID_NAME, InputErrorAction::THROW_EXCEPTION);
this->addKey(SCHEDULE_INVALID_INJPHASE, InputErrorAction::WARN);
this->addKey(SCHEDULE_GCONSALE_INVALID_INJECTION, InputErrorAction::THROW_EXCEPTION);
@@ -462,4 +464,6 @@ namespace Opm {
const std::string ParseContext::SCHEDULE_MSW_KEYWORD_ON_NON_MSW_WELL = "SCHEDULE_MSW_KEYWORD_ON_NON_MSW_WELL";
const std::string ParseContext::SCHEDULE_ICD_INCOMPATIBLE_PDROP_MODEL = "SCHEDULE_ICD_INCOMPATIBLE_PDROP_MODEL";
+ const std::string ParseContext::SCHEDULE_NETWORK_INVALID = "SCHEDULE_NETWORK_INVALID";
+
}
diff --git a/opm/input/eclipse/Parser/ParseContext.hpp b/opm/input/eclipse/Parser/ParseContext.hpp
index 25cdf5c771f..bda192bcbdb 100644
--- a/opm/input/eclipse/Parser/ParseContext.hpp
+++ b/opm/input/eclipse/Parser/ParseContext.hpp
@@ -610,6 +610,14 @@ namespace Opm {
/// with the pressure drop model chosen for a particular MSW.
const static std::string SCHEDULE_ICD_INCOMPATIBLE_PDROP_MODEL;
+ /// Inconsistent network topology (BRANPROP, NODEPROP keywords).
+ ///
+ /// For instance a node without inlets which is not also a group, or
+ /// a flow path which does not end in a fixed pressure node. Such a
+ /// network cannot be balanced, so this category reports an error and
+ /// schedules termination at the end of schedule loading by default.
+ const static std::string SCHEDULE_NETWORK_INVALID;
+
// The SIMULATOR_KEYWORD_ categories are intended to define the
// parser behaviour for when the parser itself recognises an input
// keyword, but the simulator does not support the intended use of
diff --git a/opm/input/eclipse/Schedule/Network/NetworkValidation.cpp b/opm/input/eclipse/Schedule/Network/NetworkValidation.cpp
new file mode 100644
index 00000000000..a70625287e9
--- /dev/null
+++ b/opm/input/eclipse/Schedule/Network/NetworkValidation.cpp
@@ -0,0 +1,242 @@
+/*
+ Copyright 2026 Equinor ASA.
+
+ This file is part of the Open Porous Media project (OPM).
+
+ OPM is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ OPM 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
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with OPM. If not, see .
+*/
+
+#include "NetworkValidation.hpp"
+
+#include
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+namespace {
+
+ /// Names of those nodes which take part in the network.
+ ///
+ /// A node is part of the network only for as long as it is attached to
+ /// at least one branch. A node mentioned in NODEPROP alone, or a node
+ /// whose only branch has been removed by a BRANPROP entry with a zero
+ /// VFP table number, is therefore not included here.
+ ///
+ /// \param[in] network Extended network.
+ ///
+ /// \return Names of the network's nodes, in order of first appearance
+ /// in the input.
+ std::vector
+ connectedNodes(const Opm::Network::ExtNetwork& network)
+ {
+ auto connected = std::set{};
+ for (const auto* branch : network.branches()) {
+ connected.insert(branch->uptree_node());
+ connected.insert(branch->downtree_node());
+ }
+
+ auto nodes = std::vector{};
+ nodes.reserve(connected.size());
+
+ std::ranges::copy_if(network.node_names(), std::back_inserter(nodes),
+ [&connected](const std::string& node)
+ { return connected.contains(node); });
+
+ return nodes;
+ }
+
+ /// Whether or not a node acts as a source for the network.
+ ///
+ /// Flow enters a node through its downtree branches, so a node without
+ /// downtree branches has no inlets and must supply its own flow rate.
+ ///
+ /// \param[in] network Extended network.
+ ///
+ /// \param[in] node Node name. Must be a node of \p network.
+ ///
+ /// \return Whether or not \p node has any inlets.
+ bool isSource(const Opm::Network::ExtNetwork& network,
+ const std::string& node)
+ {
+ return network.downtree_branches(node).empty();
+ }
+
+ /// Report a network inconsistency.
+ ///
+ /// \param[in] description Description of the inconsistency. Must not
+ /// contain any brace characters.
+ ///
+ /// \param[in] location Location of the keyword which prompted the
+ /// check. Included in the diagnostic by the error handling protocol.
+ ///
+ /// \param[in] parseContext Error handling controls.
+ ///
+ /// \param[in,out] errors Collection of parse errors.
+ void reportInconsistency(const std::string& description,
+ const Opm::KeywordLocation& location,
+ const Opm::ParseContext& parseContext,
+ Opm::ErrorGuard& errors)
+ {
+ parseContext.handleError(Opm::ParseContext::SCHEDULE_NETWORK_INVALID,
+ description, location, errors);
+ }
+
+ /// Check that every source node is also a group.
+ ///
+ /// A source node gets its flow rate from the wells of the group of the
+ /// same name. A source node which is not a group has no way of
+ /// contributing to the network.
+ ///
+ /// \param[in] network Extended network.
+ ///
+ /// \param[in] nodes Names of the network's nodes.
+ ///
+ /// \param[in] isGroup Predicate returning whether or not a name is that
+ /// of a group.
+ ///
+ /// \param[in] location Location of the keyword which prompted this
+ /// check.
+ ///
+ /// \param[in] parseContext Error handling controls.
+ ///
+ /// \param[in,out] errors Collection of parse errors.
+ void checkSourcesAreGroups(const Opm::Network::ExtNetwork& network,
+ const std::vector& nodes,
+ const std::function& isGroup,
+ const Opm::KeywordLocation& location,
+ const Opm::ParseContext& parseContext,
+ Opm::ErrorGuard& errors)
+ {
+ for (const auto& node : nodes) {
+ if (!isSource(network, node) || isGroup(node)) {
+ continue;
+ }
+
+ reportInconsistency(fmt::format("Network node {0} has no downtree branches, and must "
+ "therefore supply the network on its own, but no "
+ "group {0} exists.", node),
+ location, parseContext, errors);
+ }
+ }
+
+ /// Check that every flow path ends in a fixed pressure node.
+ ///
+ /// Follows the flow path uptree from each node. The pressure drop along
+ /// the path can be computed only if the path ends in a node of known
+ /// pressure.
+ ///
+ /// Flow paths merge on their way uptree, so a single problem is shared
+ /// by every node downtree of it. To report each problem only once we
+ /// stop tracing a path as soon as it reaches a node which some earlier
+ /// path already passed through.
+ ///
+ /// \param[in] network Extended network.
+ ///
+ /// \param[in] nodes Names of the network's nodes.
+ ///
+ /// \param[in] location Location of the keyword which prompted this
+ /// check.
+ ///
+ /// \param[in] parseContext Error handling controls.
+ ///
+ /// \param[in,out] errors Collection of parse errors.
+ void checkFlowPathsAreTerminated(const Opm::Network::ExtNetwork& network,
+ const std::vector& nodes,
+ const Opm::KeywordLocation& location,
+ const Opm::ParseContext& parseContext,
+ Opm::ErrorGuard& errors)
+ {
+ // Nodes whose flow path has been traced already, either to a fixed
+ // pressure node or to a problem which has been reported.
+ auto traced = std::unordered_set{};
+
+ for (const auto& start : nodes) {
+ if (traced.contains(start)) {
+ continue;
+ }
+
+ auto path = std::vector { start };
+ auto onPath = std::unordered_set { start };
+
+ auto node = start;
+ while (! network.node(node).terminal_pressure().has_value()) {
+ const auto uptree = network.uptree_branch(node);
+
+ if (! uptree.has_value()) {
+ reportInconsistency(fmt::format("Flow path from network node {} terminates in "
+ "node {}, which has neither an uptree branch "
+ "nor a fixed pressure.", start, node),
+ location, parseContext, errors);
+ break;
+ }
+
+ node = uptree->uptree_node();
+
+ if (traced.contains(node)) {
+ // Path merges into one which has been traced already.
+ // Whether that path ends well or not, there is nothing
+ // new to report here.
+ break;
+ }
+
+ if (! onPath.insert(node).second) {
+ // Cycle in the branch definitions. Stop here to avoid
+ // looping forever--this path will never reach a fixed
+ // pressure node.
+ reportInconsistency(fmt::format("Flow path from network node {} returns to "
+ "node {}: the branches form a loop.",
+ start, node),
+ location, parseContext, errors);
+ break;
+ }
+
+ path.push_back(node);
+ }
+
+ traced.insert(path.begin(), path.end());
+ }
+ }
+
+} // Anonymous namespace
+
+void Opm::Network::validateTopology(const ExtNetwork& network,
+ const std::function& isGroup,
+ const KeywordLocation& location,
+ const ParseContext& parseContext,
+ ErrorGuard& errors)
+{
+ if (!network.active() || network.is_standard_network()) {
+ // Nothing to check, or a standard network (GRUPNET) whose nodes are
+ // groups by construction.
+ return;
+ }
+
+ const auto nodes = connectedNodes(network);
+
+ checkSourcesAreGroups(network, nodes, isGroup, location, parseContext, errors);
+ checkFlowPathsAreTerminated(network, nodes, location, parseContext, errors);
+}
diff --git a/opm/input/eclipse/Schedule/Network/NetworkValidation.hpp b/opm/input/eclipse/Schedule/Network/NetworkValidation.hpp
new file mode 100644
index 00000000000..af607dd609f
--- /dev/null
+++ b/opm/input/eclipse/Schedule/Network/NetworkValidation.hpp
@@ -0,0 +1,72 @@
+/*
+ Copyright 2026 Equinor ASA.
+
+ This file is part of the Open Porous Media project (OPM).
+
+ OPM is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ OPM 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
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with OPM. If not, see .
+*/
+
+#ifndef NETWORK_VALIDATION_HPP
+#define NETWORK_VALIDATION_HPP
+
+#include
+#include
+
+namespace Opm {
+ class ErrorGuard;
+ class KeywordLocation;
+ class ParseContext;
+} // namespace Opm
+
+namespace Opm::Network {
+
+class ExtNetwork;
+
+/// Check that the topology of an extended network is internally consistent.
+///
+/// The network cannot be balanced unless every source--i.e., every node
+/// without inlets--supplies a flow rate, and unless every flow path ends in
+/// a node of known pressure. Consequently, this function reports, through
+/// the normal error handling protocol,
+///
+/// -# any node without inlets which is not also a group, and
+///
+/// -# any flow path which does not end in a fixed pressure node.
+///
+/// Nodes which are no longer attached to a branch--e.g., because a BRANPROP
+/// entry with a zero VFP table number removed the node's only branch--are
+/// not part of the network and are therefore not checked. Neither are
+/// standard networks (GRUPNET), the nodes of which are groups by
+/// construction.
+///
+/// \param[in] network Extended network, typically that of the current
+/// report step's schedule state.
+///
+/// \param[in] isGroup Predicate returning whether or not a name is that of
+/// a group at the current report step.
+///
+/// \param[in] location Location of the keyword which prompted this check.
+///
+/// \param[in] parseContext Error handling controls.
+///
+/// \param[in,out] errors Collection of parse errors.
+void validateTopology(const ExtNetwork& network,
+ const std::function& isGroup,
+ const KeywordLocation& location,
+ const ParseContext& parseContext,
+ ErrorGuard& errors);
+
+} // namespace Opm::Network
+
+#endif // NETWORK_VALIDATION_HPP
diff --git a/opm/input/eclipse/Schedule/Schedule.cpp b/opm/input/eclipse/Schedule/Schedule.cpp
index 3157ef0ef24..f9fb365d977 100644
--- a/opm/input/eclipse/Schedule/Schedule.cpp
+++ b/opm/input/eclipse/Schedule/Schedule.cpp
@@ -97,6 +97,7 @@
#include
#include
#include
+#include
#include
#include
@@ -104,6 +105,7 @@
#include "KeywordHandlers.hpp"
#include "MSW/Compsegs.hpp"
#include "MSW/WelSegsSet.hpp"
+#include "Network/NetworkValidation.hpp"
#include "Well/injection.hpp"
#include
@@ -722,6 +724,12 @@ void Schedule::iterateScheduleSection(std::size_t load_start, std::size_t load_e
std::unordered_map wpimult_global_factor;
+ // Location of the first network keyword of this report step, if
+ // any. The network is checked for consistency only for those
+ // report steps in which it is defined or redefined, and any
+ // problem is reported against that keyword.
+ std::optional network_location;
+
while (true) {
if (keyword_index == block.size())
break;
@@ -768,6 +776,13 @@ void Schedule::iterateScheduleSection(std::size_t load_start, std::size_t load_e
continue;
}
+ if (!network_location.has_value() &&
+ (keyword.is() ||
+ keyword.is()))
+ {
+ network_location = location;
+ }
+
logger(fmt::format("Processing keyword {} at line {}", location.keyword, location.lineno));
this->handleKeyword(report_step,
block,
@@ -788,6 +803,16 @@ void Schedule::iterateScheduleSection(std::size_t load_start, std::size_t load_e
this->updateICDScalingFactors();
check_compsegs_and_comptraj_consistency(welsegs_wells, compsegs_wells, comptraj_wells, this->getWells(report_step));
+
+ if (network_location.has_value()) {
+ const auto& state = this->snapshots[report_step];
+
+ Network::validateTopology(state.network.get(),
+ [&state](const std::string& name)
+ { return state.groups.has(name); },
+ *network_location, parseContext, errors);
+ }
+
this->applyGlobalWPIMULT(wpimult_global_factor);
this->end_report(report_step);
diff --git a/tests/parser/NetworkTests.cpp b/tests/parser/NetworkTests.cpp
index 82e1888e934..71ef30282e4 100644
--- a/tests/parser/NetworkTests.cpp
+++ b/tests/parser/NetworkTests.cpp
@@ -42,6 +42,9 @@
#include
+#include
+#include
+#include
#include
#include
@@ -72,6 +75,42 @@ Schedule make_schedule(const std::string& schedule_string)
runspec, std::make_shared()
};
}
+
+Schedule make_schedule(const std::string& schedule_string, ErrorGuard& errors)
+{
+ const auto deck = Parser{}.parseString(schedule_string);
+ EclipseGrid grid(10,10,10);
+ const TableManager table ( deck );
+ const FieldPropsManager fp( deck, Phases{true, true, true}, grid, table);
+ const Runspec runspec (deck);
+
+ return {
+ deck, grid, fp, NumericalAquifers{},
+ runspec, ParseContext{}, errors, std::make_shared()
+ };
+}
+
+// Build a schedule while treating an inconsistent network topology as a
+// warning rather than as a fatal input error. Enables inspecting the
+// network which the parser builds from an invalid description.
+Schedule make_schedule_lenient_network(const std::string& schedule_string)
+{
+ const auto deck = Parser{}.parseString(schedule_string);
+ EclipseGrid grid(10,10,10);
+ const TableManager table ( deck );
+ const FieldPropsManager fp( deck, Phases{true, true, true}, grid, table);
+ const Runspec runspec (deck);
+
+ ParseContext parseContext;
+ parseContext.update(ParseContext::SCHEDULE_NETWORK_INVALID, InputErrorAction::IGNORE);
+
+ ErrorGuard errors;
+
+ return {
+ deck, grid, fp, NumericalAquifers{},
+ runspec, parseContext, errors, std::make_shared()
+ };
+}
}
BOOST_AUTO_TEST_SUITE(Basic_Functionality)
@@ -137,7 +176,14 @@ NODEPROP
/
)";
- const auto& schedule = make_schedule(deck_string);
+ // B1X is a source node--it has no inlets--but it is not a group, so it
+ // cannot supply any flow to the network.
+ auto errors = ErrorGuard{};
+ make_schedule(deck_string, errors);
+ BOOST_CHECK(errors);
+ errors.clear();
+
+ const auto& schedule = make_schedule_lenient_network(deck_string);
const auto& network = schedule[0].network.get();
BOOST_CHECK(network.has_node("B1X"));
}
@@ -177,7 +223,15 @@ NODEPROP
/
)";
- const auto& schedule = make_schedule(deck_string);
+ // The flow paths from B1 and C1 both end at PLAT-AX. The fixed
+ // pressure is assigned to PLAT-A, which is not part of the network, so
+ // neither path ends in a node of known pressure.
+ auto errors = ErrorGuard{};
+ make_schedule(deck_string, errors);
+ BOOST_CHECK(errors);
+ errors.clear();
+
+ const auto& schedule = make_schedule_lenient_network(deck_string);
const auto& network = schedule[0].network.get();
BOOST_CHECK(network.has_node("PLAT-AX"));
}
@@ -598,6 +652,213 @@ BOOST_AUTO_TEST_SUITE_END() // Keyword_Consistency
// ===========================================================================
+BOOST_AUTO_TEST_SUITE(Topology_Consistency)
+
+namespace {
+ // Extended network deck with the group tree of model5. The network
+ // itself--the BRANPROP and NODEPROP keywords, and any subsequent
+ // updates--is supplied by the caller.
+ std::string network_deck(const std::string& network)
+ {
+ return R"(
+RUNSPEC
+NETWORK
+ 5 4 /
+
+SCHEDULE
+
+GRUPTREE
+ 'PROD' 'FIELD' /
+
+ 'M5S' 'PLAT-A' /
+ 'M5N' 'PLAT-A' /
+
+ 'C1' 'M5N' /
+ 'F1' 'M5N' /
+ 'B1' 'M5S' /
+ 'G1' 'M5S' /
+ 'BX' 'M5S' /
+/
+)" + network;
+ }
+
+ // The network of model5/5_NETWORK_MODEL5_STDW.DATA, which is consistent.
+ std::string valid_network()
+ {
+ return R"(
+BRANPROP
+-- Downtree Uptree #VFP ALQ
+ B1 PLAT-A 5 1* /
+ C1 PLAT-A 4 1* /
+/
+
+NODEPROP
+-- Node_name Press autoChoke? addGasLift? Group_name
+ PLAT-A 21.0 NO NO 1* /
+ B1 1* NO NO 1* /
+ C1 1* NO NO 1* /
+/
+)";
+ }
+} // Anonymous namespace
+
+BOOST_AUTO_TEST_CASE(Accept_Consistent_Network)
+{
+ BOOST_CHECK_NO_THROW(make_schedule(network_deck(valid_network())));
+}
+
+BOOST_AUTO_TEST_CASE(Reject_Source_Node_Without_Group)
+{
+ // Branch N1 -> N2 added to an otherwise consistent network. N1 has no
+ // inlets and is not a group, so it cannot act as a source.
+ const auto deck_string = network_deck(valid_network() + R"(
+BRANPROP
+-- Downtree Uptree #VFP ALQ
+ N1 N2 4 1* /
+/
+)");
+
+ auto errors = ErrorGuard{};
+ make_schedule(deck_string, errors);
+ BOOST_CHECK(errors);
+ errors.clear();
+}
+
+BOOST_AUTO_TEST_CASE(Reject_Flow_Path_Without_Fixed_Pressure)
+{
+ // Branch BX -> N2 added to an otherwise consistent network. BX is a
+ // group, so it may act as a source, but the flow path from BX ends at
+ // N2, which has no pressure of its own and no uptree branch.
+ const auto deck_string = network_deck(valid_network() + R"(
+BRANPROP
+-- Downtree Uptree #VFP ALQ
+ BX N2 4 1* /
+/
+)");
+
+ auto errors = ErrorGuard{};
+ make_schedule(deck_string, errors);
+ BOOST_CHECK(errors);
+ errors.clear();
+
+ // The offending nodes are still added to the network when the
+ // inconsistency is not treated as an error.
+ const auto schedule = make_schedule_lenient_network(deck_string);
+ const auto& network = schedule[0].network.get();
+ BOOST_CHECK(network.has_node("BX"));
+ BOOST_CHECK(network.has_node("N2"));
+}
+
+BOOST_AUTO_TEST_CASE(Reject_Cyclic_Network)
+{
+ // No node of the B1 -> N1 -> N2 -> B1 loop has any inlet outside the
+ // loop, and none of them has a fixed pressure.
+ const auto deck_string = network_deck(R"(
+BRANPROP
+-- Downtree Uptree #VFP ALQ
+ B1 N1 5 1* /
+ N1 N2 4 1* /
+ N2 B1 4 1* /
+/
+
+NODEPROP
+-- Node_name Press autoChoke? addGasLift? Group_name
+ B1 1* NO NO 1* /
+/
+)");
+
+ auto errors = ErrorGuard{};
+ make_schedule(deck_string, errors);
+ BOOST_CHECK(errors);
+ errors.clear();
+}
+
+BOOST_AUTO_TEST_CASE(Accept_Detached_Node)
+{
+ // Dropping the only branch of node C1 leaves C1 outside the network.
+ // The remaining network, B1 -> PLAT-A, is consistent.
+ const auto deck_string = network_deck(valid_network() + R"(
+TSTEP
+ 10 /
+
+BRANPROP
+-- Downtree Uptree #VFP ALQ
+ C1 PLAT-A 0 1* /
+/
+)");
+
+ BOOST_CHECK_NO_THROW(make_schedule(deck_string));
+}
+
+BOOST_AUTO_TEST_CASE(Reject_Inconsistent_Network_Update)
+{
+ // The network is consistent at the first report step and is made
+ // inconsistent at the second.
+ const auto deck_string = network_deck(valid_network() + R"(
+TSTEP
+ 10 /
+
+BRANPROP
+-- Downtree Uptree #VFP ALQ
+ N1 N2 4 1* /
+/
+)");
+
+ auto errors = ErrorGuard{};
+ make_schedule(deck_string, errors);
+ BOOST_CHECK(errors);
+ errors.clear();
+}
+
+BOOST_AUTO_TEST_CASE(Report_Multiple_Topology_Errors)
+{
+ // Two disconnected branches N1 -> N2 and N3 -> N4 added.
+ // Both N1 and N3 are source nodes without groups.
+ // All topology errors should be reported in a single pass.
+ const auto deck_string = network_deck(valid_network() + R"(
+BRANPROP
+-- Downtree Uptree #VFP ALQ
+ N1 N2 4 1* /
+ N3 N4 4 1* /
+/
+)");
+
+ auto errors = ErrorGuard{};
+ make_schedule(deck_string, errors);
+
+ const auto diagnostic = errors.formattedErrors();
+ errors.clear();
+
+ BOOST_CHECK(diagnostic.find("N1") != std::string::npos);
+ BOOST_CHECK(diagnostic.find("N3") != std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(Throw_On_Inconsistent_Network_When_Configured)
+{
+ const auto deck_string = network_deck(valid_network() + R"(
+BRANPROP
+-- Downtree Uptree #VFP ALQ
+ N1 N2 4 1* /
+/
+)");
+
+ const auto deck = Parser{}.parseString(deck_string);
+ EclipseGrid grid(10, 10, 10);
+ const TableManager table(deck);
+ const FieldPropsManager fp(deck, Phases{true, true, true}, grid, table);
+ const Runspec runspec(deck);
+
+ ParseContext parseContext;
+ parseContext.update(ParseContext::SCHEDULE_NETWORK_INVALID, InputErrorAction::THROW_EXCEPTION);
+ ErrorGuard errors;
+
+ BOOST_CHECK_THROW((Schedule{deck, grid, fp, NumericalAquifers{}, runspec, parseContext, errors, std::make_shared()}), OpmInputError);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Topology_Consistency
+
+// ===========================================================================
+
BOOST_AUTO_TEST_SUITE(Injection_Networks)
namespace {