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.slaveIsCoupled(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 this->refreshAndSendInjectionTargets_();
113 }
114 }
115}
116
117template<typename TypeTag>
118void
119BlackoilWellModelRescoup<TypeTag>::
120maybeExchangeNetworkSubIterationWithSlaves()
121{
122 // Called for rescoup master for the tight master-slave coupling (--rc-network-loose-coupling=false)
123 // mode at the sub-iteration level: Send node pressures and receive slave rates per master inner
124 // network sub-iteration.
125 // In the loose mode (--rc-network-loose-coupling=true) the exchange happens only once
126 // per master outer iteration (see updateWellControlsAndNetworkIteration), so
127 // the inner loop converges the node pressures against a frozen slave rate.
128 // This loose mode is faster but has been observed in some cases to overshoot to a value that
129 // incorrectly shuts the slave wells in.
130 // To avoid this, the tight coupling is therefore the default coupling.
131 //
132 // NOTE: This is always a non-final exchange. The inner sub-iteration loop cannot
133 // know whether the master's outer network loop will iterate again (the inner
134 // loop ending on max_sub_iter, or on a THP update, still leaves
135 // more_inner_network_update true), so it must not declare termination here.
136 // The single is_final = true send is owned by the per-outer send in
137 // updateWellControlsAndNetworkIteration() (when the outer loop converges)
138 // and by sendSlaveNetworkLoopTerminationSignal_() (when the outer loop hits
139 // max_iter).
140 if (!this->well_model_.useTightRcNetworkCoupling()) {
141 return;
142 }
143 if (!this->masterIsInCoupledNetworkIteration()) {
144 return;
145 }
146 this->sendMasterGroupNodePressuresToSlaves(/*is_final=*/false);
147 this->receiveSlaveGroupData();
148 this->refreshAndSendInjectionTargets_();
149}
150
151template<typename TypeTag>
152void
153BlackoilWellModelRescoup<TypeTag>::
154receiveCoupledNetworkActiveStatus()
155{
156 OPM_TIMEFUNCTION();
157 assert(this->isReservoirCouplingSlave());
158 this->reservoirCouplingSlave().receiveCoupledNetworkActiveStatusFromMaster();
159}
160
161template<typename TypeTag>
162void
163BlackoilWellModelRescoup<TypeTag>::
164receiveGroupConstraintsFromMaster()
165{
166 OPM_TIMEFUNCTION();
167 RescoupReceiveGroupConstraints<Scalar, IndexTraits> constraint_receiver{
168 this->well_model_.guideRateHandler(),
169 this->groupStateHelper()
170 };
171 constraint_receiver.receiveGroupConstraintsFromMaster();
172}
173
174template<typename TypeTag>
175void
176BlackoilWellModelRescoup<TypeTag>::
177receiveMasterGroupNodePressuresFromMaster()
178{
179 OPM_TIMEFUNCTION();
180 assert(this->isReservoirCouplingSlave());
181 auto& rescoup_slave = this->reservoirCouplingSlave();
182 const auto [num_pressures, _is_final] =
183 rescoup_slave.receiveNumMasterGroupNodePressuresFromMaster();
184 if (num_pressures > 0) {
185 rescoup_slave.receiveMasterGroupNodePressuresFromMaster(num_pressures);
186 }
187 // Apply pressures as dynamic THP limits on every producer whose group
188 // has a master-supplied pressure *and* whose group the slave's own deck
189 // placed in its own surface network as a fixed-pressure node. Wells in
190 // master groups that are not network leaves are not touched either (no
191 // entry in the map). Mirrors the standard local-network apply pattern in
192 // BlackoilWellModelNetworkGeneric::updatePressures(): when the well is
193 // currently THP-controlled, also write the new THP into the WellState
194 // directly, because setDynamicThpLimit() alone leaves the active
195 // control's THP value stale and the subsequent well-solve would
196 // converge against the old THP.
197 //
198 // The node-pressure exchange is a boundary condition between two
199 // networks: the master's network ends at the master groups, and the
200 // pressure it computes there is the fixed pressure of the corresponding
201 // slave group's node in the *slave's* network. A slave that declares no
202 // network has no such node, and its wells keep the THP limits from their
203 // own WCONPROD records -- so the messages are still received (the
204 // protocol is unchanged) but nothing is imposed.
205 //
206 // TODO (follow-up PR): when the slave group is a fixed-pressure node with
207 // branches below it, the pressure belongs at the top of the slave's own
208 // network solve rather than directly on the wells; the slave's branches
209 // and their pressure losses then decide the wells' THP limits. The
210 // plumbing exists -- the solver already reads
211 // Network::Node::terminal_pressure() when walking up branches -- but
212 // surfacing the master-sent value into the solver needs a runtime
213 // override path (e.g. a fixed-pressure override map on
214 // BlackoilWellModelNetwork) so we do not mutate the parsed Schedule
215 // network at runtime. Until then the direct write below is exact only
216 // for a slave group that is itself a well group, where there is no
217 // branch between the node and the wells.
218 const auto& pressures = rescoup_slave.masterGroupNodePressures();
219 if (pressures.empty()) return;
220 const auto& summary_state = this->well_model_.summaryState();
221 auto& well_state = this->wellState();
222 for (auto& well : this->wellContainer()) {
223 if (!well->isProducer() || !well->wellEcl().predictionMode()) continue;
224 const auto& group_name = well->wellEcl().groupName();
225 const auto it = pressures.find(group_name);
226 if (it == pressures.end()) continue;
227 if (!this->slaveGroupIsFixedPressureNodeInOwnNetwork_(group_name)) continue;
228 well->setDynamicThpLimit(it->second);
229 auto& ws = well_state[well->indexOfWell()];
230 if (ws.production_cmode == Well::ProducerCMode::THP) {
231 ws.thp = well->getTHPConstraint(summary_state);
232 }
233 }
234}
235
236template<typename TypeTag>
237void
238BlackoilWellModelRescoup<TypeTag>::
239receiveSlaveGroupData()
240{
241 OPM_TIMEFUNCTION();
242 assert(this->isReservoirCouplingMaster());
243 RescoupReceiveSlaveGroupData<Scalar, IndexTraits> slave_group_data_receiver{
244 this->groupStateHelper(),
245 };
246 slave_group_data_receiver.receiveSlaveGroupData();
247}
248
249template<typename TypeTag>
250void
251BlackoilWellModelRescoup<TypeTag>::
252rescoupSyncSummaryData()
253{
254 // Reservoir coupling: exchange production data between slaves and master.
255 //
256 // Master side: after its first substep, the master blocks here until all
257 // slaves have completed the sync step and sent their production data.
258 // This ensures evalSummaryState() (called next in endTimeStep) and all
259 // subsequent master substeps have correct slave production rates.
260 //
261 // Slave side: on the last substep of the sync step, the slave sends its
262 // production data to the master. The master is already waiting at this
263 // point (blocked on MPI_Recv from its first substep's timeStepSucceeded).
264 if (this->isReservoirCouplingMaster()) {
265 if (this->reservoirCouplingMaster().needsSlaveDataReceive()) {
266 this->receiveSlaveGroupData();
267 this->reservoirCouplingMaster().setNeedsSlaveDataReceive(false);
268 }
269 }
270 if (this->isReservoirCouplingSlave()) {
271 if (this->reservoirCouplingSlave().isLastSubstepOfSyncTimestep()) {
272 this->sendSlaveGroupDataToMaster();
273 }
274 }
275}
276
277template<typename TypeTag>
278void
279BlackoilWellModelRescoup<TypeTag>::
280sendCoupledNetworkActiveStatus()
281{
282 OPM_TIMEFUNCTION();
283 assert(this->isReservoirCouplingMaster());
284 // Send to each activated slave a single bool: "are you connected to the
285 // master's cross-rescoup network this sync timestep?", i.e. does this
286 // slave have a master group that is a leaf in the master network. This
287 // is per-slave: a slave with no leaf in the master network does not
288 // participate even if other slaves do. Sent as a dedicated one-element
289 // MPI message; the per-iteration node-pressure sends inside
290 // updateWellControlsAndNetworkIteration() carry their own is_final flag
291 // in the NumMasterGroupNodePressures header.
292 auto& rescoup_master = this->reservoirCouplingMaster();
293 const auto num_slaves = rescoup_master.numSlaves();
294 bool any_connected = false;
295 for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
296 if (rescoup_master.slaveIsCoupled(slave_idx)) {
297 const bool connected =
298 this->masterNetworkHasMasterGroupLeavesForSlave_(slave_idx);
299 any_connected = any_connected || connected;
300 rescoup_master.sendCoupledNetworkActiveStatusToSlave(slave_idx, connected);
301 }
302 }
303 // Mirror the *global* active state into the master's own is_final flag:
304 // the master iterates the exchange iff at least one slave is connected.
305 // (This must stay global -- a per-slave value would break the master's
306 // own updateWellControlsAndNetworkIteration() gate.)
307 this->last_sent_master_group_node_pressures_is_final_ = !any_connected;
308}
309
310template<typename TypeTag>
311void
312BlackoilWellModelRescoup<TypeTag>::
313sendMasterGroupConstraintsToSlaves()
314{
315 OPM_TIMEFUNCTION();
316 // This function is called by the master process to send the group
317 // constraints to the slaves. The "will the master iterate cross-rescoup?"
318 // flag is shipped separately by sendCoupledNetworkActiveStatus().
319 RescoupConstraintsCalculator<Scalar, IndexTraits> constraints_calculator{
320 this->well_model_.guideRateHandler(),
321 this->groupStateHelper()
322 };
323 constraints_calculator.calculateMasterGroupConstraintsAndSendToSlaves();
324}
325
326template<typename TypeTag>
327void
328BlackoilWellModelRescoup<TypeTag>::
329sendMasterGroupNodePressuresToSlaves(bool is_final)
330{
331 OPM_TIMEFUNCTION();
332 assert(this->isReservoirCouplingMaster());
333 auto& rescoup_master = this->reservoirCouplingMaster();
334 const auto& node_pressures = this->network_.nodePressures();
335 const auto num_slaves = rescoup_master.numSlaves();
336 for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
337 if (!rescoup_master.slaveIsCoupled(slave_idx)) continue;
338 std::vector<typename ReservoirCoupling::MasterGroupNodePressure<Scalar>> pressures;
339 const auto& master_groups = rescoup_master.getMasterGroupNamesForSlave(slave_idx);
340 for (std::size_t i = 0; i < master_groups.size(); ++i) {
341 const auto it = node_pressures.find(master_groups[i]);
342 if (it != node_pressures.end()) {
343 pressures.push_back({i, it->second});
344 }
345 }
346 rescoup_master.sendNumMasterGroupNodePressuresToSlave(
347 slave_idx, pressures.size(), is_final);
348 if (!pressures.empty()) {
349 rescoup_master.sendMasterGroupNodePressuresToSlave(slave_idx, pressures);
350 }
351 }
352 this->last_sent_master_group_node_pressures_is_final_ = is_final;
353}
354
355template<typename TypeTag>
356void
357BlackoilWellModelRescoup<TypeTag>::
358sendSlaveGroupDataToMaster()
359{
360 OPM_TIMEFUNCTION();
361 assert(this->isReservoirCouplingSlave());
362 RescoupSendSlaveGroupData<Scalar, IndexTraits> slave_group_data_sender{
363 this->groupStateHelper()};
364 slave_group_data_sender.sendSlaveGroupDataToMaster();
365}
366
367// Automatically manages the lifecycle of the DeferredLogger pointer
368// in the reservoir coupling logger. Ensures the logger is properly
369// cleared when it goes out of scope, preventing dangling pointer issues:
370//
371// - The ScopedLoggerGuard constructor sets the logger pointer
372// - When the guard goes out of scope, the destructor clears the pointer
373// - Move semantics transfer ownership safely when returning from this function
374// - The moved-from guard is "nullified" and its destructor does nothing
375// - Only the final guard in the caller will clear the logger
376template<typename TypeTag>
377std::optional<ReservoirCoupling::ScopedLoggerGuard>
378BlackoilWellModelRescoup<TypeTag>::
379setupScopedLogger(DeferredLogger& local_logger)
380{
381 if (this->isReservoirCouplingMaster()) {
382 return ReservoirCoupling::ScopedLoggerGuard{
383 this->reservoirCouplingMaster().logger(),
384 &local_logger
385 };
386 } else if (this->isReservoirCouplingSlave()) {
387 return ReservoirCoupling::ScopedLoggerGuard{
388 this->reservoirCouplingSlave().logger(),
389 &local_logger
390 };
391 }
392 return std::nullopt;
393}
394
395// Private methods alphabetically
396// ------------------------------
397
398template<typename TypeTag>
399bool
400BlackoilWellModelRescoup<TypeTag>::
401masterNetworkHasMasterGroupLeavesForSlave_(std::size_t slave_idx) const
402{
403 // See masterNetworkHasMasterGroupLeaves() for why the parsed Schedule
404 // topology is queried rather than the runtime node_pressures_ map.
405 if (!this->isReservoirCouplingMaster()) return false;
406 const int episodeIdx = this->simulator_.episodeIndex();
407 const auto& network = this->schedule()[episodeIdx].network();
408 if (!network.active()) return false;
409 const auto& rcm = this->reservoirCouplingMaster();
410 for (const auto& mg : rcm.getMasterGroupNamesForSlave(slave_idx)) {
411 if (network.has_node(mg)) {
412 return true;
413 }
414 }
415 return false;
416}
417
418template<typename TypeTag>
419void
420BlackoilWellModelRescoup<TypeTag>::
421refreshAndSendInjectionTargets_()
422{
423 // Called right after a receiveSlaveGroupData() inside the network iteration.
424 // Fold the rates just received into the master's group state -- that is what
425 // recomputes the reinjection and voidage rates that a GCONINJE REIN, SALE
426 // or VREP target is built from -- and ship the resulting targets to the
427 // slaves, replacing the ones they are currently holding. Without this the
428 // targets stay at the values computed in beginTimeStep(), from slave
429 // production of the previous sync step.
430 const int report_step_idx = this->well_model_.simulator().episodeIndex();
431 this->well_model_.updateAndCommunicateGroupData(
432 report_step_idx, /*update_wellgrouptarget=*/false);
433 this->sendMasterGroupInjectionTargetsToSlaves_();
434}
435
436template<typename TypeTag>
437void
438BlackoilWellModelRescoup<TypeTag>::
439sendMasterGroupInjectionTargetsToSlaves_()
440{
441 OPM_TIMEFUNCTION();
442 RescoupConstraintsCalculator<Scalar, IndexTraits> constraints_calculator{
443 this->well_model_.guideRateHandler(),
444 this->groupStateHelper()
445 };
446 constraints_calculator.recalculateInjectionTargetsAndSendToSlaves();
447}
448
449template<typename TypeTag>
450bool
451BlackoilWellModelRescoup<TypeTag>::
452slaveGroupIsFixedPressureNodeInOwnNetwork_(const std::string& group_name) const
453{
454 const int episodeIdx = this->simulator_.episodeIndex();
455 const auto& network = this->schedule()[episodeIdx].network();
456 if (!network.active()) return false;
457 if (!network.has_node(group_name)) return false;
458 return network.node(group_name).terminal_pressure().has_value();
459}
460
461
462} // namespace Opm
463
464#endif // RESERVOIR_COUPLING_ENABLED
465#endif // OPM_BLACKOILWELLMODEL_RESCOUP_IMPL_HEADER_INCLUDED
Definition: blackoilbioeffectsmodules.hh:45