Merge pull request #7025 from hakonhagland/fix_mg_gr_dist2

Reservoir coupling: Cap master-group targets at slave potential
This commit is contained in:
Tor Harald Sandve authored and GitHub committed 2026-05-04 10:36:05 +02:00
commit 42990edc31
5 files changed
+502 -39

No files matched your search

@@ -400,6 +400,15 @@ template<class Scalar> class WellContributions;
/// @brief Check if this process is a reservoir coupling slave
bool isReservoirCouplingSlave() const { return rescoup_.isSlave(); }
/// @brief Check if a group is a reservoir coupling master group.
/// @details Returns true only when this process is a master and the
/// named group is registered as a master group. Safe to
/// call unconditionally — returns false in non-MPI builds
/// and on non-master processes.
bool isReservoirCouplingMasterGroup(const std::string& group_name) const {
return rescoup_.isMasterGroup(group_name);
}
/// @brief Get reference to reservoir coupling master
/// @note Caller must ensure isReservoirCouplingMaster() is true
ReservoirCouplingMaster<Scalar>& reservoirCouplingMaster() {
@@ -1819,6 +1819,15 @@ namespace Opm {
const int reportStepIdx)
{
OPM_TIMEFUNCTION();
// Reservoir coupling: a master group's control and target are assigned
// by RescoupConstraintsCalculator at the start of each sync step and
// must be treated as read-only for the remainder of the step. Skip
// higher-level and individual-control checks so they don't override
// the assigned state. Master groups have no subordinate wells or
// groups, so no recursion is needed.
if (this->isReservoirCouplingMasterGroup(group.name())) {
return false;
}
const auto& iterCtx = simulator_.problem().iterationContext();
bool changed = false;
// restrict the number of group switches but only after nupcol iterations.
+21 -10
View File
@@ -2654,19 +2654,30 @@ GroupStateHelper<Scalar, IndexTraits>::updateGroupControlledWellsRecursive_(
{
// NOTE: The number of group controlled wells (GCW) is only relevant for groups that are NOT under
// individual control. However, it is collected for all groups here.
// For a group not under individual control, GCW is used to check if it should participate in
// target reduction and guide rate control.
const Group& group = this->schedule_.getGroup(group_name, this->report_step_);
int num_wells = 0;
// NOTE: If "group" is a reservoir coupling master group, it should have no child groups or wells.
// As a convention, we will assign GCW = 1 (GCW=Group Controlled Wells). This is consistent with
// the usage of GCW elsewhere in the code: GCW is only checked for GCW>0, the number of wells if
// greater than zero is irrelevant.
// 1) for target reductions, GCW is used to check if a group not under individual control and with a guide rate
// set should participate in target reduction, and
// 2) for guide rate control, GCW is used to check if a group not under individual control should (or
// under individual control if "always_included_child" in localFraction():FractionCalculator.cpp is true)
// should participate in guide rate control.
if (this->isReservoirCouplingMasterGroup(group)) {
num_wells = 1;
// NOTE: If "group" is a reservoir coupling master group, it should have no child groups or wells.
// During master group constraint calculation, we will set individual control on master groups
// to exclude them from guide rate distribution, see
// RescoupConstraintsCalculator::calculateMasterGroupConstraintsAndSendToSlaves().
// TODO: a master group with individual control and GCONPROD item 8 = "YES"
// (RESPOND_TO_PARENT = YES) has an own rate limit AND is available for
// higher-level group control. The current control mode-driven check
// below assigns GCW=0 for such groups, which excludes them from
// guide-rate distribution and yields a wrong target.
// Proposed fix: effective-GCW accessor on ReservoirCouplingMaster + extension
// to FractionCalculator::guideRateSum. Deferred to a follow-up PR.
if (is_production_group) {
const auto ctrl = this->groupState().production_control(group_name);
const bool individual = (ctrl != Group::ProductionCMode::FLD
&& ctrl != Group::ProductionCMode::NONE);
num_wells = individual ? 0 : 1;
} else {
num_wells = 1; // injection: not yet handled
}
}
else {
// NOTE: A group with sub groups cannot also have direct wells (one level below) under its control.
@@ -60,8 +60,54 @@ RescoupConstraintsCalculator(
// constraint for each rate type independently (the active cmode on the master side may differ
// from what is binding on the slave side).
//
// Details on the target calculation:
// ----------------------------------
// Overview of the phases:
// -----------------------
// The constraint calculation is split into three phases. All phases run on
// every rank of the master communicator (collective MPI ops are required by
// the underlying GroupStateHelper / GroupConstraintCalculator); the
// slave-facing MPI sends in Phase 3 are rank-0-only inside the send helpers.
//
// Pre-phase: restore master-group control modes from the Schedule, switch
// inactive slaves' master groups to individual control so they don't
// consume any share of the parent's target, and recompute GCW + FIELD
// target reductions with the resulting state.
//
// Phase 1 (per-slave per-master-group): compute initial guide-rate-
// distributed targets and per-rate-type limits via
// GroupConstraintCalculator. This walks up the group hierarchy from each
// master group, applying local reductions and guide-rate fractions, and
// returns the more restrictive of the higher-level distributed target and
// the master group's own GCONPROD constraint. For limits, the same
// traversal is reused once per non-active rate type (ORAT/WRAT/GRAT/LRAT/
// RESV) with an explicit cmode parameter; see the Phase 1 details below.
//
// Phase 2 (cap-and-redistribute): if any master group's Phase-1 target
// exceeds its slave's reported production potential, cap the target at
// that potential, switch the group to individual control, recompute the
// FIELD target reduction (so the capped group's rate is now subtracted),
// and recompute targets for the remaining uncapped groups via
// GroupConstraintCalculator. The shortfall is absorbed by the uncapped
// groups through the standard localReduction / FractionCalculator
// machinery. Finally all master groups are switched to individual
// control so the master completes its own time step assuming slave rates
// remain constant; the controls are restored from Schedule at the start
// of the next sync step. See capAndRedistributeProductionTargets_().
//
// Phase 3: send the resulting (target, cmode, per-rate-type limits)
// triples to each activated slave via MPI. Only rank 0 actually sends;
// other master ranks reach the send helpers but the underlying
// sendNum/Injection/Production functions in
// ReservoirCouplingMasterReportStep are gated by `if (comm.rank() == 0)`.
//
// Post-phase: aggregate any per-rank-partial group rates written by the
// redistribution (production reduction rates) via
// GroupState::communicate_rates() so the post-redistribution state is
// consistent across ranks. This is a no-op when the master has no local
// wells under FIELD, and matches the pattern at
// BlackoilWellModelGeneric.cpp:1326.
//
// Details on the target calculation (Phase 1):
// --------------------------------------------
// - If the group is a production group:
// * it is assumed that the action on exceeding the limit (GCONPROD item 7) is "RATE", other
// actions are not implemented.
@@ -107,8 +153,8 @@ RescoupConstraintsCalculator(
// injection rates, surface production rates, or voidage production rate as communicated at
// the beginning of the time step. See more details in RescoupSendSlaveGroupData.cpp.
//
// Details on the per-rate-type limit calculation:
// -----------------------------------------------
// Details on the per-rate-type limit calculation (Phase 1):
// ---------------------------------------------------------
// For each non-active rate type (ORAT, WRAT, GRAT, LRAT, RESV) that is not the active
// cmode, the same hierarchy-traversal logic described above is reused with an "explicit
// cmode" parameter. The difference is in the recursion stopping criterion:
@@ -124,36 +170,66 @@ void
RescoupConstraintsCalculator<Scalar, IndexTraits>::
calculateMasterGroupConstraintsAndSendToSlaves()
{
// NOTE: Since this object can only be constructed for a master process, we can be
// sure that if we are here, we are running as master
// NOTE: Since this object can only be constructed for a master process,
// we can be sure that if we are here, we are running as master.
//
// The body below must run on all ranks to ensure correct behavior:
// - updateGroupControlledWells() uses a collective comm_.sum()
// - GroupConstraintCalculator and updateGroupTargetReduction depend on
// GroupStateHelper which performs collective operations on the
// master's MPI communicator.
auto& rescoup_master = this->reservoir_coupling_master_;
const auto& comm = rescoup_master.getComm();
if (comm.rank() == 0) {
GroupConstraintCalculator calculator{
this->well_model_,
this->group_state_helper_
};
auto num_slaves = rescoup_master.numSlaves();
for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
if (rescoup_master.slaveIsActivated(slave_idx)) {
auto [injection_targets, production_constraints] =
this->calculateSlaveGroupConstraints_(slave_idx, calculator);
this->sendSlaveGroupConstraintsToSlave_(
rescoup_master, slave_idx, injection_targets, production_constraints
);
}
GroupConstraintCalculator calculator{
this->well_model_,
this->group_state_helper_
};
this->restoreMasterGroupControlsFromSchedule_();
this->excludeInactiveSlaveMasterGroupsFromDistribution_();
// Recompute GCW and reduction rates after the control changes above.
// The earlier updateAndCommunicateGroupData() in beginTimeStep() may
// have computed reductions with different controls. Inactive slaves'
// master groups are now on individual control with zero rates → excluded
// from guide rate fractions via GCW=0.
this->updateGCWAndTargetReductions_();
// Phase 1: compute initial targets for all slaves
const auto num_slaves = rescoup_master.numSlaves();
std::vector<std::vector<InjectionGroupTarget>> all_injection_targets(num_slaves);
std::vector<std::vector<ProductionGroupConstraints>> all_production_constraints(num_slaves);
for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
if (rescoup_master.slaveIsActivated(slave_idx)) {
auto [inj, prod] = this->calculateSlaveGroupConstraints_(slave_idx, calculator);
all_injection_targets[slave_idx] = std::move(inj);
all_production_constraints[slave_idx] = std::move(prod);
}
}
// Phase 2: cap production targets at slave potentials and redistribute
// shortfall to sibling master groups via the standard localReduction /
// FractionCalculator machinery.
this->capAndRedistributeProductionTargets_(calculator, all_production_constraints);
// Phase 3: send to slaves. The send functions are rank-0-only internally.
for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
if (rescoup_master.slaveIsActivated(slave_idx)) {
this->sendSlaveGroupConstraintsToSlave_(
rescoup_master, slave_idx,
all_injection_targets[slave_idx],
all_production_constraints[slave_idx]
);
}
}
// Aggregate any per-rank-partial group rates that the redistribution
// wrote (most importantly the FIELD-level production reduction rates set
// by updateGroupTargetReduction). For single-cell decks where the master
// has no wells the reduction has no per-rank partial component and this
// is a no-op, but it keeps the post-redistribution state correct on a
// master that owns wells under the FIELD hierarchy.
this->group_state_helper_.groupState().communicate_rates(comm);
}
// NOTE on reuse and future refactor:
// This method relies on GroupConstraintCalculator to compute group-level targets
// for reservoir coupling. Similar target logic exists in GroupStateHelper
// (checkGroupContraintsProd/getWellGroupTargetProducer and
// checkGroupContraintsInj/getWellGroupTargetInjector). The plan is to make
// GroupConstraintCalculator the general implementation and refactor the existing
// helpers to reuse it, removing duplication. We will address that in a follow-up
// PR to keep this PR scoped to reservoir coupling behavior.
template <class Scalar, class IndexTraits>
std::tuple<
std::vector<typename RescoupConstraintsCalculator<Scalar, IndexTraits>::InjectionGroupTarget>,
@@ -211,6 +287,222 @@ calculateSlaveGroupConstraints_(std::size_t slave_idx, GroupConstraintCalculator
return {injection_targets, production_constraints};
}
template <class Scalar, class IndexTraits>
void
RescoupConstraintsCalculator<Scalar, IndexTraits>::
capAndRedistributeProductionTargets_(
GroupConstraintCalculator<Scalar, IndexTraits>& calculator,
std::vector<std::vector<ProductionGroupConstraints>>& all_production_constraints
)
{
// Cap each master group's production target at its slave's reported
// potential, then redistribute the shortfall to sibling groups using
// the standard localReduction / FractionCalculator machinery.
//
// Implementation: switch capped groups to individual control so that
// updateGroupTargetReduction includes their rates as reduction for the
// parent group. Then recompute targets for uncapped groups — they get
// a larger share because the parent's available target increased.
//
// TODO: for master groups under individual control in schedule and GCONPROD
// item 8 set to "YES" (RESPOND_TO_PARENT = YES), the target calculation
// above does not compute a correct target because GCW=0 excludes the group from
// FractionCalculator::guideRateSum (see TODO in GroupStateHelper.cpp
// updateGroupControlledWells()). The slave-potential cap below is still
// correct for such groups once Phase 1 (see
// calculateMasterGroupConstraintsAndSendToSlaves() above) is fixed.
// This is deferred to a follow-up PR.
auto& rescoup_master = this->reservoir_coupling_master_;
const auto num_slaves = all_production_constraints.size();
// Step 1: identify groups where target > potential and cap them
std::set<std::string> capped_groups;
for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
const auto& master_groups = rescoup_master.getMasterGroupNamesForSlave(slave_idx);
for (auto& pc : all_production_constraints[slave_idx]) {
const auto& group_name = master_groups[pc.group_name_idx];
const auto& potentials = rescoup_master.getSlaveGroupPotentials(group_name);
const auto pot = this->potentialForProductionCmode_(
potentials, pc.cmode);
if (pot >= 0 && pc.target > pot) {
pc.target = pot;
capped_groups.insert(group_name);
// Switch to individual control so updateGroupTargetReduction()
// includes this group's rate as reduction for the parent.
this->group_state_helper_.groupState().production_control(
group_name, pc.cmode);
}
}
}
// If no groups are capped, no further action is needed.
if (capped_groups.empty()) return;
// Note: updateGroupTargetReduction() below calls sumWellSurfaceRates(), which
// for master groups reads from the MPI-received surface_rates in
// slave_group_production_data_ rather than from groupState().production_rates().
// No override is needed here: the cap condition (target > potential) implies
// the slave cannot meet the target, so its wells saturate at BHP and the
// group produces at its full potential. Both surface_rates and potentials
// are reported by the slave from the same reservoir-state snapshot, so
// surface_rates ≈ potential for any capped group, i.e. the reduction computed
// below already reflects the capped rate.
// Step 2: recompute GCW and reduction rates with capped groups now
// individually controlled.
this->updateGCWAndTargetReductions_();
// Step 3: recompute targets for uncapped groups using the updated reductions
for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
const auto& master_groups = rescoup_master.getMasterGroupNamesForSlave(slave_idx);
for (auto& pc : all_production_constraints[slave_idx]) {
const auto& group_name = master_groups[pc.group_name_idx];
if (capped_groups.count(group_name) > 0) {
this->deferred_logger_.debug(fmt::format(
"RC redistribution: {} capped at potential, target={:.4f}",
group_name, pc.target));
continue; // keep the capped target
}
const Scalar old_target = pc.target;
const Group& group = this->schedule_.getGroup(group_name, this->report_step_idx_);
auto constraints = calculator.groupProductionConstraints(group);
if (constraints.has_value()) {
pc.target = constraints->active_target;
}
this->deferred_logger_.debug(fmt::format(
"RC redistribution: {} old_target={:.4f} new_target={:.4f}",
group_name, old_target, pc.target));
}
}
// Step 4: switch ALL master groups to individual control with their
// allocated targets. I.e. the master completes its own time step, assuming
// that the production and injection rates of the slave groups remain constant
// over the time step. The controls are reset from schedule at the start of the
// next sync step (before Phase 1, see calculateMasterGroupConstraintsAndSendToSlaves()
// above) so guide rate distribution works correctly.
for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
const auto& master_groups = rescoup_master.getMasterGroupNamesForSlave(slave_idx);
for (auto& pc : all_production_constraints[slave_idx]) {
const auto& group_name = master_groups[pc.group_name_idx];
if (capped_groups.count(group_name) == 0) {
// Uncapped groups: switch to individual control too
this->group_state_helper_.groupState().production_control(
group_name, pc.cmode);
}
}
}
// Recompute GCW and reduction: all master groups are now individually
// controlled, so GCW=0 for all of them and FIELD's reduction sums every
// master group's slave-reported rate.
this->updateGCWAndTargetReductions_();
}
// Switch the master groups associated with currently-inactive slaves to
// individual control so they are excluded from guide-rate distribution.
// An inactive slave contributes zero rate and zero potential, so
// its master groups should not consume any share of the parent's target.
//
// TODO: A slave run can finish before the master. If the slave run finishes
// before the master run, the master run will continue without any production
// or injection from the slave (unless GECON item 8 is "YES"). The current
// `slaveIsActivated` flag transitions false→true on the activation
// handshake but never back to false, so finished slaves are still
// treated as contributing. When finished-slave detection lands (a
// separate PR with the corresponding MPI-protocol changes), the test
// below should also cover finished slaves so they too are excluded from
// guide-rate distribution.
template <class Scalar, class IndexTraits>
void
RescoupConstraintsCalculator<Scalar, IndexTraits>::
excludeInactiveSlaveMasterGroupsFromDistribution_()
{
auto& rescoup_master = this->reservoir_coupling_master_;
const auto num_slaves = rescoup_master.numSlaves();
// The choice of ORAT here is arbitrary, any non-FLD/NONE control mode works
// because GroupStateHelper::updateGroupControlledWells() assigns GCW=0 for
// any master group whose control is not FLD/NONE. ORAT is just a conventional
// rate mode used here as a marker.
const Group::ProductionCMode individual_cmode = Group::ProductionCMode::ORAT;
for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
if (!rescoup_master.slaveIsActivated(slave_idx)) {
const auto& master_groups = rescoup_master.getMasterGroupNamesForSlave(slave_idx);
for (const auto& group_name : master_groups) {
this->group_state_helper_.groupState().production_control(
group_name, individual_cmode);
}
}
}
}
template <class Scalar, class IndexTraits>
Scalar
RescoupConstraintsCalculator<Scalar, IndexTraits>::
potentialForProductionCmode_(
const ReservoirCoupling::Potentials<Scalar>& potentials,
Group::ProductionCMode cmode) const
{
// The potentials are stored in display units (e.g., SM3/DAY for METRIC)
// by GuideRateHandler, so we convert to SI (m3/s) for comparison with
// the target from GroupConstraintCalculator which is in SI.
using RcPhase = ReservoirCoupling::Phase;
const auto& units = this->schedule_.getUnits();
switch (cmode) {
case Group::ProductionCMode::ORAT:
return units.to_si(UnitSystem::measure::liquid_surface_rate,
potentials[RcPhase::Oil]);
case Group::ProductionCMode::GRAT:
return units.to_si(UnitSystem::measure::gas_surface_rate,
potentials[RcPhase::Gas]);
case Group::ProductionCMode::WRAT:
return units.to_si(UnitSystem::measure::liquid_surface_rate,
potentials[RcPhase::Water]);
case Group::ProductionCMode::LRAT:
return units.to_si(UnitSystem::measure::liquid_surface_rate,
potentials[RcPhase::Oil] + potentials[RcPhase::Water]);
default:
return -1; // No capping for RESV, FLD, NONE, etc.
}
}
// Restore master groups' control modes to their original GCONPROD values
// (from the Schedule) so they participate correctly in guide rate
// distribution during Phase 1 (see calculateMasterGroupConstraintsAndSendToSlaves()).
// The previous sync step's final step may have switched them to individual control.
//
// Reading from the Schedule (rather than caching the pre-sync mode) is
// intentional:
// - Master group control modes are treated as read-only outside the
// constraint calculation invoked from sendMasterGroupConstraintsToSlaves()
// at BlackoilWellModel_impl.hpp:555 (see also the master-group guard at
// the top of BlackoilWellModel::updateGroupControls).
// - Modes are only changed inside calculateMasterGroupConstraintsAndSendToSlaves(),
// which runs once per sync step. A sync step may be shorter than a
// report step, so multiple sync steps can occur within the same report
// step.
// - The Schedule's cmode is constant within a report step, so reading it
// here is equivalent to restoring to the pre-sync state for any sync
// step within the report step. At a report-step boundary, a new
// GCONPROD record is picked up automatically.
template <class Scalar, class IndexTraits>
void
RescoupConstraintsCalculator<Scalar, IndexTraits>::
restoreMasterGroupControlsFromSchedule_()
{
auto& rescoup_master = this->reservoir_coupling_master_;
const auto num_slaves = rescoup_master.numSlaves();
for (std::size_t slave_idx = 0; slave_idx < num_slaves; ++slave_idx) {
const auto& master_groups = rescoup_master.getMasterGroupNamesForSlave(slave_idx);
for (const auto& group_name : master_groups) {
const auto& group = this->schedule_.getGroup(group_name, this->report_step_idx_);
const auto original_cmode = group.productionProperties().cmode;
this->group_state_helper_.groupState().production_control(
group_name, original_cmode);
}
}
}
template <class Scalar, class IndexTraits>
void
RescoupConstraintsCalculator<Scalar, IndexTraits>::
@@ -234,6 +526,20 @@ sendSlaveGroupConstraintsToSlave_(
}
}
// Recompute GCW (Group Controlled Wells) and the FIELD-level target reduction
// after master-group control modes have been changed. GCW must be updated
// before the reduction since updateGroupTargetReduction() depends on it.
template <class Scalar, class IndexTraits>
void
RescoupConstraintsCalculator<Scalar, IndexTraits>::
updateGCWAndTargetReductions_()
{
this->group_state_helper_.updateGroupControlledWells(
/*is_production_group=*/true, /*dummy_injection_phase=*/Phase::OIL);
const Group& fieldGroup = this->schedule_.getGroup("FIELD", this->report_step_idx_);
this->group_state_helper_.updateGroupTargetReduction(fieldGroup, /*is_injector=*/false);
}
template class RescoupConstraintsCalculator<double, BlackOilDefaultFluidSystemIndices>;
#if FLOW_INSTANTIATE_FLOAT
@@ -33,20 +33,138 @@
namespace Opm {
/// @brief Computes per-master-group production targets and per-rate-type
/// limits for reservoir coupling, and sends them to the slaves.
///
/// @details Constructed once per sync step on the master process, drives
/// the full target-distribution flow when its single public entry point
/// `calculateMasterGroupConstraintsAndSendToSlaves()` is called. The
/// flow is structured as a pre-phase (control restore + inactive-slave
/// handling + GCW/reduction recompute) followed by three phases:
/// target computation (Phase 1), cap-and-redistribute (Phase 2), and
/// slave send (Phase 3). See the function-level comment on
/// `calculateMasterGroupConstraintsAndSendToSlaves()` in the
/// corresponding .cpp file for the per-phase narrative.
///
/// @tparam Scalar Floating-point type used for rates and targets.
/// @tparam IndexTraits Phase-index traits.
template<class Scalar, class IndexTraits>
class RescoupConstraintsCalculator {
public:
using InjectionGroupTarget = ReservoirCoupling::InjectionGroupTarget<Scalar>;
using ProductionGroupConstraints = ReservoirCoupling::ProductionGroupConstraints<Scalar>;
/// @brief Construct a calculator bound to the master's per-sync-step
/// state.
/// @param guide_rate_handler Source of guide rates and the deferred
/// logger used during the constraint calculation.
/// @param group_state_helper Provides access to the group state, the
/// schedule, the well state, and the reservoir-coupling master
/// facade. All intermediate state changes (control modes, GCW,
/// target reductions) are written through this helper.
RescoupConstraintsCalculator(
GuideRateHandler<Scalar, IndexTraits>& guide_rate_handler,
GroupStateHelper<Scalar, IndexTraits>& group_state_helper
);
/// @brief Run the full master-side target distribution and dispatch
/// the resulting constraints to each activated slave.
/// @details Runs on every rank of the master communicator; the
/// slave-facing MPI sends are rank-0-only inside the underlying
/// helpers. The driver writes group-state side effects (cmodes,
/// GCW, reductions) and finishes with a
/// `GroupState::communicate_rates(comm)` call so the
/// post-redistribution state is consistent across master ranks.
/// See the function-level comment on the implementation in the
/// corresponding .cpp file for the per-phase walkthrough and the
/// collective-call invariants.
void calculateMasterGroupConstraintsAndSendToSlaves();
private:
/// @brief Phase 1: compute initial guide-rate-distributed targets and
/// per-rate-type limits for one slave's master groups.
/// @details Invoked once per activated slave by
/// `calculateMasterGroupConstraintsAndSendToSlaves()`. Walks the
/// master groups attached to this slave and asks
/// `GroupConstraintCalculator` for the active target plus the four
/// non-active rate-type limits, returning them in the
/// `(injection_targets, production_constraints)` pair. Injection
/// targets are always emitted in surface-rate units (cmode `RATE`)
/// because the slave cannot evaluate derived modes (REIN, VREP,
/// RESV) on its own.
/// @param slave_idx Zero-based index of the activated slave.
/// @param calculator Group-constraint calculator bound to the current
/// group/well state. Stateful caches inside the calculator are
/// reused across slaves.
/// @return `(injection_targets, production_constraints)` for this
/// slave's master groups, ready for Phase 2 / Phase 3.
std::tuple<std::vector<InjectionGroupTarget>, std::vector<ProductionGroupConstraints>>
calculateSlaveGroupConstraints_(std::size_t slave_idx, GroupConstraintCalculator<Scalar, IndexTraits>& calculator) const;
/// @brief Phase 2: cap each capacity-limited master group's
/// production target at the slave's reported potential and
/// redistribute the surplus to sibling groups.
/// @details Switches capped groups to individual control so their
/// capped rate becomes a reduction on the parent group, recomputes
/// the FIELD-level reduction, then re-evaluates the uncapped
/// groups via `GroupConstraintCalculator`. Finally switches every
/// master group to individual control with its final allocated
/// target so the master completes its own time step assuming slave
/// rates remain constant. Injection targets are not capped here;
/// deferred to future PR.
/// @param calculator Group-constraint calculator (same instance as
/// used in Phase 1).
/// @param all_production_constraints In/out: production constraints
/// from Phase 1, modified in place.
void capAndRedistributeProductionTargets_(
GroupConstraintCalculator<Scalar, IndexTraits>& calculator,
std::vector<std::vector<ProductionGroupConstraints>>& all_production_constraints);
/// @brief Pre-phase: switch master groups that route to currently-
/// inactive slaves to individual control so they are excluded from
/// guide-rate distribution.
/// @details An inactive slave contributes zero rate and zero
/// potential and must not consume any share of the parent's
/// target. Currently handles the not-yet-activated case; the
/// slave-finished-before-master case deferred to a follow-up PR.
void excludeInactiveSlaveMasterGroupsFromDistribution_();
/// @brief Project a slave-reported potentials triple onto a single
/// value comparable to a target in the given control mode.
/// @param potentials Slave-reported potentials (oil, gas, water).
/// @param cmode Active production control mode the cap test is
/// expressed in (`ORAT`, `WRAT`, `GRAT`, `LRAT`, ...).
/// @return Potential value in SI units suitable for direct
/// comparison against the target, or a negative sentinel when the
/// cmode does not map to a single phase / linear combination
/// (e.g. `RESV`, `FLD`, `NONE`). In that case Phase 2 skips the
/// cap test for the group.
Scalar potentialForProductionCmode_(
const ReservoirCoupling::Potentials<Scalar>& potentials,
Group::ProductionCMode cmode) const;
/// @brief Pre-phase: restore each master group's `production_control`
/// cmode to its Schedule-defined GCONPROD value.
/// @details The previous sync step's Phase 2 finalize switched these
/// to individual control; this restore lets the upcoming
/// distribution see the user-specified cmode. Reads from the
/// Schedule directly (rather than caching the pre-sync state) so
/// new GCONPROD records at report-step boundaries are picked up
/// automatically.
void restoreMasterGroupControlsFromSchedule_();
/// @brief Phase 3: send the computed constraints for one slave to
/// that slave over MPI.
/// @details The underlying MPI sends in
/// `ReservoirCouplingMasterReportStep` are rank-0-only, so it is
/// safe to invoke this from every master rank inside the Phase-3
/// loop.
/// @param rescoup_master Reservoir-coupling master facade owning the
/// slave communicators.
/// @param slave_idx Zero-based index of the activated slave.
/// @param injection_targets Per-phase injection rate targets to send.
/// @param production_constraints Per-master-group production
/// constraint triples (target, cmode, per-rate-type limits) to
/// send.
void sendSlaveGroupConstraintsToSlave_(
const ReservoirCouplingMaster<Scalar>& rescoup_master,
std::size_t slave_idx,
@@ -54,8 +172,18 @@ private:
const std::vector<ProductionGroupConstraints>& production_constraints
) const;
/// @brief Recompute the Group-Controlled-Wells count and the
/// FIELD-level production target reduction.
/// @details Called from the pre-phase and from Phase 2 (twice: after
/// the cap loop and after the finalize-to-individual loop). The
/// ordering matters: `updateGroupControlledWells()` must run before
/// `updateGroupTargetReduction()` because the reduction depends on
/// GCW. Both delegated calls are MPI-collective on the master
/// communicator.
void updateGCWAndTargetReductions_();
GuideRateHandler<Scalar, IndexTraits>& guide_rate_handler_;
const GroupStateHelper<Scalar, IndexTraits>& group_state_helper_;
GroupStateHelper<Scalar, IndexTraits>& group_state_helper_;
const WellState<Scalar, IndexTraits>& well_state_;
const GroupState<Scalar>& group_state_;
const int report_step_idx_;