mirror of
https://github.com/OPM/opm-simulators.git
synced 2026-10-31 17:47:45 -05:00
Merge pull request #7025 from hakonhagland/fix_mg_gr_dist2
Reservoir coupling: Cap master-group targets at slave potential
This commit is contained in:
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.
|
||||
|
||||
@@ -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_;
|
||||
|
||||
Reference in new issue
Block a user