opm-simulators
BlackoilWellModelRescoup_impl.hpp
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>
26 #include <opm/simulators/wells/BlackoilWellModelRescoup.hpp>
27 #endif
28 
29 #ifdef RESERVOIR_COUPLING_ENABLED
30 
31 #include <opm/common/TimingMacros.hpp>
32 
33 #include <opm/simulators/wells/BlackoilWellModel.hpp>
34 #include <opm/simulators/wells/rescoup/RescoupConstraintsCalculator.hpp>
35 #include <opm/simulators/wells/rescoup/RescoupReceiveGroupConstraints.hpp>
36 #include <opm/simulators/wells/rescoup/RescoupReceiveSlaveGroupData.hpp>
37 #include <opm/simulators/wells/rescoup/RescoupSendSlaveGroupData.hpp>
38 
39 #include <cassert>
40 
41 namespace Opm {
42 
43 // Constructor
44 // -----------
45 template<typename TypeTag>
46 BlackoilWellModelRescoup<TypeTag>::
47 BlackoilWellModelRescoup(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 
57 template<typename TypeTag>
58 bool
59 BlackoilWellModelRescoup<TypeTag>::
60 masterIsInCoupledNetworkIteration() const
61 {
62  return this->isReservoirCouplingMaster()
63  && this->reservoirCouplingMaster().isFirstSubstepOfSyncTimestep()
64  && this->masterNetworkHasMasterGroupLeaves()
65  && !this->lastSentMasterGroupNodePressuresIsFinal();
66 }
67 
68 
69 template<typename TypeTag>
70 bool
71 BlackoilWellModelRescoup<TypeTag>::
72 masterNetworkHasMasterGroupLeaves() 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 
89 template<typename TypeTag>
90 void
91 BlackoilWellModelRescoup<TypeTag>::
92 maybeExchangeNetworkOuterIterationWithSlaves(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 
116 template<typename TypeTag>
117 void
118 BlackoilWellModelRescoup<TypeTag>::
119 maybeExchangeNetworkSubIterationWithSlaves()
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 
149 template<typename TypeTag>
150 void
151 BlackoilWellModelRescoup<TypeTag>::
152 receiveCoupledNetworkActiveStatus()
153 {
154  OPM_TIMEFUNCTION();
155  assert(this->isReservoirCouplingSlave());
156  this->reservoirCouplingSlave().receiveCoupledNetworkActiveStatusFromMaster();
157 }
158 
159 template<typename TypeTag>
160 void
161 BlackoilWellModelRescoup<TypeTag>::
162 receiveGroupConstraintsFromMaster()
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 
172 template<typename TypeTag>
173 void
174 BlackoilWellModelRescoup<TypeTag>::
175 receiveMasterGroupNodePressuresFromMaster()
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 
225 template<typename TypeTag>
226 void
227 BlackoilWellModelRescoup<TypeTag>::
228 receiveSlaveGroupData()
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 
238 template<typename TypeTag>
239 void
240 BlackoilWellModelRescoup<TypeTag>::
241 rescoupSyncSummaryData()
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 
266 template<typename TypeTag>
267 void
268 BlackoilWellModelRescoup<TypeTag>::
269 sendCoupledNetworkActiveStatus()
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 
299 template<typename TypeTag>
300 void
301 BlackoilWellModelRescoup<TypeTag>::
302 sendMasterGroupConstraintsToSlaves()
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 
315 template<typename TypeTag>
316 void
317 BlackoilWellModelRescoup<TypeTag>::
318 sendMasterGroupNodePressuresToSlaves(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 
344 template<typename TypeTag>
345 void
346 BlackoilWellModelRescoup<TypeTag>::
347 sendSlaveGroupDataToMaster()
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
365 template<typename TypeTag>
366 std::optional<ReservoirCoupling::ScopedLoggerGuard>
367 BlackoilWellModelRescoup<TypeTag>::
368 setupScopedLogger(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 
387 template<typename TypeTag>
388 bool
389 BlackoilWellModelRescoup<TypeTag>::
390 masterNetworkHasMasterGroupLeavesForSlave_(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
Structs needed for tpfalinearizer and its gpuparams struct extracted to be defined in one place that ...
Definition: blackoilbioeffectsmodules.hh:45