Skip to content

Injection networks: structure, and VFP lookups that stay inside the tables - #7368

Open
hnil wants to merge 12 commits into
OPM:masterfrom
hnil:injection-network-pressures
Open

Injection networks: structure, and VFP lookups that stay inside the tables#7368
hnil wants to merge 12 commits into
OPM:masterfrom
hnil:injection-network-pressures

Conversation

@hnil

@hnil hnil commented Aug 28, 2026

Copy link
Copy Markdown
Member

First slice out of #7348, as agreed there: atgeirr's nine base commits, plus the one on top of them that stops the network reading its VFP tables outside where they are defined.

That last commit introduces NetworkBranchPressure (pressure, plus whether the lookup was clamped and whether it has a solution at all) and the invalid-node concept: a node whose lookup lands outside the table gets no pressure rather than a zero, and its descendants inherit that instead of being poisoned by it. Six cases in tests/test_networkpressure.cpp cover it, each written to fail without the fix.

Nothing here changes production networks: NETWORK-01 and NETWORK-01_STANDARD are identical to master across all 369 summary vectors except TCPU, and compareParallelSim_flow+NETWORK-01 passes. Note that the seven compareECLFiles_flow+NETWORK-01* tests fail in my checkout on master as well, because my opm-tests predates GWGR/FWGR in the summary — nothing to do with this branch.

Needs OPM/opm-common#5314 (GNETINJE implies a standard network type). The bracketing node-pressure update, the summary output and the simultaneous solve follow in later slices; #7348 keeps the whole picture in the meantime.

Atgeirr Flø Rasmussen and others added 10 commits August 24, 2026 10:52
A network branch lookup clamped the rate and upstream pressure to nothing:
VFPHelpers::findInterpData extrapolates linearly past the axis ends, and the
flow-line tables are zero-filled where the line cannot deliver the rate, so an
injection network with rates beyond the table axis produced node pressures of
-260 bar, and a THP below the axis start gave 0. Both were then handed to the
wells as THP limits.

The branch calculator now clamps the lookup point to the table axes (rates
scaled uniformly so prod WFR/GFR are kept) and treats a result <= 1 atm as
'no solution'. Such nodes, and their descendants, are reported by
NetworkPressureComputation::invalidNodes(); updatePressures() keeps their
previous pressure, counts the network as unbalanced (max update bound), warns
once per report step, and does not push a dynamic THP to wells under them.

Also make updateActiveStateImpl accumulate instead of clearing active_ on an
inactive domain (unreachable today, but wrong).

Tests: gas_injection_rate_beyond_flow_axis, gas_injection_zero_cell_region and
gas_injection_thp_below_axis in test_networkpressure.cpp; on 0996672 they
give -213 bar, 0 bar and 28.9 bar respectively.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@atgeirr atgeirr added manual:enhancement This is an enhancement/improvent that needs to be documented in the manual manual:irrelevant This PR is a minor fix and should not appear in the manual and removed manual:enhancement This is an enhancement/improvent that needs to be documented in the manual labels Aug 28, 2026
@atgeirr

atgeirr commented Aug 28, 2026

Copy link
Copy Markdown
Member

While this does introduce a new feature, I think describing it for the manual is better done at once, at the end of the work. This is just the first of several PRs introducing the injection network feature, and the improved network solver. I therefore made the label here manual:irrelevant.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is the first slice extracted from #7348. It generalizes the well-network machinery so that injection networks (gas and water, driven by GNETINJE) are handled alongside the existing production network, and it hardens VFP-table lookups so a network branch never extrapolates outside its defined table region. The central new concept is NetworkBranchPressure (pressure + valid/clamped flags) and the notion of invalid nodes: a node whose VFP lookup lands in a zero-filled cell or off the table axes gets no pressure (a placeholder is used) rather than a spurious zero/negative value, and its descendants inherit that invalidity. Network pressure state is now partitioned per domain (Production / InjectionGas / InjectionWater) to avoid collisions, with the legacy production maps kept in sync for output/restart. The PR states production results are unchanged (validated against NETWORK-01) and depends on OPM/opm-common#5314.

Changes:

  • Add axis-clamping of VFP lookups and an invalid-node mechanism in the network pressure computation, exposed via invalidNodes().
  • Introduce domain-scoped network pressure/branch state and iterate over all active networks (production + injection) in active-state, rebalance, and pressure-update paths, applying network THP to injectors only when it lies within the well's VFPINJ THP range.
  • Bypass the stale well-potential check for injection wells with a dynamic (network-derived) THP, refresh injector potentials after re-solving, and break the outer loop quietly when the imbalance is already within tolerance.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_networkpressure.cpp Adds six cases covering rate-beyond-axis, zero-cell region, and THP-below-axis clamping/invalid-node behavior; parameterizes mock leaf rates and terminal pressure.
opm/simulators/wells/BlackoilWellModelNetworkPressureComputation.hpp Adds NetworkBranchPressure, clampToTableAxes, invalid-node tracking, and injection-phase plumbing; compute now returns clamp/validity flags.
opm/simulators/wells/BlackoilWellModelNetworkGeneric.hpp/.cpp Domain-scoped pressure/branch/invalid-node state, active-networks helpers, injection computePressures overload, and injector THP application within VFPINJ range.
opm/simulators/wells/BlackoilWellModelNetwork_impl.hpp Iterates injectors as well as producers per domain and refreshes injector well potentials after re-solve.
opm/simulators/wells/BlackoilWellModel_impl.hpp Uses anyNetworkActive and breaks the network loop quietly once within tolerance.
opm/simulators/wells/WellConstraints.cpp Uses wellHasTHPConstraints and bypasses the stale rate-vs-potential check for dynamic-THP injectors.
opm/simulators/wells/GroupState.hpp/.cpp Adds has_network_leaf_node_{injection,production}_rates presence checks.
opm/simulators/wells/GroupStateHelper.cpp Populates injection-network leaf-node rates for gas and water.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +579 to +583
&& this->last_valid_branch_data_ == rhs.last_valid_branch_data_
&& this->domain_node_pressures_ == rhs.domain_node_pressures_
&& this->last_valid_domain_node_pressures_ == rhs.last_valid_domain_node_pressures_
&& this->domain_branch_data_ == rhs.domain_branch_data_
&& this->last_valid_domain_branch_data_ == rhs.last_valid_domain_branch_data_;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e0cb206.

Comment on lines +148 to +157
std::optional<details::NetworkDomain> domain;
if (well->isProducer()) {
domain = details::NetworkDomain::Production;
} else if (well->isInjector()) {
if (well->wellEcl().injectorType() == InjectorType::GAS) {
domain = details::NetworkDomain::InjectionGas;
} else if (well->wellEcl().injectorType() == InjectorType::WATER) {
domain = details::NetworkDomain::InjectionWater;
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e0cb206details::domainForWell(), used at all three sites.

Comment on lines +250 to +258
OpmLog::debug("Network pressure computation completed for root " + root.get().name() + ". Node pressures:");
for (const auto& [node, pressure] : node_pressures_) {
OpmLog::debug("Network node " + node + " pressure: " + std::to_string(pressure/1e5) + " bar");
}
OpmLog::debug("Node inflows:");
for (const auto& [node, inflows] : node_inflows) {
OpmLog::debug("Network node " + node + " inflows: "
+ std::to_string(inflows[0]*86400) + ", " + std::to_string(inflows[1]*86400) + ", " + std::to_string(inflows[2]*86400));
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e0cb206 — behind OPM_NETWORK_PRESSURE_TRACE, undefined by default, following atgeirr's suggestion. You were right that it ran regardless of whether the log kept the string.

@atgeirr

atgeirr commented Aug 28, 2026

Copy link
Copy Markdown
Member

All three comments from copilot review are relevant and should be handled. I suggest that the debugging output is put in an #ifdef block using a unique macro so it will only be activated if the developer explicitly defined that macro.

hnil added a commit to hnil/opm-simulators that referenced this pull request Aug 28, 2026
Three from the review on OPM#7368.

`details::domainForWell()` already existed but two callers still open-coded the
producer/gas/water mapping; they use the helper now, so a new injector type is
one edit rather than three.

The per-node pressure and inflow trace in NetworkPressureComputation::run()
built a string for every node on every sub-iteration of every domain whether or
not the log kept it. It is behind OPM_NETWORK_PRESSURE_TRACE now, undefined by
default, as atgeirr suggested -- these lines are for someone debugging the tree,
not for a production run to pay for.

And the && chain in operator== is indented like the lines above it.

Tests and both GNETINJE regressions unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The producer/gas-injector/water-injector mapping to a NetworkDomain was written
out at three call sites. It is now details::domainForWell(), so a new injector
type is one edit rather than three.

The per-node pressure and inflow trace in NetworkPressureComputation::run()
built a string for every node on every sub-iteration of every domain whether or
not the log kept it. Behind OPM_NETWORK_PRESSURE_TRACE now, undefined by
default, as suggested in the review -- these lines are for someone debugging the
tree, not something a production run should pay for.

And the && chain in operator== is indented like the lines above it.

test_networkpressure passes; NETWORK-01 and NETWORK-01_STANDARD still match
master, and the parallel comparison passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hnil

hnil commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

All three handled in e0cb206.

The domain mapping is now details::domainForWell(), used at all three sites. The node pressure and inflow trace is behind OPM_NETWORK_PRESSURE_TRACE, undefined by default, as you suggested — it built a string per node per sub-iteration per domain whether or not the log kept it. And the && chain is indented like the lines above it.

test_networkpressure passes, NETWORK-01 and NETWORK-01_STANDARD still agree with master on every summary vector but TCPU, and compareParallelSim_flow+NETWORK-01 passes.

The same three are on #7348 as 71efe14, so the two do not drift apart before this one lands.

@atgeirr atgeirr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the minor changes requested here, I think this part would be ready for final review and merge.

, unit_system_(unit_system)
, report_step_idx_(report_step_idx)
, comm_(comm)
, injection_phase_(injection_phase)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This member is unnecessary, and it should also be removed as argument from the leafNodeRate() functions. This was already done in #7348 which this should be a subset of, but apparently that did not get moved over properly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5d4d4ce — the member, the ctor argument and the parameter on hasLeafNodeRate()/leafNodeRate(). Both specialisations picked their rate map from the VFP property type and ignored the phase, so nothing read it.

Also removed what fed it: the injectionPhase argument of the injection computePressures() and details::injectionPhaseForDomain(), which had no other caller. #7348 had the same leftover — the value was still computed and asserted there before being dropped on the floor — so it is gone there too, in e60d49d.

std::reference_wrapper<const Network::ExtNetwork> network;
};

/// The network a well belongs to, or nullopt for a well that is in none of them.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is inaccurate, a well may not be in a network corresponding to its domain. To belong to a network, the well must be in a group that is a leaf node of the network.

Suggest rewording:

/// The network domain corresponding to a well's type
/// (producer, or injector and water/gas injection phase), or nullopt.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken verbatim in 5d4d4ce, and on #7348 too. You are right that the two are different things — the domain follows from the well type alone, membership needs the group to be a leaf node of the network.

Both leafNodeRate() specialisations picked their rate map from the VFP
property type, and ignored the phase argument; the phase it was derived
from was dead with it.

Removes the NetworkPressureComputation member and ctor argument, the
parameter on hasLeafNodeRate()/leafNodeRate(), the injectionPhase
argument of the injection computePressures(), and
details::injectionPhaseForDomain(), whose only caller this was.

Also corrects the domainForWell() comment: it maps a well's type to a
domain, which is not the same as membership of that network -- a well
belongs to a network only if its group is a leaf node of one.
@atgeirr
atgeirr marked this pull request as ready for review August 31, 2026 13:49
@atgeirr

atgeirr commented Aug 31, 2026

Copy link
Copy Markdown
Member

jenkins build this please

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual:irrelevant This PR is a minor fix and should not appear in the manual

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants