diff --git a/opm/simulators/flow/rescoup/ReservoirCouplingSlave.cpp b/opm/simulators/flow/rescoup/ReservoirCouplingSlave.cpp index 7a827f47007..bac0a48cd9b 100644 --- a/opm/simulators/flow/rescoup/ReservoirCouplingSlave.cpp +++ b/opm/simulators/flow/rescoup/ReservoirCouplingSlave.cpp @@ -460,6 +460,24 @@ markSlaveGroupsInSchedule(Schedule& schedule, const int report_step_idx) this->report_step_data_->markSlaveGroupsInSchedule(schedule, report_step_idx); } +template +void +ReservoirCouplingSlave:: +setWellsSolvedThisSyncStep(bool value) +{ + assert(this->report_step_data_); + this->report_step_data_->setWellsSolvedThisSyncStep(value); +} + +template +bool +ReservoirCouplingSlave:: +wellsSolvedThisSyncStep() const +{ + assert(this->report_step_data_); + return this->report_step_data_->wellsSolvedThisSyncStep(); +} + // ------------------ // Private methods // ------------------ diff --git a/opm/simulators/flow/rescoup/ReservoirCouplingSlave.hpp b/opm/simulators/flow/rescoup/ReservoirCouplingSlave.hpp index 970d9c728a7..b55e89851bd 100644 --- a/opm/simulators/flow/rescoup/ReservoirCouplingSlave.hpp +++ b/opm/simulators/flow/rescoup/ReservoirCouplingSlave.hpp @@ -161,6 +161,10 @@ class ReservoirCouplingSlave { /// @details Delegates to ReservoirCouplingSlaveReportStep. void markSlaveGroupsInSchedule(Schedule& schedule, int report_step_idx); + /// @brief Record whether the initial well solve for this sync step has run + /// @details Delegates to ReservoirCouplingSlaveReportStep + void setWellsSolvedThisSyncStep(bool value); + const std::string& slaveGroupIdxToGroupName(std::size_t group_idx) const { return this->slave_group_order_.at(group_idx); } @@ -187,10 +191,15 @@ class ReservoirCouplingSlave { /// MPI_Comm_disconnect() to complete - it is a collective operation. void receiveTerminateAndDisconnect(); + /// @brief True once the initial well solve for this sync step has run + /// @details Delegates to ReservoirCouplingSlaveReportStep + bool wellsSolvedThisSyncStep() const; + private: void checkGrupSlavGroupNames_(); std::pair getGrupSlavActivationDateAndCheckHistoryMatchingMode_() const; bool historyMatchingMode_() const { return this->history_matching_mode_; } + std::size_t numMasterGroups_() const { return this->slave_to_master_group_map_.size(); } //! \brief Receive the master's go-ahead after reporting our OK status. //! diff --git a/opm/simulators/flow/rescoup/ReservoirCouplingSlaveReportStep.hpp b/opm/simulators/flow/rescoup/ReservoirCouplingSlaveReportStep.hpp index 915e155108e..1a3fc78f753 100644 --- a/opm/simulators/flow/rescoup/ReservoirCouplingSlaveReportStep.hpp +++ b/opm/simulators/flow/rescoup/ReservoirCouplingSlaveReportStep.hpp @@ -267,6 +267,17 @@ class ReservoirCouplingSlaveReportStep { /// @param cmode Production control mode dictated by the master void setMasterProductionTarget(const std::string& gname, const Scalar target, const Group::ProductionCMode cmode); + /// @brief Record whether the initial well solve for this sync step has run + /// @param value False before the solve, true after it + void setWellsSolvedThisSyncStep(bool value) { wells_solved_this_sync_step_ = value; } + + /// @brief True once the initial well solve for this sync step has run + /// @details Before it, a well that opens in this report step still carries the + /// rates SingleWellState::update_producer_targets() derived from its WCONPROD + /// target, which must not be reported to the master as achieved production. + /// See RescoupSendSlaveGroupData::unsolvedNewWellProductionRates_(). + bool wellsSolvedThisSyncStep() const { return wells_solved_this_sync_step_; } + private: /// @brief Generic helper method for sending data to the master process via MPI @@ -301,6 +312,10 @@ class ReservoirCouplingSlaveReportStep { // Used to control reservoir coupling synchronization of summary data sent from // the slave to the master process. bool is_last_substep_of_sync_timestep_{false}; + // Flag to track whether the initial well solve of this sync timestep has run. + // Cleared before the pre-solve send of slave group data and set after the solve; + // see wellsSolvedThisSyncStep(). + bool wells_solved_this_sync_step_{false}; // Master-imposed targets and corresponding control modes, received from the master // process at the beginning of each sync timestep. Cleared and repopulated on every diff --git a/opm/simulators/wells/BlackoilWellModelRescoup.hpp b/opm/simulators/wells/BlackoilWellModelRescoup.hpp index d62b119f775..a84a6ef96f1 100644 --- a/opm/simulators/wells/BlackoilWellModelRescoup.hpp +++ b/opm/simulators/wells/BlackoilWellModelRescoup.hpp @@ -140,7 +140,9 @@ class BlackoilWellModelRescoup { /// Blocking receive of the per-master-group production targets and /// injection limits computed by the master. The received values are /// written into the slave's group state via the receiver helper. - /// Called from the slave's beginTimeStep first-substep handshake. + /// Called from the slave's beginTimeStep first-substep handshake, and + /// once per network iteration from maybeSendSlaveGroupFlowToMaster_(), + /// where the master sends injection targets only. void receiveGroupConstraintsFromMaster(); /// \brief Receive master-computed network-leaf node pressures and @@ -242,6 +244,22 @@ class BlackoilWellModelRescoup { /// activated slaves and gates the master's own iteration. bool masterNetworkHasMasterGroupLeavesForSlave_(std::size_t slave_idx) const; + /// \brief Master-side: recompute the master's group state from the slave + /// rates just received and send the resulting injection targets on. + /// + /// Called immediately after each non-final receiveSlaveGroupData() inside + /// the network iteration, so that a target derived from slave production + /// (GCONINJE REIN, SALE or VREP) keeps up with the production the slaves + /// report as the network iteration proceeds. Its slave-side counterpart + /// is the receiveGroupConstraintsFromMaster() in + /// BlackoilWellModel::maybeSendSlaveGroupFlowToMaster_(). + void refreshAndSendInjectionTargets_(); + + /// \brief Send injection targets to each activated slave, replacing the + /// ones the slaves are currently holding. Production constraints are + /// not resent. Only called by refreshAndSendInjectionTargets_(). + void sendMasterGroupInjectionTargetsToSlaves_(); + /// \brief Slave-side: true iff this slave's own deck put the named group /// into its own surface network as a fixed-pressure node. /// diff --git a/opm/simulators/wells/BlackoilWellModelRescoup_impl.hpp b/opm/simulators/wells/BlackoilWellModelRescoup_impl.hpp index 27379d0a55f..1efada79b18 100644 --- a/opm/simulators/wells/BlackoilWellModelRescoup_impl.hpp +++ b/opm/simulators/wells/BlackoilWellModelRescoup_impl.hpp @@ -109,6 +109,7 @@ maybeExchangeNetworkOuterIterationWithSlaves(bool more_network_update) if (!is_final) { // receive slaves' updated network_surface_rates for the next outer iteration. this->receiveSlaveGroupData(); + this->refreshAndSendInjectionTargets_(); } } } @@ -144,6 +145,7 @@ maybeExchangeNetworkSubIterationWithSlaves() } this->sendMasterGroupNodePressuresToSlaves(/*is_final=*/false); this->receiveSlaveGroupData(); + this->refreshAndSendInjectionTargets_(); } template @@ -413,6 +415,37 @@ masterNetworkHasMasterGroupLeavesForSlave_(std::size_t slave_idx) const return false; } +template +void +BlackoilWellModelRescoup:: +refreshAndSendInjectionTargets_() +{ + // Called right after a receiveSlaveGroupData() inside the network iteration. + // Fold the rates just received into the master's group state -- that is what + // recomputes the reinjection and voidage rates that a GCONINJE REIN, SALE + // or VREP target is built from -- and ship the resulting targets to the + // slaves, replacing the ones they are currently holding. Without this the + // targets stay at the values computed in beginTimeStep(), from slave + // production of the previous sync step. + const int report_step_idx = this->well_model_.simulator().episodeIndex(); + this->well_model_.updateAndCommunicateGroupData( + report_step_idx, /*update_wellgrouptarget=*/false); + this->sendMasterGroupInjectionTargetsToSlaves_(); +} + +template +void +BlackoilWellModelRescoup:: +sendMasterGroupInjectionTargetsToSlaves_() +{ + OPM_TIMEFUNCTION(); + RescoupConstraintsCalculator constraints_calculator{ + this->well_model_.guideRateHandler(), + this->groupStateHelper() + }; + constraints_calculator.recalculateInjectionTargetsAndSendToSlaves(); +} + template bool BlackoilWellModelRescoup:: diff --git a/opm/simulators/wells/BlackoilWellModel_impl.hpp b/opm/simulators/wells/BlackoilWellModel_impl.hpp index 1cfeaf3712e..e62cc423610 100644 --- a/opm/simulators/wells/BlackoilWellModel_impl.hpp +++ b/opm/simulators/wells/BlackoilWellModel_impl.hpp @@ -476,6 +476,10 @@ namespace Opm { #ifdef RESERVOIR_COUPLING_ENABLED if (this->isReservoirCouplingSlave()) { if (this->reservoirCouplingSlave().isFirstSubstepOfSyncTimestep()) { + // The wells have not been solved for this sync step yet, so a well that + // opens in this report step still carries its WCONPROD target as its rate. + // See RescoupSendSlaveGroupData::collectSlaveGroupSurfaceProductionRates_(). + this->reservoirCouplingSlave().setWellsSolvedThisSyncStep(false); this->rescoupHelper_.sendSlaveGroupDataToMaster(); this->rescoupHelper_.receiveGroupConstraintsFromMaster(); this->rescoupHelper_.receiveCoupledNetworkActiveStatus(); @@ -538,6 +542,8 @@ namespace Opm { #ifdef RESERVOIR_COUPLING_ENABLED if (slave_needs_well_solution) { // isReservoirCouplingSlave() + // The initial well solve above has run, so the well states now hold solved rates. + this->reservoirCouplingSlave().setWellsSolvedThisSyncStep(true); // Need to update group data based on new well solution. this->updateAndCommunicateGroupData(reportStepIdx, /*update_wellgrouptarget*/ false); this->rescoupHelper_.sendSlaveGroupDataToMaster(); @@ -2362,6 +2368,11 @@ namespace Opm { if (!is_final) { this->updateAndCommunicateGroupData(reportStepIdx, /*update_wellgrouptarget=*/false); this->rescoupHelper_.sendSlaveGroupDataToMaster(); + // The master turns the rates just sent into fresh injection targets + // for the groups it controls through a derived GCONINJE mode, and + // sends them straight back. See + // BlackoilWellModelRescoup::refreshAndSendInjectionTargets_(). + this->rescoupHelper_.receiveGroupConstraintsFromMaster(); return /*more_network_update=*/true; } return /*more_network_update=*/false; diff --git a/opm/simulators/wells/GroupStateHelper.cpp b/opm/simulators/wells/GroupStateHelper.cpp index ebab4871ea7..67c086fa5a2 100644 --- a/opm/simulators/wells/GroupStateHelper.cpp +++ b/opm/simulators/wells/GroupStateHelper.cpp @@ -1155,39 +1155,8 @@ GroupStateHelper::sumWellPhaseRates(bool res_rates, } for (const std::string& well_name : group.wells()) { - const auto well_index = this->wellState().index(well_name); - if (!well_index.has_value()) - continue; - - if (!this->wellState().wellIsOwned(well_index.value(), well_name)) // Only sum once - { - continue; - } - - const auto& well_ecl = this->schedule_.getWell(well_name, this->report_step_); - // only count producers or injectors - if ((well_ecl.isProducer() && is_injector) || (well_ecl.isInjector() && !is_injector)) - continue; - - const auto& ws = this->wellState().well(well_index.value()); - if (ws.status == Opm::Well::Status::SHUT) - continue; - - const Scalar factor = well_ecl.getEfficiencyFactor(network) - * this->wellState().well(well_index.value()).efficiency_scaling_factor; - if (res_rates) { - const auto& well_rates = ws.reservoir_rates; - if (is_injector) - rate += factor * well_rates[phase_pos]; - else - rate -= factor * well_rates[phase_pos]; - } else { - const auto& well_rates = ws.surface_rates; - if (is_injector) - rate += factor * well_rates[phase_pos]; - else - rate -= factor * well_rates[phase_pos]; - } + rate += this->wellRateContributionToGroup( + well_name, phase_pos, res_rates, is_injector, network); } return rate; } @@ -1615,6 +1584,41 @@ GroupStateHelper::updateWellRatesFromGroupTargetScale( } } +template +Scalar +GroupStateHelper::wellRateContributionToGroup(const std::string& well_name, + const int phase_pos, + const bool res_rates, + const bool is_injector, + const bool network) const +{ + const auto well_index = this->wellState().index(well_name); + if (!well_index.has_value()) + return 0.0; + + if (!this->wellState().wellIsOwned(well_index.value(), well_name)) // Only sum once + { + return 0.0; + } + + const auto& well_ecl = this->schedule_.getWell(well_name, this->report_step_); + // only count producers or injectors + if ((well_ecl.isProducer() && is_injector) || (well_ecl.isInjector() && !is_injector)) + return 0.0; + + const auto& ws = this->wellState().well(well_index.value()); + if (ws.status == Opm::Well::Status::SHUT) + return 0.0; + + const Scalar factor = well_ecl.getEfficiencyFactor(network) + * ws.efficiency_scaling_factor; + const auto& well_rates = res_rates ? ws.reservoir_rates : ws.surface_rates; + // Production rates are negative in the well state; a group rate sum is a positive + // magnitude for both directions. + return is_injector ? factor * well_rates[phase_pos] + : -factor * well_rates[phase_pos]; +} + template std::pair, Scalar> GroupStateHelper::worstOffendingWell(const Group& group, diff --git a/opm/simulators/wells/GroupStateHelper.hpp b/opm/simulators/wells/GroupStateHelper.hpp index 09e9f883692..8813ea42b54 100644 --- a/opm/simulators/wells/GroupStateHelper.hpp +++ b/opm/simulators/wells/GroupStateHelper.hpp @@ -517,6 +517,28 @@ class GroupStateHelper bool is_injector, WellState& well_state) const; + /// \brief The contribution of a single well to a group rate sum. + /// + /// Applies the filters, the efficiency factor and the sign convention that + /// sumWellPhaseRates() uses, so a caller that needs to add or remove one well's + /// share of such a sum stays consistent with it. Returns zero when the well does + /// not contribute at all: not present in this rank's well state, not owned by this + /// rank (each well is counted once), of the wrong type for the sum, or shut. + /// + /// Being owner-filtered, the result is rank-local, exactly like sumWellPhaseRates(); + /// a caller that compares it against an already-reduced quantity must reduce it too. + /// + /// \param well_name Name of the well + /// \param phase_pos Active phase index + /// \param res_rates Use reservoir rates instead of surface rates + /// \param is_injector Sum injectors rather than producers + /// \param network Use the network efficiency factors (GEFAC/WEFAC item 3) + Scalar wellRateContributionToGroup(const std::string& well_name, + const int phase_pos, + const bool res_rates, + const bool is_injector, + const bool network = false) const; + /// Returns the name of the worst offending well and its fraction (i.e. violated_phase / preferred_phase) std::pair, Scalar> worstOffendingWell(const Group& group, diff --git a/opm/simulators/wells/rescoup/RescoupConstraintsCalculator.cpp b/opm/simulators/wells/rescoup/RescoupConstraintsCalculator.cpp index 55f99e60fa3..fb40d99657b 100644 --- a/opm/simulators/wells/rescoup/RescoupConstraintsCalculator.cpp +++ b/opm/simulators/wells/rescoup/RescoupConstraintsCalculator.cpp @@ -260,6 +260,42 @@ calculateMasterGroupConstraintsAndSendToSlaves() this->group_state_helper_.groupState().communicate_rates(comm); } +// Recompute the injection targets against the slave rates the master holds now +// and ship them to the slaves, replacing the targets sent earlier in this sync +// step. See the declaration in the header for why this second send exists. +template +void +RescoupConstraintsCalculator:: +recalculateInjectionTargetsAndSendToSlaves() +{ + // As in calculateMasterGroupConstraintsAndSendToSlaves(), the body must run + // on every rank of the master communicator: GroupConstraintCalculator relies + // on GroupStateHelper, which performs collective operations. The MPI sends + // themselves are rank-0-only inside the send helpers. + auto& rescoup_master = this->reservoir_coupling_master_; + GroupConstraintCalculator calculator{ + this->well_model_, + this->group_state_helper_ + }; + const 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)) { + continue; + } + auto injection_targets = this->calculateSlaveGroupInjectionTargets_(slave_idx, calculator); + // An empty production-constraint list tells the slave that no production + // constraints follow; the ones it received earlier this sync step stay in + // force. + this->sendSlaveGroupConstraintsToSlave_( + rescoup_master, slave_idx, injection_targets, /*production_constraints=*/{} + ); + } +} + +// ---------------------------------------------------------------------- +// Private methods alphabetically for class RescoupConstraintsCalculator +// ---------------------------------------------------------------------- + template std::tuple< std::vector::InjectionGroupTarget>, @@ -268,34 +304,14 @@ std::tuple< RescoupConstraintsCalculator:: calculateSlaveGroupConstraints_(std::size_t slave_idx, GroupConstraintCalculator& calculator) const { - std::vector injection_targets; + std::vector injection_targets = + this->calculateSlaveGroupInjectionTargets_(slave_idx, calculator); std::vector production_constraints; auto& rescoup_master = this->reservoir_coupling_master_; - static const std::array phases = { - ReservoirCoupling::Phase::Water, ReservoirCoupling::Phase::Oil, ReservoirCoupling::Phase::Gas - }; const auto& master_groups = rescoup_master.getMasterGroupNamesForSlave(slave_idx); for (std::size_t group_idx = 0; group_idx < master_groups.size(); ++group_idx) { const auto& group_name = master_groups[group_idx]; const Group& group = this->schedule_.getGroup(group_name, this->report_step_idx_); - if (group.isInjectionGroup()) { - for (ReservoirCoupling::Phase phase : phases) { - auto target_info = calculator.groupInjectionTarget(group, phase); - if (target_info.has_value()) { - // Always send injection targets as RATE. The numeric value is - // already a surface rate for all modes (RATE, REIN, RESV, VREP), - // and the slave cannot evaluate derived modes (REIN, VREP, RESV) - // because it lacks the master's schedule data (reinj_group, - // voidage_group, GCONSUMP, resv_coeff, etc.). - injection_targets.push_back( - InjectionGroupTarget{ - group_idx, target_info->constraint, - Group::InjectionCMode::RATE, phase - } - ); - } - } - } if (group.isProductionGroup()) { auto constraints = calculator.groupProductionConstraints(group); if (constraints.has_value()) { @@ -317,6 +333,43 @@ calculateSlaveGroupConstraints_(std::size_t slave_idx, GroupConstraintCalculator return {injection_targets, production_constraints}; } +template +std::vector::InjectionGroupTarget> +RescoupConstraintsCalculator:: +calculateSlaveGroupInjectionTargets_(std::size_t slave_idx, GroupConstraintCalculator& calculator) const +{ + std::vector injection_targets; + auto& rescoup_master = this->reservoir_coupling_master_; + static const std::array phases = { + ReservoirCoupling::Phase::Water, ReservoirCoupling::Phase::Oil, ReservoirCoupling::Phase::Gas + }; + const auto& master_groups = rescoup_master.getMasterGroupNamesForSlave(slave_idx); + for (std::size_t group_idx = 0; group_idx < master_groups.size(); ++group_idx) { + const auto& group_name = master_groups[group_idx]; + const Group& group = this->schedule_.getGroup(group_name, this->report_step_idx_); + if (!group.isInjectionGroup()) { + continue; + } + for (ReservoirCoupling::Phase phase : phases) { + auto target_info = calculator.groupInjectionTarget(group, phase); + if (target_info.has_value()) { + // Always send injection targets as RATE. The numeric value is + // already a surface rate for all modes (RATE, REIN, RESV, VREP), + // and the slave cannot evaluate derived modes (REIN, VREP, RESV) + // because it lacks the master's schedule data (reinj_group, + // voidage_group, GCONSUMP, resv_coeff, etc.). + injection_targets.push_back( + InjectionGroupTarget{ + group_idx, target_info->constraint, + Group::InjectionCMode::RATE, phase + } + ); + } + } + } + return injection_targets; +} + template void RescoupConstraintsCalculator:: diff --git a/opm/simulators/wells/rescoup/RescoupConstraintsCalculator.hpp b/opm/simulators/wells/rescoup/RescoupConstraintsCalculator.hpp index 043ef486464..ef46cabeca3 100644 --- a/opm/simulators/wells/rescoup/RescoupConstraintsCalculator.hpp +++ b/opm/simulators/wells/rescoup/RescoupConstraintsCalculator.hpp @@ -79,6 +79,39 @@ class RescoupConstraintsCalculator { /// corresponding .cpp file for the per-phase walkthrough and the /// collective-call invariants. void calculateMasterGroupConstraintsAndSendToSlaves(); + + /// @brief Recompute only the injection targets and send them to each + /// activated slave, replacing the ones sent earlier in this sync step. + /// @details The injection targets computed by + /// `calculateMasterGroupConstraintsAndSendToSlaves()` are derived from + /// the slave production rates the master holds at that moment, which + /// are the rates the slaves reported *before* solving their wells for + /// this sync step. The `GCONINJE` modes that turn production into an + /// injection target are `REIN` (a fraction of the reinjection rate), + /// `SALE` (the reinjection rate less the `GCONSALE` sales target) and + /// `VREP` (the voidage of what was produced), so under any of those a + /// step in which slave production changes gets an injection target for + /// the previous step's production. This entry point is called from each + /// master network iteration, once the slaves' latest rates have arrived, + /// and ships targets recomputed from them. Production constraints are + /// deliberately left untouched: the slaves have already solved their + /// wells against them, and they are what produced the rates this + /// recomputation is based on. + /// + /// The recomputation is unconditional: it runs for every master group of + /// every activated slave, whatever mode each one is under. A `RATE` + /// target is a constant from the deck and a `RESV` target moves only with + /// the other phases' reservoir injection, so for those the recomputation + /// produces the value the slave already holds and the send is redundant. + /// TODO: skip the groups whose target cannot have changed. The test is + /// not simply the group's own `GCONINJE` record: `groupInjectionTarget()` + /// walks up the hierarchy, so it is the mode of the controlling ancestor + /// that decides, and that mode changes during a run -- a group on `REIN` + /// switches to `RATE` as soon as its target reaches the `GCONINJE` + /// maximum. The filter would therefore have to be evaluated per refresh + /// over the whole chain. + void recalculateInjectionTargetsAndSendToSlaves(); + private: /// @brief Phase 1: compute initial guide-rate-distributed targets and /// per-rate-type limits for one slave's master groups. @@ -100,6 +133,18 @@ class RescoupConstraintsCalculator { std::tuple, std::vector> calculateSlaveGroupConstraints_(std::size_t slave_idx, GroupConstraintCalculator& calculator) const; + /// @brief Compute the per-phase injection targets for one slave's + /// master groups. + /// @details The injection half of `calculateSlaveGroupConstraints_()`, + /// split out so that `recalculateInjectionTargetsAndSendToSlaves()` + /// can reuse it without recomputing the production constraints. + /// @param slave_idx Zero-based index of the activated slave. + /// @param calculator Group-constraint calculator bound to the current + /// group/well state. + /// @return One entry per (master group, phase) that has a target. + std::vector + calculateSlaveGroupInjectionTargets_(std::size_t slave_idx, GroupConstraintCalculator& 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. @@ -142,16 +187,6 @@ class RescoupConstraintsCalculator { const ReservoirCoupling::Potentials& 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 diff --git a/opm/simulators/wells/rescoup/RescoupSendSlaveGroupData.cpp b/opm/simulators/wells/rescoup/RescoupSendSlaveGroupData.cpp index 19432e83395..496577153ec 100644 --- a/opm/simulators/wells/rescoup/RescoupSendSlaveGroupData.cpp +++ b/opm/simulators/wells/rescoup/RescoupSendSlaveGroupData.cpp @@ -180,6 +180,13 @@ collectSlaveGroupSurfaceProductionRates_(std::size_t group_idx) const auto& rescoup_slave = this->reservoir_coupling_slave_; const auto& group_name = rescoup_slave.slaveGroupIdxToGroupName(group_idx); GuideRate::RateVector production_rates = this->groupStateHelper_.getProductionGroupRateVector(group_name); + // Correct the sum for wells that have not been solved yet; see + // unsolvedNewWellProductionRates_(). The group state rates read above have already + // been reduced across the ranks, so the rank-local correction is reduced to match. + const auto unsolved = this->unsolvedNewWellProductionRates_(group_name, /*network=*/false); + production_rates.oil_rat -= this->comm().sum(unsolved[ReservoirCoupling::Phase::Oil]); + production_rates.gas_rat -= this->comm().sum(unsolved[ReservoirCoupling::Phase::Gas]); + production_rates.wat_rat -= this->comm().sum(unsolved[ReservoirCoupling::Phase::Water]); // NOTE: GuideRate::RateVector is a vector of doubles, so we need to convert it to Scalar // TODO: Fix GuideRate::RateVector to be a vector of Scalars instead of doubles return ProductionRates{production_rates}; @@ -218,6 +225,15 @@ collectSlaveGroupNetworkSurfaceProductionRates_(std::size_t group_idx) const /*is_injector=*/false, /*network=*/true); } + // Correct the sums for wells that have not been solved yet; see + // unsolvedNewWellProductionRates_(). The master uses these rates as the flow of + // its network's leaf nodes. The correction is rank-local, like the sums above, so + // it is applied before the reduction. + const auto unsolved = this->unsolvedNewWellProductionRates_(group_name, /*network=*/true); + oil_rate -= unsolved[ReservoirCoupling::Phase::Oil]; + gas_rate -= unsolved[ReservoirCoupling::Phase::Gas]; + water_rate -= unsolved[ReservoirCoupling::Phase::Water]; + // Sum across all MPI ranks since wells in a group may be owned by different ranks oil_rate = this->comm().sum(oil_rate); gas_rate = this->comm().sum(gas_rate); @@ -382,6 +398,75 @@ sendSlaveGroupInjectionDataToMaster_() const rescoup_slave.sendInjectionDataToMaster(injection_data); } +// A well that opens in this report step is put on its WCONPROD target by +// SingleWellState::update_producer_targets() before the sync step's well solve runs, +// so on the send that precedes that solve the well state holds a target, not a solved +// rate. Shipping it to the master would report a target as achieved +// production: the master folds the slave group rates into the field's +// reinjection base, so a multi-million sm3/day gas target arriving that way +// inflates the reinjection target and pushes the injectors onto their own rate +// limits. Leave such a well out of the sums until it has been solved; from the +// post-solve send onwards its real rates are included. +// +// The correction is built with GroupStateHelper::wellRateContributionToGroup(), the same +// per-well step the sums themselves use, so it removes exactly what they added: only +// producers, only wells this rank owns and that are not shut, with the same efficiency +// factor and sign. It is therefore rank-local, and each call site reduces it to match +// whatever it is being subtracted from. +template +typename RescoupSendSlaveGroupData::ProductionRates +RescoupSendSlaveGroupData:: +unsolvedNewWellProductionRates_(const std::string& group_name, bool network) const +{ + ProductionRates rates; + if (this->reservoir_coupling_slave_.wellsSolvedThisSyncStep()) { + return rates; + } + const auto report_step = this->groupStateHelper_.reportStepIdx(); + const auto& schedule = this->groupStateHelper_.schedule(); + const auto& group = schedule.getGroup(group_name, report_step); + // NOTE: the live schedule events are read on purpose. NEW_WELL is cleared after the + // first time step of the report step, so the correction applies only to the sync step + // in which the well opens; from the next one its rates come from a solve and belong + // in the sum. Do not switch this to BlackoilWellModel::reportStepStartEvents(), + // which keeps the flag for the whole report step: wellsSolvedThisSyncStep() is + // cleared before the pre-solve send of *every* sync step, so a valid converged rate + // would then be subtracted on each of them. + const auto& events = schedule[report_step].wellgroup_events(); + // Mirror sumWellPhaseRates(): a slave group may hold no wells of its own, so + // descend into child groups, applying each child's efficiency factor. + for (const auto& child_name : group.groups()) { + const auto& child = schedule.getGroup(child_name, report_step); + const auto child_rates = this->unsolvedNewWellProductionRates_(child_name, network); + const auto gefac = child.getGroupEfficiencyFactor(network); + for (const auto phase : {ReservoirCoupling::Phase::Oil, + ReservoirCoupling::Phase::Gas, + ReservoirCoupling::Phase::Water}) { + rates[phase] += gefac * child_rates[phase]; + } + } + const auto& pu = this->phase_usage_; + for (const auto& wname : group.wells()) { + if (!events.hasEvent(wname, ScheduleEvents::NEW_WELL)) { + continue; + } + // Ask for the well's share of the sum we are correcting, so the filters + // (producers only, present, owned by this rank, not shut), the efficiency + // factor and the sign are the ones that sum actually used. + auto contribution = [&](const auto canonical_phase_idx) { + return pu.phaseIsActive(canonical_phase_idx) + ? this->groupStateHelper_.wellRateContributionToGroup( + wname, pu.canonicalToActivePhaseIdx(canonical_phase_idx), + /*res_rates=*/false, /*is_injector=*/false, network) + : Scalar{0}; + }; + rates[ReservoirCoupling::Phase::Oil] += contribution(IndexTraits::oilPhaseIdx); + rates[ReservoirCoupling::Phase::Gas] += contribution(IndexTraits::gasPhaseIdx); + rates[ReservoirCoupling::Phase::Water] += contribution(IndexTraits::waterPhaseIdx); + } + return rates; +} + template class RescoupSendSlaveGroupData; diff --git a/opm/simulators/wells/rescoup/RescoupSendSlaveGroupData.hpp b/opm/simulators/wells/rescoup/RescoupSendSlaveGroupData.hpp index ba4ee7f7011..23a14a73181 100644 --- a/opm/simulators/wells/rescoup/RescoupSendSlaveGroupData.hpp +++ b/opm/simulators/wells/rescoup/RescoupSendSlaveGroupData.hpp @@ -144,6 +144,20 @@ class RescoupSendSlaveGroupData { /// via MPI communication through the ReservoirCouplingSlave. void sendSlaveGroupInjectionDataToMaster_() const; + /// @brief The part of a group's surface production rates that comes from + /// wells which open in this report step but have not been solved yet + /// @param group_name Name of the slave group; the walk descends into child + /// groups, so a slave group that holds no wells of its own is handled too + /// @param network True to use the network efficiency factors (GEFAC/WEFAC + /// item 3), matching the `network` argument of the rate sum being + /// corrected + /// @return Rates to subtract, all zero once the wells have been solved + /// @note Before this sync step's well solve, such a well contributes the + /// rates updateWellStateWithTarget() derived from its WCONPROD + /// target rather than rates from a solve. Reporting those to the + /// master states a target as achieved production; see the call sites. + ProductionRates unsolvedNewWellProductionRates_(const std::string& group_name, bool network) const; + /// Reference to the GroupStateHelper for group state management const GroupStateHelperType& groupStateHelper_;