BlackoilWellModelRescoup_impl.hpp
Go to the documentation of this file.
1/*
2 Copyright 2025 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_BLACKOILWELLMODEL_RESCOUP_IMPL_HEADER_INCLUDED
21#define OPM_BLACKOILWELLMODEL_RESCOUP_IMPL_HEADER_INCLUDED
22
23// Improve IDE experience
24#ifndef OPM_BLACKOILWELLMODEL_RESCOUP_HEADER_INCLUDED
25#include <config.h>
27#endif
28
29#ifdef RESERVOIR_COUPLING_ENABLED
30
31#include <opm/common/TimingMacros.hpp>
32
38
39#include <cassert>
40
41namespace Opm {
42
43// Constructor
44// -----------
45template<typename TypeTag>
46BlackoilWellModelRescoup<TypeTag>::
47BlackoilWellModelRescoup(BlackoilWellModel<TypeTag>& well_model)
48 : well_model_{well_model}
49 , network_{well_model.network()}
50 , simulator_{well_model.simulator()}
51 , param_{well_model.param()}
52{}
53
54// Public methods alphabetically
55// ------------------------------
56
57template<typename TypeTag>
58bool
59BlackoilWellModelRescoup<TypeTag>::
60masterIsInCoupledNetworkIteration() const
61{
62 return this->isReservoirCouplingMaster()
63 && this->reservoirCouplingMaster().isFirstSubstepOfSyncTimestep()
64 && this->masterNetworkHasMasterGroupLeaves()
65 && !this->lastSentMasterGroupNodePressuresIsFinal();
66}
67
68
69template<typename TypeTag>
70bool
71BlackoilWellModelRescoup<TypeTag>::
72masterNetworkHasMasterGroupLeaves() const
73{
74 // Query the parsed Schedule topology (`network.has_node(...)`) rather than the runtime
75 // `node_pressures_` map. The runtime map is empty on the very first beginTimeStep call
76 // (network_.update has not yet run for this substep).
77 if (!this->isReservoirCouplingMaster()) return false;
78 const auto& rcm = this->reservoirCouplingMaster();
79 const auto num_slaves = rcm.numSlaves();
80 for (std::size_t s = 0; s < num_slaves; ++s) {
81 if (!rcm.slaveIsActivated(s)) continue;
82 if (this->masterNetworkHasMasterGroupLeavesForSlave_(s)) {
83 return true;
84 }
85 }
86 return false;
87}
88
89template<typename TypeTag>
90void
91BlackoilWellModelRescoup<TypeTag>::
92maybeExchangeNetworkOuterIterationWithSlaves(bool more_network_update)
93{
94 if (!this->masterIsInCoupledNetworkIteration()) {
95 return;
96 }
97 const bool is_final = !more_network_update;
98 // In tight coupling the inner sub-loop (maybeExchangeNetworkSubIterationWithSlaves)
99 // performs every non-final exchange, so here we only emit the single
100 // terminating (is_final) message -- the non-final case would just resend the
101 // same node pressures the last sub-iteration already shipped, and get the same
102 // rates back. The retained final send still covers the case where the network
103 // is not balanced this iteration (the inner loop never runs). In loose coupling
104 // this per-outer send is the sole master<->slave coupling and runs every outer
105 // iteration.
106 if (is_final || !this->well_model_.useTightRcNetworkCoupling()) {
107 // send the pressures freshly computed by network_.update() to all activated slaves
108 this->sendMasterGroupNodePressuresToSlaves(is_final);
109 if (!is_final) {
110 // receive slaves' updated network_surface_rates for the next outer iteration.
111 this->receiveSlaveGroupData();
112 }
113 }
114}
115
116template<typename TypeTag>
117void
118BlackoilWellModelRescoup<TypeTag>::
119maybeExchangeNetworkSubIterationWithSlaves()
120{
121 // Called for rescoup master for the tight master-slave coupling (--rc-network-loose-coupling=false)
122 // mode at the sub-iteration level: Send node pressures and receive slave rates per master inner
123 // network sub-iteration.
124 // In the loose mode (--rc-network-loose-coupling=true) the exchange happens only once
125 // per master outer iteration (see updateWellControlsAndNetworkIteration), so
126 // the inner loop converges the node pressures against a frozen slave rate.
127 // This loose mode is faster but has been observed in some cases to overshoot to a value that
128 // incorrectly shuts the slave wells in.
129 // To avoid this, the tight coupling is therefore the default coupling.
130 //
131 // NOTE: This is always a non-final exchange. The inner sub-iteration loop cannot
132 // know whether the master's outer network loop will iterate again (the inner
133 // loop ending on max_sub_iter, or on a THP update, still leaves
134 // more_inner_network_update true), so it must not declare termination here.
135 // The single is_final = true send is owned by the per-outer send in
136 // updateWellControlsAndNetworkIteration() (when the outer loop converges)
137 // and by sendSlaveNetworkLoopTerminationSignal_() (when the outer loop hits
138 // max_iter).
139 if (!this->well_model_.useTightRcNetworkCoupling()) {
140 return;
141 }
142 if (!this->masterIsInCoupledNetworkIteration()) {
143 return;
144 }
145 this->sendMasterGroupNodePressuresToSlaves(/*is_final=*/false);
146 this->receiveSlaveGroupData();
147}
148
149template<typename TypeTag>
150void
151BlackoilWellModelRescoup<TypeTag>::
152receiveCoupledNetworkActiveStatus()
153{
154 OPM_TIMEFUNCTION();
155 assert(this->isReservoirCouplingSlave());
156 this->reservoirCouplingSlave().receiveCoupledNetworkActiveStatusFromMaster();
157}
158
159template<typename TypeTag>
160void
161BlackoilWellModelRescoup<TypeTag>::
162receiveGroupConstraintsFromMaster()
163{
164 OPM_TIMEFUNCTION();
165 RescoupReceiveGroupConstraints<Scalar, IndexTraits> constraint_receiver{
166 this->well_model_.guideRateHandler(),
167 this->groupStateHelper()
168 };
169 constraint_receiver.receiveGroupConstraintsFromMaster();
170}
171
172template<typename TypeTag>
173void
174BlackoilWellModelRescoup<TypeTag>::
175receiveMasterGroupNodePressuresFromMaster()
176{
177 OPM_TIMEFUNCTION();
178 assert(this->isReservoirCouplingSlave());
179 auto& rescoup_slave = this->reservoirCouplingSlave();
180 const auto [num_pressures, _is_final] =
181 rescoup_slave.receiveNumMasterGroupNodePressuresFromMaster();
182 if (num_pressures > 0) {
183 rescoup_slave.receiveMasterGroupNodePressuresFromMaster(num_pressures);
184 }
185 // Apply pressures as dynamic THP limits on every producer whose
186 // group has a master-supplied pressure. Wells in master groups that
187 // are not network leaves are not touched (no entry in the map).
188 // Mirrors the standard local-network apply pattern in
189 // BlackoilWellModelNetworkGeneric::updatePressures(): when the well is
190 // currently THP-controlled, also write the new THP into the WellState
191 // directly, because setDynamicThpLimit() alone leaves the active
192 // control's THP value stale and the subsequent well-solve would
193 // converge against the old THP.
194 //
195 // TODO (follow-up PR): this THP-direct path is only correct when the
196 // slave has no extended network between its slave group and the wells
197 // (the case exercised here). When the slave uses an extended network
198 // with the slave group declared as a fixed-pressure node, the master-
199 // supplied pressure should instead be installed as that node's terminal
200 // pressure and the slave's network solver should propagate it down to
201 // the wells. The plumbing exists -- the solver already reads
202 // Network::Node::terminal_pressure() when walking up branches -- but
203 // surfacing the master-sent value into the solver needs a runtime
204 // override path (e.g. a fixed-pressure override map on
205 // BlackoilWellModelNetwork) so we do not mutate the parsed Schedule
206 // network at runtime. Until then, decks where the slave group is a
207 // fixed-pressure node in the slave's extended network are not handled
208 // correctly.
209 const auto& pressures = rescoup_slave.masterGroupNodePressures();
210 if (pressures.empty()) return;
211 const auto& summary_state = this->well_model_.summaryState();
212 auto& well_state = this->wellState();
213 for (auto& well : this->wellContainer()) {
214 if (!well->isProducer() || !well->wellEcl().predictionMode()) continue;
215 const auto it = pressures.find(well->wellEcl().groupName());
216 if (it == pressures.end()) continue;
217 well->setDynamicThpLimit(it->second);
218 auto& ws = well_state[well->indexOfWell()];
219 if (ws.production_cmode == Well::ProducerCMode::THP) {
220 ws.thp = well->getTHPConstraint(summary_state);
221 }
222 }
223}
224
225template<typename TypeTag>
226void
227BlackoilWellModelRescoup<TypeTag>::
228receiveSlaveGroupData()
229{
230 OPM_TIMEFUNCTION();
231 assert(this->isReservoirCouplingMaster());
232 RescoupReceiveSlaveGroupData<Scalar, IndexTraits> slave_group_data_receiver{
233 this->groupStateHelper(),
234 };
235 slave_group_data_receiver.receiveSlaveGroupData();
236}
237
238template<typename TypeTag>
239void
240BlackoilWellModelRescoup<TypeTag>::
241rescoupSyncSummaryData()
242{
243 // Reservoir coupling: exchange production data between slaves and master.
244 //
245 // Master side: after its first substep, the master blocks here until all
246 // slaves have completed the sync step and sent their production data.
247 // This ensures evalSummaryState() (called next in endTimeStep) and all
248 // subsequent master substeps have correct slave production rates.
249 //
250 // Slave side: on the last substep of the sync step, the slave sends its
251 // production data to the master. The master is already waiting at this
252 // point (blocked on MPI_Recv from its first substep's timeStepSucceeded).
253 if (this->isReservoirCouplingMaster()) {
254 if (this->reservoirCouplingMaster().needsSlaveDataReceive()) {
255 this->receiveSlaveGroupData();
256 this->reservoirCouplingMaster().setNeedsSlaveDataReceive(false);
257 }
258 }
259 if (this->isReservoirCouplingSlave()) {
260 if (this->reservoirCouplingSlave().isLastSubstepOfSyncTimestep()) {
261 this->sendSlaveGroupDataToMaster();
262 }
263 }
264}
265
266template<typename TypeTag>
267void
268BlackoilWellModelRescoup<TypeTag>::
269sendCoupledNetworkActiveStatus()
270{
271 OPM_TIMEFUNCTION();
272 assert(this->isReservoirCouplingMaster());
273 // Send to each activated slave a single bool: "are you connected to the
274 // master's cross-rescoup network this sync timestep?", i.e. does this
275 // slave have a master group that is a leaf in the master network. This
276 // is per-slave: a slave with no leaf in the master network does not
277 // participate even if other slaves do. Sent as a dedicated one-element
278 // MPI message; the per-iteration node-pressure sends inside
279 // updateWellControlsAndNetworkIteration() carry their own is_final flag
280 // in the NumMasterGroupNodePressures header.
281 auto& rescoup_master = this->reservoirCouplingMaster();
282 const auto num_slaves = rescoup_master.numSlaves();
283 bool any_connected = false;
284 for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
285 if (rescoup_master.slaveIsActivated(slave_idx)) {
286 const bool connected =
287 this->masterNetworkHasMasterGroupLeavesForSlave_(slave_idx);
288 any_connected = any_connected || connected;
289 rescoup_master.sendCoupledNetworkActiveStatusToSlave(slave_idx, connected);
290 }
291 }
292 // Mirror the *global* active state into the master's own is_final flag:
293 // the master iterates the exchange iff at least one slave is connected.
294 // (This must stay global -- a per-slave value would break the master's
295 // own updateWellControlsAndNetworkIteration() gate.)
296 this->last_sent_master_group_node_pressures_is_final_ = !any_connected;
297}
298
299template<typename TypeTag>
300void
301BlackoilWellModelRescoup<TypeTag>::
302sendMasterGroupConstraintsToSlaves()
303{
304 OPM_TIMEFUNCTION();
305 // This function is called by the master process to send the group
306 // constraints to the slaves. The "will the master iterate cross-rescoup?"
307 // flag is shipped separately by sendCoupledNetworkActiveStatus().
308 RescoupConstraintsCalculator<Scalar, IndexTraits> constraints_calculator{
309 this->well_model_.guideRateHandler(),
310 this->groupStateHelper()
311 };
312 constraints_calculator.calculateMasterGroupConstraintsAndSendToSlaves();
313}
314
315template<typename TypeTag>
316void
317BlackoilWellModelRescoup<TypeTag>::
318sendMasterGroupNodePressuresToSlaves(bool is_final)
319{
320 OPM_TIMEFUNCTION();
321 assert(this->isReservoirCouplingMaster());
322 auto& rescoup_master = this->reservoirCouplingMaster();
323 const auto& node_pressures = this->network_.nodePressures();
324 const auto num_slaves = rescoup_master.numSlaves();
325 for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
326 if (!rescoup_master.slaveIsActivated(slave_idx)) continue;
327 std::vector<typename ReservoirCoupling::MasterGroupNodePressure<Scalar>> pressures;
328 const auto& master_groups = rescoup_master.getMasterGroupNamesForSlave(slave_idx);
329 for (std::size_t i = 0; i < master_groups.size(); ++i) {
330 const auto it = node_pressures.find(master_groups[i]);
331 if (it != node_pressures.end()) {
332 pressures.push_back({i, it->second});
333 }
334 }
335 rescoup_master.sendNumMasterGroupNodePressuresToSlave(
336 slave_idx, pressures.size(), is_final);
337 if (!pressures.empty()) {
338 rescoup_master.sendMasterGroupNodePressuresToSlave(slave_idx, pressures);
339 }
340 }
341 this->last_sent_master_group_node_pressures_is_final_ = is_final;
342}
343
344template<typename TypeTag>
345void
346BlackoilWellModelRescoup<TypeTag>::
347sendSlaveGroupDataToMaster()
348{
349 OPM_TIMEFUNCTION();
350 assert(this->isReservoirCouplingSlave());
351 RescoupSendSlaveGroupData<Scalar, IndexTraits> slave_group_data_sender{
352 this->groupStateHelper()};
353 slave_group_data_sender.sendSlaveGroupDataToMaster();
354}
355
356// Automatically manages the lifecycle of the DeferredLogger pointer
357// in the reservoir coupling logger. Ensures the logger is properly
358// cleared when it goes out of scope, preventing dangling pointer issues:
359//
360// - The ScopedLoggerGuard constructor sets the logger pointer
361// - When the guard goes out of scope, the destructor clears the pointer
362// - Move semantics transfer ownership safely when returning from this function
363// - The moved-from guard is "nullified" and its destructor does nothing
364// - Only the final guard in the caller will clear the logger
365template<typename TypeTag>
366std::optional<ReservoirCoupling::ScopedLoggerGuard>
367BlackoilWellModelRescoup<TypeTag>::
368setupScopedLogger(DeferredLogger& local_logger)
369{
370 if (this->isReservoirCouplingMaster()) {
371 return ReservoirCoupling::ScopedLoggerGuard{
372 this->reservoirCouplingMaster().logger(),
373 &local_logger
374 };
375 } else if (this->isReservoirCouplingSlave()) {
376 return ReservoirCoupling::ScopedLoggerGuard{
377 this->reservoirCouplingSlave().logger(),
378 &local_logger
379 };
380 }
381 return std::nullopt;
382}
383
384// Private methods alphabetically
385// ------------------------------
386
387template<typename TypeTag>
388bool
389BlackoilWellModelRescoup<TypeTag>::
390masterNetworkHasMasterGroupLeavesForSlave_(std::size_t slave_idx) const
391{
392 // See masterNetworkHasMasterGroupLeaves() for why the parsed Schedule
393 // topology is queried rather than the runtime node_pressures_ map.
394 if (!this->isReservoirCouplingMaster()) return false;
395 const int episodeIdx = this->simulator_.episodeIndex();
396 const auto& network = this->schedule()[episodeIdx].network();
397 if (!network.active()) return false;
398 const auto& rcm = this->reservoirCouplingMaster();
399 for (const auto& mg : rcm.getMasterGroupNamesForSlave(slave_idx)) {
400 if (network.has_node(mg)) {
401 return true;
402 }
403 }
404 return false;
405}
406
407
408} // namespace Opm
409
410#endif // RESERVOIR_COUPLING_ENABLED
411#endif // OPM_BLACKOILWELLMODEL_RESCOUP_IMPL_HEADER_INCLUDED
Definition: blackoilbioeffectsmodules.hh:45