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
33#include <opm/input/eclipse/EclipseState/Grid/RegionSetMatcher.hpp>
34#include <opm/input/eclipse/Schedule/ResCoup/ReservoirCouplingInfo.hpp>
35#include <opm/input/eclipse/Schedule/Schedule.hpp>
36#include <opm/input/eclipse/Schedule/SummaryState.hpp>
37#include <opm/input/eclipse/Schedule/UDQ/UDQConfig.hpp>
38#include <opm/input/eclipse/Schedule/UDQ/UDQDefine.hpp>
39#include <opm/input/eclipse/Schedule/UDQ/UDQEnums.hpp>
40#include <opm/input/eclipse/Schedule/Well/WellMatcher.hpp>
41#include <opm/input/eclipse/Units/UnitSystem.hpp>
42
50
51#include <algorithm>
52#include <array>
53#include <cassert>
54#include <cstddef>
55#include <memory>
56#include <optional>
57#include <string>
58#include <tuple>
59
60namespace Opm {
61
62// Constructor
63// -----------
64template<typename TypeTag>
65BlackoilWellModelRescoup<TypeTag>::
66BlackoilWellModelRescoup(BlackoilWellModel<TypeTag>& well_model)
67 : well_model_{well_model}
68 , network_{well_model.network()}
69 , simulator_{well_model.simulator()}
70 , param_{well_model.param()}
71{}
72
73// Public methods alphabetically
74// ------------------------------
75
76template<typename TypeTag>
77void
78BlackoilWellModelRescoup<TypeTag>::
79evalGroupAndFieldUDQs()
80{
81 const int reportStepIdx = this->groupStateHelper().reportStepIdx();
82 auto& deferred_logger = this->groupStateHelper().deferredLogger();
83 // UDQs are normally evaluated at the end of a time step. A slave UDQ
84 // that depends on a target imposed by the master would then lag one
85 // step behind, so re-evaluate the group and field level UDQs now that
86 // this step's targets have arrived. Well and segment level UDQs are
87 // left alone: their inputs are not available yet. So is any
88 // "UPDATE NEXT" DEFINE: it is a one-shot that the ordinary end-of-step
89 // evaluation is meant to consume, and this extra evaluation must not
90 // use it up early.
91 const auto select = [](const UDQDefine& def) {
92 const auto var_type = def.var_type();
93 return ((var_type == UDQVarType::GROUP_VAR) || (var_type == UDQVarType::FIELD_VAR))
94 && (def.status().first != UDQUpdate::NEXT);
95 };
96 const auto& schedule = this->schedule();
98 {
99 schedule[reportStepIdx].udq().eval(
100 reportStepIdx,
101 schedule.wellMatcher(reportStepIdx),
102 schedule[reportStepIdx].group_order(),
103 schedule.segmentMatcherFactory(reportStepIdx),
104 [es = std::cref(this->simulator_.vanguard().eclState())]() {
105 return std::make_unique<RegionSetMatcher>(es.get().fipRegionStatistics());
106 },
107 this->simulator_.vanguard().summaryState(),
108 this->simulator_.vanguard().udqState(),
109 select);
110 }
111 OPM_END_PARALLEL_TRY_CATCH_LOG(deferred_logger,
112 "Failed to evaluate UDQs for reservoir coupling slave: ",
113 this->well_model_.terminalOutput(),
114 this->simulator_.vanguard().grid().comm())
115}
116
117template<typename TypeTag>
118bool
119BlackoilWellModelRescoup<TypeTag>::
120masterIsInCoupledNetworkIteration() const
121{
122 return this->isReservoirCouplingMaster()
123 && this->reservoirCouplingMaster().isFirstSubstepOfSyncTimestep()
124 && this->masterNetworkHasMasterGroupLeaves()
125 && !this->lastSentMasterGroupNodePressuresIsFinal();
126}
127
128
129template<typename TypeTag>
130bool
131BlackoilWellModelRescoup<TypeTag>::
132masterNetworkHasMasterGroupLeaves() const
133{
134 // Query the parsed Schedule topology (`network.has_node(...)`) rather than the runtime
135 // `node_pressures_` map. The runtime map is empty on the very first beginTimeStep call
136 // (network_.update has not yet run for this substep).
137 if (!this->isReservoirCouplingMaster()) return false;
138 const auto& rcm = this->reservoirCouplingMaster();
139 const auto num_slaves = rcm.numSlaves();
140 for (std::size_t s = 0; s < num_slaves; ++s) {
141 if (!rcm.slaveIsCoupled(s)) continue;
142 if (this->masterNetworkHasMasterGroupLeavesForSlave_(s)) {
143 return true;
144 }
145 }
146 return false;
147}
148
149template<typename TypeTag>
150void
151BlackoilWellModelRescoup<TypeTag>::
152maybeExchangeNetworkOuterIterationWithSlaves(bool more_network_update)
153{
154 if (!this->masterIsInCoupledNetworkIteration()) {
155 return;
156 }
157 const bool is_final = !more_network_update;
158 // In tight coupling the inner sub-loop (maybeExchangeNetworkSubIterationWithSlaves)
159 // performs every non-final exchange, so here we only emit the single
160 // terminating (is_final) message -- the non-final case would just resend the
161 // same node pressures the last sub-iteration already shipped, and get the same
162 // rates back. The retained final send still covers the case where the network
163 // is not balanced this iteration (the inner loop never runs). In loose coupling
164 // this per-outer send is the sole master<->slave coupling and runs every outer
165 // iteration.
166 if (is_final || !this->well_model_.useTightRcNetworkCoupling()) {
167 // send the pressures freshly computed by network_.update() to all activated slaves
168 this->sendMasterGroupNodePressuresToSlaves(is_final);
169 if (!is_final) {
170 // receive slaves' updated network_surface_rates for the next outer iteration.
171 this->receiveSlaveGroupData();
172 this->refreshAndSendGroupConstraints();
173 }
174 }
175}
176
177template<typename TypeTag>
178void
179BlackoilWellModelRescoup<TypeTag>::
180maybeExchangeNetworkSubIterationWithSlaves()
181{
182 // Called for rescoup master for the tight master-slave coupling (--rc-network-loose-coupling=false)
183 // mode at the sub-iteration level: Send node pressures and receive slave rates per master inner
184 // network sub-iteration.
185 // In the loose mode (--rc-network-loose-coupling=true) the exchange happens only once
186 // per master outer iteration (see updateWellControlsAndNetworkIteration), so
187 // the inner loop converges the node pressures against a frozen slave rate.
188 // This loose mode is faster but has been observed in some cases to overshoot to a value that
189 // incorrectly shuts the slave wells in.
190 // To avoid this, the tight coupling is therefore the default coupling.
191 //
192 // NOTE: This is always a non-final exchange. The inner sub-iteration loop cannot
193 // know whether the master's outer network loop will iterate again (the inner
194 // loop ending on max_sub_iter, or on a THP update, still leaves
195 // more_inner_network_update true), so it must not declare termination here.
196 // The single is_final = true send is owned by the per-outer send in
197 // updateWellControlsAndNetworkIteration() (when the outer loop converges)
198 // and by sendSlaveNetworkLoopTerminationSignal_() (when the outer loop hits
199 // max_iter).
200 if (!this->well_model_.useTightRcNetworkCoupling()) {
201 return;
202 }
203 if (!this->masterIsInCoupledNetworkIteration()) {
204 return;
205 }
206 this->sendMasterGroupNodePressuresToSlaves(/*is_final=*/false);
207 this->receiveSlaveGroupData();
208 this->refreshAndSendGroupConstraints();
209}
210
211template<typename TypeTag>
212void
213BlackoilWellModelRescoup<TypeTag>::
214receiveCoupledNetworkActiveStatus()
215{
216 OPM_TIMEFUNCTION();
217 assert(this->isReservoirCouplingSlave());
218 this->reservoirCouplingSlave().receiveCoupledNetworkActiveStatusFromMaster();
219 if (!this->reservoirCouplingSlave().connectedToMasterCoupledNetwork()) {
220 // No master pressures this sync step: the node pressures are the slave's own again.
221 this->network_.setNodePressureOverrides({});
222 for (auto& well : this->wellContainer()) {
223 well->setLogAllControlSwitches(false);
224 }
225 }
226}
227
228template<typename TypeTag>
229void
230BlackoilWellModelRescoup<TypeTag>::
231receiveGroupConstraintsFromMaster()
232{
233 OPM_TIMEFUNCTION();
234 RescoupReceiveGroupConstraints<Scalar, IndexTraits> constraint_receiver{
235 this->well_model_.guideRateHandler(),
236 this->groupStateHelper()
237 };
238 constraint_receiver.receiveGroupConstraintsFromMaster();
239}
240
241template<typename TypeTag>
242void
243BlackoilWellModelRescoup<TypeTag>::
244receiveMasterGroupNodePressuresFromMaster()
245{
246 OPM_TIMEFUNCTION();
247 assert(this->isReservoirCouplingSlave());
248 auto& rescoup_slave = this->reservoirCouplingSlave();
249 const auto [num_pressures, is_final] =
250 rescoup_slave.receiveNumMasterGroupNodePressuresFromMaster();
251 if (num_pressures > 0) {
252 rescoup_slave.receiveMasterGroupNodePressuresFromMaster(num_pressures);
253 }
254 // Apply pressures as dynamic THP limits on every producer whose group
255 // has a master-supplied pressure *and* whose group the slave's own deck
256 // placed in its own surface network as a fixed-pressure node. Wells in
257 // master groups that are not network leaves are not touched either (no
258 // entry in the map). Mirrors the standard local-network apply pattern in
259 // BlackoilWellModelNetworkGeneric::updatePressures(): when the well is
260 // currently THP-controlled, also write the new THP into the WellState
261 // directly, because setDynamicThpLimit() alone leaves the active
262 // control's THP value stale and the subsequent well-solve would
263 // converge against the old THP.
264 //
265 // The node-pressure exchange is a boundary condition between two
266 // networks: the master's network ends at the master groups, and the
267 // pressure it computes there is the fixed pressure of the corresponding
268 // slave group's node in the *slave's* network. A slave that declares no
269 // network has no such node, and its wells keep the THP limits from their
270 // own WCONPROD records -- so the messages are still received (the
271 // protocol is unchanged) but nothing is imposed.
272 //
273 // The pressure is also the fixed pressure of the node in the slave's own network (see
274 // BlackoilWellModelNetworkGeneric::setNodePressureOverrides()), so that the pressures of
275 // the nodes below it, which are the THP limits of the wells in groups below the node, are
276 // the master's pressure plus the pressure drops in the slave's branches. Without it the
277 // slave's own network update, which follows in every iteration, sets the THP limits of
278 // the wells from the node's own fixed pressure, and the well control update (before it)
279 // and the well solve (after it) would see different limits. The direct write below only
280 // reaches wells in a group that is the node itself.
281 const auto& pressures = rescoup_slave.masterGroupNodePressures();
282 std::map<std::string, Scalar> overrides;
283 for (const auto& [group_name, pressure] : pressures) {
284 if (this->slaveGroupIsFixedPressureNodeInOwnNetwork_(group_name)) {
285 overrides[group_name] = pressure;
286 }
287 }
288 this->network_.setNodePressureOverrides(overrides);
289 // A well whose THP limit comes from the master's pressure, in the node itself or in a node
290 // below it, can sit on the boundary between its group target and that limit: on THP
291 // control its rate exceeds the target, on GRUP the THP it needs is below the limit, and
292 // it switches between the two in every exchange while the master's pressure follows the
293 // rates. Flow's limit on the number of well
294 // switches ends this, but only counts switches after NUPCOL iterations, which the
295 // exchanges (made before the Newton iterations) never reach. Count the switches
296 // during the exchange so that such a well is kept on THP control.
297 for (auto& well : this->wellContainer()) {
298 well->setLogAllControlSwitches(false);
299 }
300 if (pressures.empty()) return;
301 const auto& summary_state = this->well_model_.summaryState();
302 auto& well_state = this->wellState();
303 for (auto& well : this->wellContainer()) {
304 if (!well->isProducer() || !well->wellEcl().predictionMode()) continue;
305 const auto& group_name = well->wellEcl().groupName();
306 if (this->wellIsBelowOverriddenNode_(group_name, overrides)) {
307 well->setLogAllControlSwitches(!is_final);
308 }
309 const auto it = pressures.find(group_name);
310 if (it == pressures.end()) continue;
311 if (!this->slaveGroupIsFixedPressureNodeInOwnNetwork_(group_name)) continue;
312 well->setDynamicThpLimit(it->second);
313 auto& ws = well_state[well->indexOfWell()];
314 // The exchange is made in the first substep of a sync step only. The well objects are
315 // rebuilt at the start of every substep, and a limit that is not in the well state is
316 // lost then (see BlackoilWellModelNetworkGeneric::initializeWell()), so the later
317 // substeps would solve the wells against the limit of their own deck.
318 ws.network_thp_limit = it->second;
319 if (ws.production_cmode == Well::ProducerCMode::THP) {
320 ws.thp = well->getTHPConstraint(summary_state);
321 }
322 }
323}
324
325template<typename TypeTag>
326void
327BlackoilWellModelRescoup<TypeTag>::
328receiveRefreshedGroupConstraintsFromMaster()
329{
330 // The counterpart of refreshAndSendGroupConstraints() on the master. The
331 // constraints received replace the ones in force, so the slave group
332 // control modes, the targets and limits in force and the UDQs built on
333 // them follow suit.
334 this->receiveGroupConstraintsFromMaster();
335 this->groupStateHelper().updateSlaveGroupCmodesFromMaster();
336 this->refreshSlaveGroupTargets();
337}
338
339template<typename TypeTag>
340void
341BlackoilWellModelRescoup<TypeTag>::
342receiveSlaveGroupData()
343{
344 OPM_TIMEFUNCTION();
345 assert(this->isReservoirCouplingMaster());
346 RescoupReceiveSlaveGroupData<Scalar, IndexTraits> slave_group_data_receiver{
347 this->groupStateHelper(),
348 };
349 slave_group_data_receiver.receiveSlaveGroupData();
350}
351
352template<typename TypeTag>
353void
354BlackoilWellModelRescoup<TypeTag>::
355refreshAndSendGroupConstraints()
356{
357 // Called right after a receiveSlaveGroupData() within a sync step: after
358 // the slaves' initial well solve at the start of the step, and inside the
359 // network iteration. Fold the rates just received into the master's group
360 // state and ship constraints recomputed from them to the slaves, replacing
361 // the ones they are currently holding. The constraints sent first in the
362 // sync step are computed from the slave rates before the slaves solved
363 // their wells, which lag behind in two ways:
364 // - a group whose producers are not under group control has its current
365 // rate subtracted from the parent's target (see
366 // RescoupConstraintsCalculator::excludeAndRedistributeProductionTargets_()),
367 // and for wells that open this step that rate is not known before the
368 // solve, so the siblings would share the full parent target;
369 // - a target derived from slave production (GCONINJE REIN, SALE or VREP)
370 // would be built from the previous sync step's production.
371 const int report_step_idx = this->well_model_.simulator().episodeIndex();
372 this->well_model_.updateAndCommunicateGroupData(
373 report_step_idx, /*update_wellgrouptarget=*/false);
374 this->sendMasterGroupConstraintsToSlaves();
375}
376
377template<typename TypeTag>
378void
379BlackoilWellModelRescoup<TypeTag>::
380refreshSlaveGroupTargets()
381{
382 // The deck's own GCONINJE and GCONPROD limits may be UDAs, resolved from
383 // the summary state, and the UDQ evaluation below may change them. So
384 // prime, evaluate the selected UDQs, and work the targets in force out
385 // once more with the UDQs current, as the well control path will see
386 // them. The second pass repeats the loops over the slave groups, not
387 // the UDQ evaluation.
388 this->storeSlaveGroupInjectionTargets();
389 this->storeSlaveGroupProductionTargets();
390 this->evalGroupAndFieldUDQs();
391 this->storeSlaveGroupInjectionTargets();
392 this->storeSlaveGroupProductionTargets();
393}
394
395template<typename TypeTag>
396void
397BlackoilWellModelRescoup<TypeTag>::
398rescoupSyncSummaryData()
399{
400 // Reservoir coupling: exchange production data between slaves and master.
401 //
402 // Master side: after its first substep, the master blocks here until all
403 // slaves have completed the sync step and sent their production data.
404 // This ensures evalSummaryState() (called next in endTimeStep) and all
405 // subsequent master substeps have correct slave production rates.
406 //
407 // Slave side: on the last substep of the sync step, the slave sends its
408 // production data to the master. The master is already waiting at this
409 // point (blocked on MPI_Recv from its first substep's timeStepSucceeded).
410 // The group rates are first recomputed from the converged well rates: the
411 // group state holds the rates of the NUPCOL well state, which is frozen
412 // after the first NUPCOL Newton iterations of the step. The step has
413 // converged, so there is nothing for NUPCOL to stabilise any more, and the
414 // master uses these rates for its summary output and for its own substeps
415 // until the next sync step. (The data sent at the start of a sync step is
416 // computed in the first Newton iteration, where the NUPCOL well state is the
417 // current one.)
418 if (this->isReservoirCouplingMaster()) {
419 if (this->reservoirCouplingMaster().needsSlaveDataReceive()) {
420 // The rates held before the receive are the slaves' start-of-step rates, which
421 // GRUPMAST item 4 compares with the end-of-step rates. They come from the last
422 // receive of the sync step, so they are the converged start state: after the
423 // slaves' initial well solve and any network iterations. The first receive is
424 // too early, wells that open at the start of the step still have zero rate then.
425 this->reservoirCouplingMaster().saveGroupFlowsAtStartOfSyncStep();
426 this->receiveSlaveGroupData();
427 this->reservoirCouplingMaster().updateFlowChangeTimeStepLimit();
428 this->reservoirCouplingMaster().setNeedsSlaveDataReceive(false);
429 }
430 }
431 if (this->isReservoirCouplingSlave()) {
432 if (this->reservoirCouplingSlave().isLastSubstepOfSyncTimestep()) {
433 this->well_model_.updateGroupRatesFromWellState(
434 this->well_model_.simulator().episodeIndex());
435 this->sendSlaveGroupDataToMaster();
436 }
437 }
438}
439
440template<typename TypeTag>
441void
442BlackoilWellModelRescoup<TypeTag>::
443sendCoupledNetworkActiveStatus()
444{
445 OPM_TIMEFUNCTION();
446 assert(this->isReservoirCouplingMaster());
447 // Send to each activated slave a single bool: "are you connected to the
448 // master's cross-rescoup network this sync timestep?", i.e. does this
449 // slave have a master group that is a leaf in the master network. This
450 // is per-slave: a slave with no leaf in the master network does not
451 // participate even if other slaves do. Sent as a dedicated one-element
452 // MPI message; the per-iteration node-pressure sends inside
453 // updateWellControlsAndNetworkIteration() carry their own is_final flag
454 // in the NumMasterGroupNodePressures header.
455 auto& rescoup_master = this->reservoirCouplingMaster();
456 const auto num_slaves = rescoup_master.numSlaves();
457 bool any_connected = false;
458 for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
459 if (rescoup_master.slaveIsCoupled(slave_idx)) {
460 const bool connected =
461 this->masterNetworkHasMasterGroupLeavesForSlave_(slave_idx);
462 any_connected = any_connected || connected;
463 rescoup_master.sendCoupledNetworkActiveStatusToSlave(slave_idx, connected);
464 }
465 }
466 // Mirror the *global* active state into the master's own is_final flag:
467 // the master iterates the exchange iff at least one slave is connected.
468 // (This must stay global -- a per-slave value would break the master's
469 // own updateWellControlsAndNetworkIteration() gate.)
470 this->last_sent_master_group_node_pressures_is_final_ = !any_connected;
471}
472
473template<typename TypeTag>
474void
475BlackoilWellModelRescoup<TypeTag>::
476sendMasterGroupConstraintsToSlaves()
477{
478 OPM_TIMEFUNCTION();
479 // This function is called by the master process to send the group
480 // constraints to the slaves. The "will the master iterate cross-rescoup?"
481 // flag is shipped separately by sendCoupledNetworkActiveStatus().
482 RescoupConstraintsCalculator<Scalar, IndexTraits> constraints_calculator{
483 this->well_model_.guideRateHandler(),
484 this->groupStateHelper()
485 };
486 constraints_calculator.calculateMasterGroupConstraintsAndSendToSlaves();
487}
488
489template<typename TypeTag>
490void
491BlackoilWellModelRescoup<TypeTag>::
492sendMasterGroupNodePressuresToSlaves(bool is_final)
493{
494 OPM_TIMEFUNCTION();
495 assert(this->isReservoirCouplingMaster());
496 auto& rescoup_master = this->reservoirCouplingMaster();
497 const auto& node_pressures = this->network_.nodePressures();
498 const auto num_slaves = rescoup_master.numSlaves();
499 for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
500 if (!rescoup_master.slaveIsCoupled(slave_idx)) continue;
501 std::vector<typename ReservoirCoupling::MasterGroupNodePressure<Scalar>> pressures;
502 const auto& master_groups = rescoup_master.getMasterGroupNamesForSlave(slave_idx);
503 for (std::size_t i = 0; i < master_groups.size(); ++i) {
504 const auto it = node_pressures.find(master_groups[i]);
505 if (it != node_pressures.end()) {
506 pressures.push_back({i, it->second});
507 }
508 }
509 rescoup_master.sendNumMasterGroupNodePressuresToSlave(
510 slave_idx, pressures.size(), is_final);
511 if (!pressures.empty()) {
512 rescoup_master.sendMasterGroupNodePressuresToSlave(slave_idx, pressures);
513 }
514 }
515 this->last_sent_master_group_node_pressures_is_final_ = is_final;
516}
517
518template<typename TypeTag>
519void
520BlackoilWellModelRescoup<TypeTag>::
521sendSlaveGroupDataToMaster()
522{
523 OPM_TIMEFUNCTION();
524 assert(this->isReservoirCouplingSlave());
525 RescoupSendSlaveGroupData<Scalar, IndexTraits> slave_group_data_sender{
526 this->groupStateHelper(), this->well_model_.nupcolWellState()};
527 slave_group_data_sender.sendSlaveGroupDataToMaster();
528}
529
530// Automatically manages the lifecycle of the DeferredLogger pointer
531// in the reservoir coupling logger. Ensures the logger is properly
532// cleared when it goes out of scope, preventing dangling pointer issues:
533//
534// - The ScopedLoggerGuard constructor sets the logger pointer
535// - When the guard goes out of scope, the destructor clears the pointer
536// - Move semantics transfer ownership safely when returning from this function
537// - The moved-from guard is "nullified" and its destructor does nothing
538// - Only the final guard in the caller will clear the logger
539template<typename TypeTag>
540std::optional<ReservoirCoupling::ScopedLoggerGuard>
541BlackoilWellModelRescoup<TypeTag>::
542setupScopedLogger(DeferredLogger& local_logger)
543{
544 if (this->isReservoirCouplingMaster()) {
545 return ReservoirCoupling::ScopedLoggerGuard{
546 this->reservoirCouplingMaster().logger(),
547 &local_logger
548 };
549 } else if (this->isReservoirCouplingSlave()) {
550 return ReservoirCoupling::ScopedLoggerGuard{
551 this->reservoirCouplingSlave().logger(),
552 &local_logger
553 };
554 }
555 return std::nullopt;
556}
557
558template<typename TypeTag>
559void
560BlackoilWellModelRescoup<TypeTag>::
561storeSlaveGroupInjectionTargets()
562{
563 const int reportStepIdx = this->groupStateHelper().reportStepIdx();
564 // The injection target that applies to a slave group is not something
565 // the slave's schedule can know on its own: it depends on the target
566 // the master imposes at every synchronization step and on the group's
567 // GRUPSLAV filter flag, which says whether the master's limit, the
568 // deck's own GCONINJE limit, or the smaller of the two is in force.
569 // Work out that effective target and keep it on the slave, where the
570 // summary writer picks it up and reports it as GGIRT/GWIRT. Only
571 // surface rate targets map onto these keywords.
572 //
573 // A UDQ that refers to the target reads it from the summary state, so
574 // the summary state is primed with the same value, in output units,
575 // ahead of the UDQ evaluation that follows. When nothing is in force
576 // the slot is primed with the schedule's own target instead -- what the
577 // end-of-step summary evaluation will report -- so that a target in
578 // force on an earlier sync step cannot linger there.
579 using M = UnitSystem::measure;
580 auto& summary_state = this->simulator_.vanguard().summaryState();
581 const auto& units = this->simulator_.vanguard().eclState().getUnits();
582 auto& slave = this->reservoirCouplingSlave();
583 const auto& rescoup = this->schedule()[reportStepIdx].rescoup();
584 // The surface-rate group injection target vectors, which is all of them:
585 // there is no oil equivalent, so a master oil injection target reaches the
586 // control path but has nothing to be reported as; and GVIRT is a reservoir
587 // volume target, which cannot be derived from what the master sends, since
588 // it converts every injection mode to a surface rate before sending (see
589 // RescoupConstraintsCalculator::calculateSlaveGroupInjectionTargets_()).
590 // A slave UDQ naming GVIRT for a slave group therefore still reads the
591 // schedule, as it did before reservoir coupling could report a target at
592 // all.
593 const auto targets = std::array {
594 std::tuple { Phase::GAS, std::string{"GGIRT"}, M::gas_surface_rate },
595 std::tuple { Phase::WATER, std::string{"GWIRT"}, M::liquid_surface_rate },
596 };
597 auto& in_force = slave.effectiveInjectionTargets();
598 in_force.clear();
599 for (std::size_t i = 0; i < slave.numSlaveGroups(); ++i) {
600 const auto& gname = slave.slaveGroupIdxToGroupName(i);
601 for (const auto& [phase, keyword, unit] : targets) {
602 const auto effective = this->effectiveSlaveGroupInjectionTarget_(
603 gname, phase, reportStepIdx, rescoup, summary_state);
604 if (effective.has_value()) {
605 in_force[gname][phase] = *effective;
606 summary_state.update_group_var(gname, keyword, units.from_si(unit, *effective));
607 }
608 else {
609 summary_state.update_group_var(
610 gname, keyword,
611 units.from_si(unit, this->scheduleInjectionTarget_(gname, phase, reportStepIdx, summary_state)));
612 }
613 }
614 }
615}
616
617template<typename TypeTag>
618void
619BlackoilWellModelRescoup<TypeTag>::
620storeSlaveGroupProductionTargets()
621{
622 const int reportStepIdx = this->groupStateHelper().reportStepIdx();
623 // As storeSlaveGroupInjectionTargets(), for the production rate limits.
624 // The master sends a slave group a limit for every rate type: the target
625 // of its active control mode, and its share of the other limits found up
626 // the master's group tree. The GRUPSLAV flag says how each combines with
627 // the deck's own GCONPROD limit. Work out the limit in force for each
628 // rate type, keep it on the slave for the summary writer, and prime the
629 // summary state for the UDQ evaluation that follows -- with the
630 // schedule's own limit when nothing is in force.
631 //
632 // The values are primed in the measures the summary evaluators use.
633 // RESV is left out: GVPRT does not report a group target, it adds up the
634 // reservoir volume rate targets of the group's wells.
635 using M = UnitSystem::measure;
636 using CMode = Group::ProductionCMode;
637 auto& summary_state = this->simulator_.vanguard().summaryState();
638 const auto& units = this->simulator_.vanguard().eclState().getUnits();
639 auto& slave = this->reservoirCouplingSlave();
640 const auto& rescoup = this->schedule()[reportStepIdx].rescoup();
641 const auto targets = std::array {
642 std::tuple { CMode::ORAT, std::string{"GOPRT"}, M::rate },
643 std::tuple { CMode::WRAT, std::string{"GWPRT"}, M::rate },
644 std::tuple { CMode::GRAT, std::string{"GGPRT"}, M::gas_surface_rate },
645 std::tuple { CMode::LRAT, std::string{"GLPRT"}, M::rate },
646 };
647 auto& in_force = slave.effectiveProductionTargets();
648 in_force.clear();
649 for (std::size_t i = 0; i < slave.numSlaveGroups(); ++i) {
650 const auto& gname = slave.slaveGroupIdxToGroupName(i);
651 for (const auto& [cmode, keyword, unit] : targets) {
652 const auto effective = this->effectiveSlaveGroupProductionTarget_(
653 gname, cmode, reportStepIdx, rescoup, summary_state);
654 if (effective.has_value()) {
655 in_force[gname][cmode] = *effective;
656 summary_state.update_group_var(gname, keyword, units.from_si(unit, *effective));
657 }
658 else {
659 summary_state.update_group_var(
660 gname, keyword,
661 units.from_si(unit, this->scheduleProductionTarget_(gname, cmode, reportStepIdx, summary_state)));
662 }
663 }
664 }
665}
666
667// Private methods alphabetically
668// ------------------------------
669
670template<typename TypeTag>
671std::optional<typename BlackoilWellModelRescoup<TypeTag>::Scalar>
672BlackoilWellModelRescoup<TypeTag>::
673effectiveSlaveGroupInjectionTarget_(const std::string& gname,
674 const Phase phase,
675 const int reportStepIdx,
676 const ReservoirCoupling::CouplingInfo& rescoup,
677 const SummaryState& summary_state) const
678{
679 using FilterFlag = ReservoirCoupling::GrupSlav::FilterFlag;
680 const auto& slave = this->reservoirCouplingSlave();
681 if (! slave.hasMasterInjectionTarget(gname, phase)) {
682 return std::nullopt;
683 }
684 const auto [master_target, cmode] = slave.masterInjectionTarget(gname, phase);
685 if (cmode != Group::InjectionCMode::RATE) {
686 return std::nullopt;
687 }
688
689 // Same fallback as GroupStateHelper::getInjectionFilterFlag_(): a slave
690 // group without a GRUPSLAV record follows the master.
691 auto filter = FilterFlag::MAST;
692 if (rescoup.hasGrupSlav(gname)) {
693 const auto& grup_slav = rescoup.grupSlav(gname);
694 filter = (phase == Phase::GAS) ? grup_slav.gasInjFlag()
695 : (phase == Phase::WATER) ? grup_slav.waterInjFlag()
696 : grup_slav.oilInjFlag();
697 }
698 if (filter == FilterFlag::SLAV) {
699 // The deck's own limit applies; the summary evaluator reads it from
700 // the schedule as for any other group.
701 return std::nullopt;
702 }
703 if (filter == FilterFlag::BOTH) {
704 const auto& group = this->schedule().getGroup(gname, reportStepIdx);
705 if (group.hasInjectionControl(phase)) {
706 return std::min(master_target,
707 this->scheduleInjectionTarget_(gname, phase, reportStepIdx, summary_state));
708 }
709 }
710 return master_target;
711}
712
713template<typename TypeTag>
714std::optional<typename BlackoilWellModelRescoup<TypeTag>::Scalar>
715BlackoilWellModelRescoup<TypeTag>::
716effectiveSlaveGroupProductionTarget_(const std::string& gname,
717 const Group::ProductionCMode cmode,
718 const int reportStepIdx,
719 const ReservoirCoupling::CouplingInfo& rescoup,
720 const SummaryState& summary_state) const
721{
722 // The rule GroupStateHelper::getEffectiveProductionLimit_() applies in
723 // the control path, with one difference. That method is only reached
724 // for a rate type the group has a GCONPROD limit of its own for, while
725 // a limit is reported here for every rate type, so the deck's own limit
726 // only takes part under BOTH when the group has one.
727 using CMode = Group::ProductionCMode;
728 using FilterFlag = ReservoirCoupling::GrupSlav::FilterFlag;
729 const auto& slave = this->reservoirCouplingSlave();
730 if (! slave.hasMasterProductionLimits(gname)) {
731 return std::nullopt;
732 }
733 const auto& limits = slave.masterProductionLimits(gname);
734 const Scalar master_limit = (cmode == CMode::ORAT) ? limits.oil_limit
735 : (cmode == CMode::WRAT) ? limits.water_limit
736 : (cmode == CMode::GRAT) ? limits.gas_limit
737 : limits.liquid_limit;
738 if (master_limit < 0) {
739 // No limit of this type up the master's group tree: the deck's own
740 // limit applies, and the summary evaluator reads it from the schedule.
741 return std::nullopt;
742 }
743
744 // Same mapping and fallback as GroupStateHelper::getProductionFilterFlag_():
745 // the second production flag of GRUPSLAV covers both water and liquid, and
746 // a slave group without a GRUPSLAV record follows the master.
747 auto filter = FilterFlag::MAST;
748 if (rescoup.hasGrupSlav(gname)) {
749 const auto& grup_slav = rescoup.grupSlav(gname);
750 filter = (cmode == CMode::ORAT) ? grup_slav.oilProdFlag()
751 : (cmode == CMode::GRAT) ? grup_slav.gasProdFlag()
752 : grup_slav.liquidProdFlag();
753 }
754 if (filter == FilterFlag::SLAV) {
755 return std::nullopt;
756 }
757 if (filter == FilterFlag::BOTH) {
758 const auto& group = this->schedule().getGroup(gname, reportStepIdx);
759 if (group.has_control(cmode)) {
760 return std::min(master_limit,
761 this->scheduleProductionTarget_(gname, cmode, reportStepIdx, summary_state));
762 }
763 }
764 return master_limit;
765}
766
767template<typename TypeTag>
768bool
769BlackoilWellModelRescoup<TypeTag>::
770masterNetworkHasMasterGroupLeavesForSlave_(std::size_t slave_idx) const
771{
772 // See masterNetworkHasMasterGroupLeaves() for why the parsed Schedule
773 // topology is queried rather than the runtime node_pressures_ map.
774 if (!this->isReservoirCouplingMaster()) return false;
775 const int episodeIdx = this->simulator_.episodeIndex();
776 const auto& network = this->schedule()[episodeIdx].network();
777 if (!network.active()) return false;
778 const auto& rcm = this->reservoirCouplingMaster();
779 for (const auto& mg : rcm.getMasterGroupNamesForSlave(slave_idx)) {
780 if (network.has_node(mg)) {
781 return true;
782 }
783 }
784 return false;
785}
786
787template<typename TypeTag>
788typename BlackoilWellModelRescoup<TypeTag>::Scalar
789BlackoilWellModelRescoup<TypeTag>::
790scheduleInjectionTarget_(const std::string& gname,
791 const Phase phase,
792 const int reportStepIdx,
793 const SummaryState& summary_state) const
794{
795 const auto& group = this->schedule().getGroup(gname, reportStepIdx);
796 if (! group.hasInjectionControl(phase)) {
797 return Scalar{0};
798 }
799 return static_cast<Scalar>(group.injectionControls(phase, summary_state).surface_max_rate);
800}
801
802template<typename TypeTag>
803typename BlackoilWellModelRescoup<TypeTag>::Scalar
804BlackoilWellModelRescoup<TypeTag>::
805scheduleProductionTarget_(const std::string& gname,
806 const Group::ProductionCMode cmode,
807 const int reportStepIdx,
808 const SummaryState& summary_state) const
809{
810 using CMode = Group::ProductionCMode;
811 const auto controls = this->schedule().getGroup(gname, reportStepIdx)
812 .productionControls(summary_state);
813 switch (cmode) {
814 case CMode::ORAT: return static_cast<Scalar>(controls.oil_target);
815 case CMode::WRAT: return static_cast<Scalar>(controls.water_target);
816 case CMode::GRAT: return static_cast<Scalar>(controls.gas_target);
817 case CMode::LRAT: return static_cast<Scalar>(controls.liquid_target);
818 default: return Scalar{0};
819 }
820}
821
822template<typename TypeTag>
823bool
824BlackoilWellModelRescoup<TypeTag>::
825slaveGroupIsFixedPressureNodeInOwnNetwork_(const std::string& group_name) const
826{
827 const int episodeIdx = this->simulator_.episodeIndex();
828 const auto& network = this->schedule()[episodeIdx].network();
829 if (!network.active()) return false;
830 if (!network.has_node(group_name)) return false;
831 return network.node(group_name).terminal_pressure().has_value();
832}
833
834template<typename TypeTag>
835bool
836BlackoilWellModelRescoup<TypeTag>::
837wellIsBelowOverriddenNode_(const std::string& group_name,
838 const std::map<std::string, Scalar>& overrides) const
839{
840 if (overrides.empty()) return false;
841 const int episodeIdx = this->simulator_.episodeIndex();
842 const auto& network = this->schedule()[episodeIdx].network();
843 if (!network.active() || !network.has_node(group_name)) return false;
844 return nodeFollowsOverriddenNode(network, group_name, overrides);
845}
846
847
848} // namespace Opm
849
850#endif // RESERVOIR_COUPLING_ENABLED
851#endif // OPM_BLACKOILWELLMODEL_RESCOUP_IMPL_HEADER_INCLUDED
#define OPM_END_PARALLEL_TRY_CATCH_LOG(obptc_logger, obptc_prefix, obptc_output, comm)
Catch exception, log, and throw in a parallel try-catch clause.
Definition: DeferredLoggingErrorHelpers.hpp:207
#define OPM_BEGIN_PARALLEL_TRY_CATCH()
Macro to setup the try of a parallel try-catch.
Definition: DeferredLoggingErrorHelpers.hpp:160
if(!linsolver_)
Definition: FlexibleSolver_impl.hpp:336
Phase
Phase indices for reservoir coupling, we currently only support black-oil phases (oil,...
Definition: ReservoirCoupling.hpp:183
Definition: blackoilbioeffectsmodules.hh:45
bool nodeFollowsOverriddenNode(const Network::ExtNetwork &network, std::string node, const std::map< std::string, Scalar > &overrides)
Whether the pressure of a node follows from one of the overridden nodes (see the terminal pressure ov...
Definition: BlackoilWellModelNetworkPressureComputation.hpp:304