BlackoilWellModelNetworkPressureComputation.hpp
Go to the documentation of this file.
1/*
2 Copyright 2020-2026 Equinor ASA
3
4 This file is part of the Open Porous Media project (OPM).
5
6 OPM is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 OPM is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with OPM. If not, see <http://www.gnu.org/licenses/>.
18*/
19
20#ifndef OPM_BLACKOIL_WELL_MODEL_NETWORK_PRESSURE_COMPUTATION_HPP
21#define OPM_BLACKOIL_WELL_MODEL_NETWORK_PRESSURE_COMPUTATION_HPP
22
23#include <opm/common/TimingMacros.hpp>
24
25#include <opm/input/eclipse/Schedule/Group/GSatProd.hpp>
26#include <opm/input/eclipse/Schedule/Network/ExtNetwork.hpp>
27#include <opm/input/eclipse/Schedule/Schedule.hpp>
28#include <opm/input/eclipse/Schedule/ScheduleState.hpp>
29#include <opm/input/eclipse/Schedule/VFPProdTable.hpp>
30#include <opm/input/eclipse/Units/Units.hpp>
31
32#include <opm/output/data/Groups.hpp>
33
38
39#include <fmt/format.h>
40
41#include <algorithm>
42#include <cassert>
43#include <cmath>
44#include <map>
45#include <ranges>
46#include <set>
47#include <stack>
48#include <string>
49#include <vector>
50
51namespace Opm {
52
54template<typename Scalar>
56{
57 Scalar pressure{0.0};
58 // False when the table has no solution at this point (zero-filled cells give
59 // bhp <= 1 atm); the pressure must then not be used as a node pressure.
60 bool valid{true};
61 // True when the flow rate or upstream pressure had to be clamped to the table axes.
62 bool clamped{false};
63};
64
65namespace detail {
68 template<typename Scalar, typename IndexTraits, typename Table>
69 bool clampToTableAxes(const Table& table, std::vector<Scalar>& rates, Scalar& up_press)
70 {
71 bool clamped = false;
72 const auto& thp_axis = table.getTHPAxis();
73 const Scalar thp_lo = thp_axis.front();
74 const Scalar thp_hi = thp_axis.back();
75 if (up_press < thp_lo || up_press > thp_hi) {
76 up_press = std::clamp(up_press, thp_lo, thp_hi);
77 clamped = true;
78 }
79 const auto& flo_axis = table.getFloAxis();
80 const Scalar flo = std::abs(getFlo(table,
81 rates[IndexTraits::waterPhaseIdx],
82 rates[IndexTraits::oilPhaseIdx],
83 rates[IndexTraits::gasPhaseIdx]));
84 const Scalar flo_hi = flo_axis.back();
85 if (flo > flo_hi && flo > 0.0) {
86 const Scalar s = flo_hi / flo;
87 std::ranges::transform(rates, rates.begin(), [s](const auto r) { return s * r; });
88 clamped = true;
89 } else if (flo < flo_axis.front()) {
90 const Scalar flo_lo = flo_axis.front();
91 if (flo > 0.0) {
92 const Scalar s = flo_lo / flo;
93 std::ranges::transform(rates, rates.begin(), [s](const auto r) { return s * r; });
94 } else {
95 // A zero vector cannot be scaled. Choose the phase represented
96 // by the VFPINJ flow axis for this lookup only.
97 switch (table.getFloType()) {
98 case VFPInjTable::FLO_TYPE::FLO_OIL:
99 rates[IndexTraits::oilPhaseIdx] = flo_lo;
100 break;
101 case VFPInjTable::FLO_TYPE::FLO_WAT:
102 rates[IndexTraits::waterPhaseIdx] = flo_lo;
103 break;
104 case VFPInjTable::FLO_TYPE::FLO_GAS:
105 rates[IndexTraits::gasPhaseIdx] = flo_lo;
106 break;
107 }
108 }
109 clamped = true;
110 }
111 return clamped;
112 }
113} // namespace detail
114
117template<typename Scalar, typename IndexTraits, typename VfpProperties>
119
120// Production specialization.
121template<typename Scalar, typename IndexTraits>
122struct NetworkVfpPressureCalculator<Scalar, IndexTraits, VFPProdProperties<Scalar>>
123{
124 static void prepareRates(std::vector<Scalar>& rates)
125 {
126 // Network rates are positive, while production VFP expects negative rates.
127 std::ranges::transform(rates, rates.begin(), [](const auto r) { return -r; });
128 }
129
130 template <class GroupState>
131 static bool hasLeafNodeRate(const GroupState& group_state,
132 const std::string& node)
133 {
134 return group_state.has_network_leaf_node_production_rates(node);
135 }
136
137 template <class GroupState>
138 static const std::vector<Scalar>
139 leafNodeRate(const GroupState& group_state,
140 const std::string& node)
141 {
142 return group_state.network_leaf_node_production_rates(node);
143 }
144
145 template<typename Branch>
147 const int table_id,
148 std::vector<Scalar> rates,
149 Scalar up_press,
150 const Branch& upbranch,
151 const UnitSystem& unit_system)
152 {
153 // NB! ALQ in extended network is never implicitly the gas lift rate (GRAT), i.e., the
154 // gas lift rates only enters the network pressure calculations through the rates
155 // (e.g., in GOR calculations) unless a branch ALQ is set in BRANPROP.
156 const auto& table = vfp_props.getTable(table_id);
157 const auto alq_type = table.getALQType();
158 const auto dimension = VFPProdTable::ALQDimension(alq_type, unit_system);
159 const Scalar alq = upbranch.alq_value(dimension).value_or(0.0);
160
162 // Preserve the established production-network behaviour. Production VFP
163 // tables have historically been extrapolated outside their axes, and
164 // existing production cases rely on that when the result remains valid.
165 result.clamped = false;
166 result.pressure = vfp_props.bhp(table_id,
167 rates[IndexTraits::waterPhaseIdx],
168 rates[IndexTraits::oilPhaseIdx],
169 rates[IndexTraits::gasPhaseIdx],
170 up_press,
171 alq,
172 0.0, // explicit_wfr
173 0.0, // explicit_gfr
174 false); // use_expvfp we dont support explicit lookup
175 result.valid = result.pressure > unit::atm;
176 return result;
177 }
178};
179
180// Injection specialization.
181template<typename Scalar, typename IndexTraits>
182struct NetworkVfpPressureCalculator<Scalar, IndexTraits, VFPInjProperties<Scalar>>
183{
184 static void prepareRates(std::vector<Scalar>&)
185 {
186 }
187
188 template <class GroupState>
189 static bool hasLeafNodeRate(const GroupState& group_state,
190 const std::string& node)
191 {
192 return group_state.has_network_leaf_node_injection_rates(node);
193 }
194
195 template <class GroupState>
196 static const std::vector<Scalar>
197 leafNodeRate(const GroupState& group_state,
198 const std::string& node)
199 {
200 return group_state.network_leaf_node_injection_rates(node);
201 }
202
203 template<typename Branch>
205 const int table_id,
206 std::vector<Scalar> rates,
207 Scalar up_press,
208 const Branch&,
209 const UnitSystem&)
210 {
212 result.clamped = detail::clampToTableAxes<Scalar, IndexTraits>(vfp_props.getTable(table_id), rates, up_press);
213 result.pressure = vfp_props.bhp(table_id,
214 rates[IndexTraits::waterPhaseIdx],
215 rates[IndexTraits::oilPhaseIdx],
216 rates[IndexTraits::gasPhaseIdx],
217 up_press);
218 result.valid = result.pressure > unit::atm;
219 return result;
220 }
221};
222
226template<typename GenericWellModel, typename VfpProperties, typename Communication = Parallel::Communication>
228{
229public:
230 NetworkPressureComputation(const GenericWellModel& well_model,
231 const Network::ExtNetwork& network,
232 const VfpProperties& vfp_props,
233 const UnitSystem& unit_system,
234 const int report_step_idx,
235 const Communication& comm)
236 : well_model_(well_model)
237 , network_(network)
238 , vfp_props_(vfp_props)
239 , unit_system_(unit_system)
240 , report_step_idx_(report_step_idx)
241 , comm_(comm)
242 {
243 }
244
245 using Scalar = typename GenericWellModel::Scalar;
246 using IndexTraits = GenericWellModel::IndexTraits;
247 std::pair<std::map<std::string, Scalar>, std::map<std::string, data::BranchData>> run()
248 {
249 const auto roots = network_.roots();
250 for (const auto& root : roots) {
251 // Fixed pressure nodes of the network are the roots of trees.
252 // Leaf nodes must correspond to groups in the group structure.
253 // Let us first find all leaf nodes of the network. We also
254 // create a vector of all nodes, ordered so that a child is
255 // always after its parent.
256 const auto [root_to_child_nodes, leaf_nodes] = collectTreeNodes(root.get().name());
257
258 // Starting with the leaf nodes of the network, get the flow rates
259 // from the corresponding groups.
260 auto node_inflows = initializeLeafInflows(leaf_nodes);
261
262 // Accumulate flow rates in the network, towards the roots.
263 // Note that a root (i.e. fixed pressure node) can still be
264 // contributing flow towards other nodes in the network, i.e.
265 // a node can be the root of a subtree.
266 accumulateInflows(root_to_child_nodes, node_inflows);
267
268 // Going the other way (from roots to leafs), calculate the pressure
269 // at each node using VFP tables and rates.
270 computeNodePressures(root_to_child_nodes, node_inflows);
271
272#ifdef OPM_NETWORK_PRESSURE_TRACE
273 // Off unless the macro is defined: this builds a string per node per
274 // sub-iteration per domain, whether or not the log keeps it.
275 OpmLog::debug("Network pressure computation completed for root " + root.get().name() + ". Node pressures:");
276 for (const auto& [node, pressure] : node_pressures_) {
277 OpmLog::debug("Network node " + node + " pressure: " + std::to_string(pressure/1e5) + " bar");
278 }
279 OpmLog::debug("Node inflows:");
280 for (const auto& [node, inflows] : node_inflows) {
281 OpmLog::debug("Network node " + node + " inflows: "
282 + std::to_string(inflows[0]*86400) + ", " + std::to_string(inflows[1]*86400) + ", " + std::to_string(inflows[2]*86400));
283 }
284#endif
285
286 }
287
288 return {node_pressures_, branch_data_};
289 }
290
294 const std::set<std::string>& invalidNodes() const
295 {
296 return invalid_nodes_;
297 }
298
299private:
300 std::pair<std::vector<std::string>, std::set<std::string>>
301 collectTreeNodes(const std::string& root) const
302 {
303 std::stack<std::string> children;
304 std::set<std::string> leaf_nodes;
305 std::vector<std::string> root_to_child_nodes;
306 children.push(root);
307 while (!children.empty()) {
308 const auto node = children.top();
309 children.pop();
310 root_to_child_nodes.push_back(node);
311 auto branches = network_.downtree_branches(node);
312 if (branches.empty()) {
313 leaf_nodes.insert(node);
314 }
315 for (const auto& branch : branches) {
316 children.push(branch.downtree_node());
317 }
318 }
319
320 assert(children.empty());
321 return {root_to_child_nodes, leaf_nodes};
322 }
323
324 std::map<std::string, std::vector<Scalar>>
325 initializeLeafInflows(const std::set<std::string>& leaf_nodes) const
326 {
327 std::map<std::string, std::vector<Scalar>> node_inflows;
328 const std::vector<Scalar> zero_rates(3, 0.0);
329
330 for (const auto& node : leaf_nodes) {
331 // Guard against empty leaf nodes (may not be present in GRUPTREE).
332 // Use the domain-correct check so injection networks query the injection
333 // rate map rather than the production rate map (which is always empty for
334 // pure injection groups, causing zero-rate pressure calculations).
335 using Calc = NetworkVfpPressureCalculator<Scalar, IndexTraits, VfpProperties>;
336 if (!Calc::hasLeafNodeRate(well_model_.groupStateHelper().groupState(), node)) {
337 node_inflows[node] = zero_rates;
338 continue;
339 }
340
341 node_inflows[node] = Calc::leafNodeRate(well_model_.groupStateHelper().groupState(),
342 node);
343 if (network_.node(node).add_gas_lift_gas()) {
344 addGasLiftGas(node, node_inflows[node]);
345 }
346 }
347
348 return node_inflows;
349 }
350
351 void addGasLiftGas(const std::string& node,
352 std::vector<Scalar>& rates) const
353 {
354 const auto& group = well_model_.schedule().getGroup(node, report_step_idx_);
355 const auto& well_state = well_model_.groupStateHelper().wellState();
356 Scalar alq = 0.0;
357 // Add gas lift from all wells on this process
358 for (const std::string& wellname : group.wells()) {
359 const Well& well = well_model_.schedule().getWell(wellname, report_step_idx_);
360 if (well.isInjector() || !well_state.isOpen(wellname)) {
361 continue;
362 }
363
364 const Scalar efficiency = well.getEfficiencyFactor(/*network*/ true)
365 * well_state.getGlobalEfficiencyScalingFactor(wellname);
366 const auto& well_index = well_state.index(wellname);
367 if (well_index.has_value() &&
368 well_state.wellIsOwned(well_index.value(), wellname))
369 {
370 alq += well_state.well(wellname).alq_state.get() * efficiency;
371 }
372 }
373 // Sum ALQ across all processes to get total ALQ for the node.
374 // Note that communication is required here since each
375 // process has different wells, and the loop above therefore
376 // only considers local wells.
377 // However, all processes have all groups and their rates available,
378 // so we do not need to communicate those.
379 alq = comm_.sum(alq);
380 // Only add satellite production once for parallel runs
381 // (i.e. add after communication)
382 if (group.hasSatelliteProduction()) {
383 const auto gsrate = this->well_model_
384 .schedule()[this->report_step_idx_]
385 .satelliteProduction(node)
386 .getRate(GSatProd::Rate::GLift, this->well_model_.summaryState());
387
388 alq += static_cast<Scalar>(gsrate);
389 }
390
391 rates[IndexTraits::gasPhaseIdx] += alq;
392 }
393
394 void accumulateInflows(const std::vector<std::string>& root_to_child_nodes,
395 std::map<std::string, std::vector<Scalar>>& node_inflows) const
396 {
397 const auto child_to_root_nodes = std::ranges::reverse_view(root_to_child_nodes);
398
399 for (const auto& node : child_to_root_nodes) {
400 const auto upbranch = network_.uptree_branch(node);
401 if (!upbranch) {
402 continue;
403 }
404 std::vector<Scalar>& up = node_inflows[(*upbranch).uptree_node()];
405 const std::vector<Scalar>& down = node_inflows[node];
406 // NEFAC support
407 const Scalar efficiency = network_.node(node).efficiency();
408 if (up.empty()) {
409 up = std::vector<Scalar>(down.size(), 0.0);
410 }
411 assert(up.size() == down.size());
412 for (std::size_t ii = 0; ii < up.size(); ++ii) {
413 up[ii] += efficiency * down[ii];
414 }
415 }
416 }
417
418 void computeNodePressures(const std::vector<std::string>& root_to_child_nodes,
419 const std::map<std::string, std::vector<Scalar>>& node_inflows)
420 {
421 for (const auto& node : root_to_child_nodes) {
422 // Do not traverse subtree more than once
423 if (node_pressures_.find(node) != node_pressures_.end()) {
424 continue;
425 }
426
427 const auto terminal_pressure = network_.node(node).terminal_pressure();
428 const auto upbranch = network_.uptree_branch(node);
429 assert(upbranch || terminal_pressure); // If not root, must have uptree branch, and if root, must have terminal pressure.
430 using Calc = NetworkVfpPressureCalculator<Scalar, IndexTraits, VfpProperties>;
431
432 if (terminal_pressure) {
433 node_pressures_[node] = *terminal_pressure;
434 if (upbranch) {
435 // If terminal pressure is specified on a non-root node, we still want to calculate the branch data for the uptree branch.
436 const Scalar up_press = node_pressures_[(*upbranch).uptree_node()];
437 auto rates = node_inflows.at(node);
438 branch_data_.try_emplace(node,
439 *terminal_pressure - up_press,
440 rates[IndexTraits::oilPhaseIdx],
441 rates[IndexTraits::waterPhaseIdx],
442 rates[IndexTraits::gasPhaseIdx]);
443 } else {
444 // Root node with terminal pressure and no uptree branch, inserting a zero-valued placeholder.
445 branch_data_.emplace(node, data::BranchData{0.0, 0.0, 0.0, 0.0});
446 }
447 continue;
448 }
449
450 const std::string& up_node = (*upbranch).uptree_node();
451 const Scalar up_press = node_pressures_[up_node];
452 // Descendants of a node without a valid pressure have none either.
453 if (invalid_nodes_.count(up_node) > 0) {
454 invalid_nodes_.insert(node);
455 }
456 const auto vfp_table = (*upbranch).vfp_table();
457 if (!vfp_table) {
458 // Table number specified as 9999 in the deck, no pressure loss.
459 if (network_.node(node).as_choke()) {
460 // Node pressure is set to the group THP.
461 node_pressures_[node] = well_model_.groupStateHelper().groupState().well_group_thp(node);
462 } else {
463 node_pressures_[node] = up_press;
464 }
465 auto rates = node_inflows.at(node);
466 branch_data_.try_emplace(node,
467 node_pressures_[node] - up_press,
468 rates[IndexTraits::oilPhaseIdx],
469 rates[IndexTraits::waterPhaseIdx],
470 rates[IndexTraits::gasPhaseIdx]);
471 continue;
472 }
473
474 OPM_TIMEBLOCK(NetworkVfpCalculations);
475 auto rates = node_inflows.at(node);
476 assert(rates.size() == 3);
477 Calc::prepareRates(rates);
478 const auto branch = Calc::compute(vfp_props_, *vfp_table, rates, up_press, *upbranch, unit_system_);
479 // An invalid lookup (zero-filled table cells) gets the upstream pressure as a
480 // placeholder so downstream lookups stay in range; callers must consult invalidNodes().
481 const Scalar node_pressure = branch.valid ? branch.pressure : up_press;
482 if (!branch.valid) {
483 invalid_nodes_.insert(node);
484 }
485 if (!branch.valid || branch.clamped) {
486 OpmLog::debug(fmt::format("Network branch {} -> {}: VFP table {} {} at rates ({:.4g}, {:.4g}, {:.4g}) sm3/d, "
487 "upstream pressure {:.2f} bar",
488 up_node, node, *vfp_table,
489 branch.valid ? "lookup clamped to the table axes" : "has no solution",
490 rates[IndexTraits::waterPhaseIdx] * unit::day,
491 rates[IndexTraits::oilPhaseIdx] * unit::day,
492 rates[IndexTraits::gasPhaseIdx] * unit::day,
493 up_press / unit::barsa));
494 }
495 node_pressures_[node] = node_pressure;
496 // Prefer inserting after computing the pressure, hence negating rates
497 branch_data_.try_emplace(node,
498 node_pressure - up_press,
499 -rates[IndexTraits::oilPhaseIdx],
500 -rates[IndexTraits::waterPhaseIdx],
501 -rates[IndexTraits::gasPhaseIdx]);
502 }
503 }
504
505 const GenericWellModel& well_model_;
506 const Network::ExtNetwork& network_;
507 const VfpProperties& vfp_props_;
508 const UnitSystem& unit_system_;
509 const int report_step_idx_;
510 const Communication& comm_;
511 std::map<std::string, Scalar> node_pressures_;
512 std::map<std::string, data::BranchData> branch_data_;
513 std::set<std::string> invalid_nodes_;
514};
515
516} // namespace Opm
517
518#endif // OPM_BLACKOIL_WELL_MODEL_NETWORK_PRESSURE_COMPUTATION_HPP
Definition: GroupState.hpp:42
bool has_network_leaf_node_production_rates(const std::string &gname) const
const std::vector< Scalar > & network_leaf_node_injection_rates(const std::string &gname) const
bool has_network_leaf_node_injection_rates(const std::string &gname) const
const std::vector< Scalar > & network_leaf_node_production_rates(const std::string &gname) const
Class to compute network pressures using VFP tables, given flow rates for each group and fixed pressu...
Definition: BlackoilWellModelNetworkPressureComputation.hpp:228
const std::set< std::string > & invalidNodes() const
Definition: BlackoilWellModelNetworkPressureComputation.hpp:294
typename GenericWellModel::Scalar Scalar
Definition: BlackoilWellModelNetworkPressureComputation.hpp:245
std::pair< std::map< std::string, Scalar >, std::map< std::string, data::BranchData > > run()
Definition: BlackoilWellModelNetworkPressureComputation.hpp:247
GenericWellModel::IndexTraits IndexTraits
Definition: BlackoilWellModelNetworkPressureComputation.hpp:246
NetworkPressureComputation(const GenericWellModel &well_model, const Network::ExtNetwork &network, const VfpProperties &vfp_props, const UnitSystem &unit_system, const int report_step_idx, const Communication &comm)
Definition: BlackoilWellModelNetworkPressureComputation.hpp:230
Definition: VFPInjProperties.hpp:34
const VFPInjTable & getTable(const int table_id) const
EvalWell bhp(const int table_id, const EvalWell &aqua, const EvalWell &liquid, const EvalWell &vapour, const Scalar thp) const
Definition: VFPProdProperties.hpp:38
EvalWell bhp(const int table_id, const EvalWell &aqua, const EvalWell &liquid, const EvalWell &vapour, const Scalar thp, const Scalar alq, const Scalar explicit_wfr, const Scalar explicit_gfr, const bool use_expvfp) const
const VFPProdTable & getTable(const int table_id) const
Dune::Communication< MPIComm > Communication
Definition: ParallelCommunication.hpp:30
bool clampToTableAxes(const Table &table, std::vector< Scalar > &rates, Scalar &up_press)
Definition: BlackoilWellModelNetworkPressureComputation.hpp:69
T getFlo(const VFPProdTable &table, const T &aqua, const T &liquid, const T &vapour)
Definition: blackoilbioeffectsmodules.hh:45
std::string to_string(const ConvergenceReport::ReservoirFailure::Type t)
Result of a single network branch VFP lookup.
Definition: BlackoilWellModelNetworkPressureComputation.hpp:56
bool valid
Definition: BlackoilWellModelNetworkPressureComputation.hpp:60
Scalar pressure
Definition: BlackoilWellModelNetworkPressureComputation.hpp:57
bool clamped
Definition: BlackoilWellModelNetworkPressureComputation.hpp:62
static void prepareRates(std::vector< Scalar > &)
Definition: BlackoilWellModelNetworkPressureComputation.hpp:184
static bool hasLeafNodeRate(const GroupState &group_state, const std::string &node)
Definition: BlackoilWellModelNetworkPressureComputation.hpp:189
static NetworkBranchPressure< Scalar > compute(const VFPInjProperties< Scalar > &vfp_props, const int table_id, std::vector< Scalar > rates, Scalar up_press, const Branch &, const UnitSystem &)
Definition: BlackoilWellModelNetworkPressureComputation.hpp:204
static const std::vector< Scalar > leafNodeRate(const GroupState &group_state, const std::string &node)
Definition: BlackoilWellModelNetworkPressureComputation.hpp:197
static const std::vector< Scalar > leafNodeRate(const GroupState &group_state, const std::string &node)
Definition: BlackoilWellModelNetworkPressureComputation.hpp:139
static void prepareRates(std::vector< Scalar > &rates)
Definition: BlackoilWellModelNetworkPressureComputation.hpp:124
static NetworkBranchPressure< Scalar > compute(const VFPProdProperties< Scalar > &vfp_props, const int table_id, std::vector< Scalar > rates, Scalar up_press, const Branch &upbranch, const UnitSystem &unit_system)
Definition: BlackoilWellModelNetworkPressureComputation.hpp:146
static bool hasLeafNodeRate(const GroupState &group_state, const std::string &node)
Definition: BlackoilWellModelNetworkPressureComputation.hpp:131
Helper class to insulate the NetworkPressureComputation class from the differences between production...
Definition: BlackoilWellModelNetworkPressureComputation.hpp:118