Injection networks: structure, and VFP lookups that stay inside the tables - #7368
Injection networks: structure, and VFP lookups that stay inside the tables#7368hnil wants to merge 12 commits into
Conversation
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>
|
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. |
There was a problem hiding this comment.
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.
| && 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_; |
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
Done in e0cb206 — details::domainForWell(), used at all three sites.
| 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)); | ||
| } |
There was a problem hiding this comment.
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.
|
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. |
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>
|
All three handled in e0cb206. The domain mapping is now
The same three are on #7348 as 71efe14, so the two do not drift apart before this one lands. |
atgeirr
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
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.
|
jenkins build this please |
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 intests/test_networkpressure.cppcover 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-01passes. Note that the sevencompareECLFiles_flow+NETWORK-01*tests fail in my checkout on master as well, because my opm-tests predatesGWGR/FWGRin 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.