Merge pull request #6835 from hakonhagland/test_autochoke_gtgt

Add tests and fix autochoke group target rate
This commit is contained in:
Tor Harald Sandve authored and GitHub committed 2026-02-19 16:01:01 +01:00
commit fa509b2563
7 files changed
+644 -89

No files matched your search

+1
View File
@@ -679,6 +679,7 @@ list (APPEND TEST_DATA_FILES
tests/GCONSUMP.DATA
tests/GCONSUMP_COMPLEX.DATA
tests/GROUP_HIGHER_CONSTRAINTS.DATA
tests/GROUP_HIGHER_CONSTRAINTS_NETWORK.DATA
tests/GLIFT1.DATA
tests/RC-01_MAST_PRED.DATA
tests/include/flowl_b_vfp.ecl
+4 -1
View File
@@ -168,7 +168,10 @@ guideRate(const std::string& name,
const bool always_use_potentials)
{
if (schedule_.hasWell(name, report_step_)) {
return this->groupStateHelper().getGuideRate(name, target_);
if (guide_rate_->has(name) || guide_rate_->hasPotentials(name)) {
return guide_rate_->get(name, target_, this->groupStateHelper().getWellRateVector(name));
}
return 0.0;
} else {
if (groupControlledWells(name, always_included_child) > 0) {
if (is_producer_ && guide_rate_->has(name) && !always_use_potentials) {
+46 -65
View File
@@ -189,7 +189,7 @@ GroupStateHelper<Scalar, IndexTraits>::checkGroupConstraintsInj(const std::strin
const Scalar orig_target = tcalc.groupTarget();
const Scalar current_rate_available = tcalc.calcModeRateFromRates(rates);
const std::size_t local_reduction_level = this->getLocalReductionLevel_(
const std::size_t local_reduction_level = this->getLocalReductionLevel(
chain, /*is_production_group=*/false, injection_phase);
const Scalar target = this->applyReductionsAndFractions_(chain,
@@ -330,7 +330,7 @@ GroupStateHelper<Scalar, IndexTraits>::checkGroupConstraintsProd(const std::stri
return std::make_pair(current_well_rate_available > group_target_rate_available, scale);
}
const std::size_t local_reduction_level = this->getLocalReductionLevel_(
const std::size_t local_reduction_level = this->getLocalReductionLevel(
chain, /*is_production_group=*/true, /*injection_phase=*/Phase::OIL);
const Scalar target = this->applyReductionsAndFractions_(chain,
@@ -353,62 +353,6 @@ GroupStateHelper<Scalar, IndexTraits>::checkGroupConstraintsProd(const std::stri
return std::make_pair(current_rate_available > target_rate_available, scale);
}
template <typename Scalar, typename IndexTraits>
Scalar
GroupStateHelper<Scalar, IndexTraits>::getGuideRate(const std::string& name,
const GuideRateModel::Target target) const
{
if (this->schedule_.hasWell(name, this->report_step_)) {
if (this->guide_rate_.has(name) || this->guide_rate_.hasPotentials(name)) {
return this->guide_rate_.get(name, target, this->getWellRateVector(name));
} else {
return 0.0;
}
}
if (this->guide_rate_.has(name)) {
return this->guide_rate_.get(name, target, this->getProductionGroupRateVector(name));
}
Scalar total_guide_rate = 0.0;
const Group& group = this->schedule_.getGroup(name, this->report_step_);
for (const std::string& group_name : group.groups()) {
const Group::ProductionCMode& current_group_control
= this->groupState().production_control(group_name);
if (current_group_control == Group::ProductionCMode::FLD
|| current_group_control == Group::ProductionCMode::NONE) {
// accumulate from sub wells/groups
total_guide_rate += this->getGuideRate(group_name, target);
}
}
for (const std::string& well_name : group.wells()) {
const auto& well_tmp = this->schedule_.getWell(well_name, this->report_step_);
if (well_tmp.isInjector())
continue;
const auto well_index = this->wellState().index(well_name);
if (!well_index.has_value())
continue;
const auto& ws = this->wellState().well(well_index.value());
if (ws.status == Well::Status::SHUT)
continue;
if (!this->wellState().isProductionGrup(well_name))
continue;
// Only count wells under group control or the ru
if (!this->wellState().isProductionGrup(well_name))
continue;
total_guide_rate += this->getGuideRate(well_name, target);
}
return total_guide_rate;
}
template <typename Scalar, typename IndexTraits>
Scalar
GroupStateHelper<Scalar, IndexTraits>::
@@ -671,7 +615,7 @@ GroupStateHelper<Scalar, IndexTraits>::getWellGroupTargetInjector(const std::str
const Scalar current_rate_available = tcalc.calcModeRateFromRates(rates);
const auto chain = this->groupChainTopBot(name, group.name());
const std::size_t local_reduction_level = this->getLocalReductionLevel_(
const std::size_t local_reduction_level = this->getLocalReductionLevel(
chain, /*is_production_group=*/false, injection_phase);
// Add-back is only performed when the well is under individual control (not GRUP)
@@ -767,7 +711,7 @@ GroupStateHelper<Scalar, IndexTraits>::getWellGroupTargetProducer(const std::str
const Scalar current_rate_available = -tcalc.calcModeRateFromRates(rates);
const auto chain = this->groupChainTopBot(name, group.name());
const std::size_t local_reduction_level = this->getLocalReductionLevel_(
const std::size_t local_reduction_level = this->getLocalReductionLevel(
chain, /*is_production_group=*/true, /*injection_phase=*/Phase::OIL);
// Add-back is only performed when the well is under individual control (not GRUP)
@@ -1596,7 +1540,7 @@ GroupStateHelper<Scalar, IndexTraits>::getGuideRateVector_(const std::vector<Sca
template <typename Scalar, typename IndexTraits>
std::size_t
GroupStateHelper<Scalar, IndexTraits>::getLocalReductionLevel_(const std::vector<std::string>& chain,
GroupStateHelper<Scalar, IndexTraits>::getLocalReductionLevel(const std::vector<std::string>& chain,
const bool is_production_group,
const Phase injection_phase) const
{
@@ -1684,21 +1628,58 @@ GroupStateHelper<Scalar, IndexTraits>::isAutoChokeGroupUnderperforming_(const Gr
const auto& control_group_name = this->controlGroup_(group);
const auto& control_group = this->schedule_.getGroup(control_group_name, this->report_step_);
// Calculate target rate for this auto choke group
// Calculate target rate for this auto choke group using a guide rate ratio
// formula. This is an approximation that ignores intermediate reductions.
// A more accurate calculation would use WellGroupControls::getAutoChokeGroupProductionTargetRate(),
// but that requires reduction rates not yet available during
// updateGroupControlledWells() (circular dependency). The efficiency factor is however accounted for
// below meaning that the current target rate will be correct for the special case where the control
// group has no reductions and the auto choke group is a direct child of the control group.
// TODO: The issue with missing handling of reductions should be fixed by refactoring the auto choke
// target calculation below to use the more accurate
// WellGroupControls::getAutoChokeGroupProductionTargetRate() function.
const auto num_phases = this->numPhases();
std::vector<Scalar> resv_coeff(num_phases, 1.0);
GroupStateHelpers::TargetCalculator<Scalar, IndexTraits> tcalc{*this,
resv_coeff,
control_group};
const auto& control_group_target = tcalc.groupTarget();
const auto& control_group_guide_rate
= this->getGuideRate(control_group_name, tcalc.guideTargetMode());
// Sum guide rates of the control group's FLD children. The control group's
// own guide rate (from GCONPROD) is NOT used here — it governs the control
// group's share of its parent's target, not how its own target is distributed.
Scalar control_group_guide_rate = 0.0;
for (const std::string& child : control_group.groups()) {
const auto child_ctrl = this->groupState().production_control(child);
if (child_ctrl == Group::ProductionCMode::FLD
|| child_ctrl == Group::ProductionCMode::NONE) {
if (this->guide_rate_.has(child)) {
control_group_guide_rate += this->guide_rate_.get(
child, tcalc.guideTargetMode(),
this->getProductionGroupRateVector(child));
}
}
}
if (control_group_guide_rate <= 0) {
return false;
}
const Scalar target_rate = control_group_target * group_guide_rate / control_group_guide_rate;
// Accumulate group efficiency factors from the autochoke group up to (but not
// including) the control group. This matches what
// getAutoChokeGroupProductionTargetRate() does during its recursion.
Scalar accumulated_efficiency = 1.0;
{
std::string current = group.name();
while (current != control_group_name) {
const auto& g = this->schedule_.getGroup(current, this->report_step_);
accumulated_efficiency *= g.getGroupEfficiencyFactor();
current = g.parent();
}
}
const Scalar target_rate = control_group_target * group_guide_rate
/ control_group_guide_rate / accumulated_efficiency;
// Calculate current rate for this group
std::vector<Scalar> rates(num_phases, 0.0);
+14 -15
View File
@@ -234,8 +234,6 @@ public:
std::vector<Scalar> getGroupRatesAvailableForHigherLevelControl(const Group& group, const bool is_injector) const;
Scalar getGuideRate(const std::string& name, const GuideRateModel::Target target) const;
Scalar getInjectionGroupTarget(const Group& group,
const Phase& injection_phase,
const std::vector<Scalar>& resv_coeff) const;
@@ -245,6 +243,20 @@ public:
/// @return The corresponding GuideRateModel::Target for the injection phase
GuideRateModel::Target getInjectionGuideTargetMode(Phase injection_phase) const;
//! \brief Find the local reduction level in a group chain.
//!
//! The local reduction level is the deepest level in the chain (starting from level 1)
//! where a group has both a guide rate and group-controlled wells (GCW > 0).
//! This level determines where reductions are applied and add-back occurs.
//!
//! \param chain Group chain from control group (top) to bottom group
//! \param is_production_group True for production, false for injection
//! \param injection_phase Phase for injection groups (ignored for production)
//! \return The local reduction level (0 if no intermediate group qualifies)
std::size_t getLocalReductionLevel(const std::vector<std::string>& chain,
bool is_production_group,
Phase injection_phase) const;
Scalar getProductionGroupTarget(const Group& group) const;
/// @brief Get the guide rate target mode for a production group
@@ -525,19 +537,6 @@ private:
GuideRate::RateVector getGuideRateVector_(const std::vector<Scalar>& rates) const;
//! \brief Find the local reduction level in a group chain.
//!
//! The local reduction level is the deepest level in the chain (starting from level 1)
//! where a group has both a guide rate and group-controlled wells (GCW > 0).
//! This level determines where reductions are applied and add-back occurs.
//!
//! \param chain Group chain from control group (top) to bottom group
//! \param is_production_group True for production, false for injection
//! \param injection_phase Phase for injection groups (ignored for production)
//! \return The local reduction level (0 if no intermediate group qualifies)
std::size_t getLocalReductionLevel_(const std::vector<std::string>& chain,
bool is_production_group,
Phase injection_phase) const;
#ifdef RESERVOIR_COUPLING_ENABLED
Scalar getReservoirCouplingMasterGroupRate_(const Group& group,
+10 -4
View File
@@ -392,13 +392,19 @@ getAutoChokeGroupProductionTargetRate(const Group& bottom_group,
const auto chain = groupStateHelper.groupChainTopBot(bottom_group.name(), group.name());
// Because 'name' is the last of the elements, and not an ancestor, we subtract one below.
const std::size_t num_ancestors = chain.size() - 1;
const std::size_t local_reduction_level = groupStateHelper.getLocalReductionLevel(
chain, /*is_production_group=*/true, Phase::OIL);
Scalar target = orig_target;
for (std::size_t ii = 0; ii < num_ancestors; ++ii) {
if ((ii == 0) || guideRate->has(chain[ii])) {
// Apply local reductions only at the control level
// (top) and for levels where we have a specified
// group guide rate.
target -= localReduction(chain[ii]);
// Apply local reductions only at the control level (top) and for
// intermediate levels where we have both a guide rate and
// group-controlled wells (local_reduction_level >= ii). Without
// this gating, reductions from a group with GCW=0 would be
// double-counted.
if (local_reduction_level >= ii) {
target -= localReduction(chain[ii]);
}
}
target *= localFraction(chain[ii+1]);
}
+278
View File
@@ -0,0 +1,278 @@
-- This reservoir simulation deck is made available under the Open Database
-- License: http://opendatacommons.org/licenses/odbl/1.0/. Any rights in
-- individual contents of the database are licensed under the Database Contents
-- License: http://opendatacommons.org/licenses/dbcl/1.0/
-- Extended network variant of GROUP_HIGHER_CONSTRAINTS.DATA
--
-- Adds NETWORK, BRANPROP and NODEPROP keywords to enable the extended network
-- model. MANI is designated as a sub-sea manifold (autochoke group) via
-- NODEPROP, which sets as_choke()=true on the MANI group.
--
-- Group hierarchy:
--
-- FIELD (ORAT = 10000, terminal pressure = 200 barsa)
-- / \
-- / \
-- PLAT (GEFAC=0.9) PLAT-2 (GEFAC=0.85)
-- / \ |
-- / \ WELL-C
-- MANI (0.8,SSM) MANI-2 (0.75)
-- / \ |
-- WELL-A WELL-A2 WELL-B
--
-- Network branches (all VFP=9999, no pressure drop):
-- MANI -> PLAT (autochoke branch)
-- MANI-2 -> PLAT
-- PLAT -> FIELD
-- PLAT-2 -> FIELD
--
-- NODEPROP effects on MANI (as_choke=YES):
-- - group.as_choke() = true
-- - Wells switched to THP control (ProducerCMode::THP)
-- - Wells' guide rate availability disabled
--
RUNSPEC
TITLE
EFFICIENCY_FACTOR_NETWORK_TEST
DIMENS
5 5 3 /
OIL
WATER
GAS
DISGAS
METRIC
START
1 'JAN' 2020 /
WELLDIMS
-- max wells, max cons/well, max groups, max wells/group
6 5 6 4 /
TABDIMS
/
EQLDIMS
/
UNIFIN
UNIFOUT
NETWORK
-- max_nodes max_branches
5 4 /
-- =============================================================================
GRID
-- =============================================================================
DXV
5*100.0 /
DYV
5*100.0 /
DZV
3*10.0 /
TOPS
25*2000 /
PORO
75*0.25 /
PERMX
75*500 /
COPY
PERMX PERMY /
PERMX PERMZ /
/
MULTIPLY
PERMZ 0.1 /
/
-- =============================================================================
PROPS
-- =============================================================================
PVTW
-- ref.pres Bw Cw Vw Vw.visc
250.0 1.02 4.0E-05 0.5 0.0 /
ROCK
-- ref.pres compressibility
250.0 4.0E-05 /
DENSITY
-- oil water gas
850 1014 1.0 /
PVDO
-- P Bo Vo
100.0 1.05 2.0
250.0 1.02 2.5
400.0 1.00 3.0 /
PVDG
-- P Bg Vg
100.0 0.010 0.015
250.0 0.005 0.020
400.0 0.003 0.025 /
SWOF
-- Sw Krw Kro Pcow
0.2 0.0 1.0 0.0
0.3 0.05 0.6 0.0
0.5 0.15 0.3 0.0
0.7 0.35 0.1 0.0
0.8 0.5 0.0 0.0 /
SGOF
-- Sg Krg Kro Pcog
0.0 0.0 1.0 0.0
0.1 0.02 0.6 0.0
0.3 0.10 0.3 0.0
0.5 0.25 0.1 0.0
0.7 0.5 0.0 0.0 /
-- =============================================================================
SOLUTION
-- =============================================================================
EQUIL
-- datum pressure OWC Pc@OWC GOC Pc@GOC RSVD RVVD SOLN
2000 250.0 2100 0.0 1900 0.0 1 0 0 /
RSVD
1900 100.0
2100 100.0 /
-- =============================================================================
SUMMARY
-- =============================================================================
WOPR
/
WGPR
/
WWPR
/
WBHP
/
GOPR
/
FOPR
FGPR
FWPR
-- =============================================================================
SCHEDULE
-- =============================================================================
GRUPTREE
'PLAT' 'FIELD' /
'PLAT-2' 'FIELD' /
'MANI' 'PLAT' /
'MANI-2' 'PLAT' /
/
-- Well specifications
WELSPECS
'WELL-A' 'MANI' 3 3 2010 'OIL' /
'WELL-A2' 'MANI' 4 4 2010 'OIL' /
'WELL-B' 'MANI-2' 5 5 2010 'OIL' /
'WELL-C' 'PLAT-2' 2 2 2010 'OIL' /
/
COMPDAT
'WELL-A' 3 3 1 3 'OPEN' 1* 1* 0.2 /
'WELL-A2' 4 4 1 3 'OPEN' 1* 1* 0.2 /
'WELL-B' 5 5 1 3 'OPEN' 1* 1* 0.2 /
'WELL-C' 2 2 1 3 'OPEN' 1* 1* 0.2 /
/
-- Group efficiency factors
GEFAC
'PLAT' 0.9 /
'PLAT-2' 0.85 /
'MANI' 0.8 /
'MANI-2' 0.75 /
/
-- Well efficiency factors
WEFAC
'WELL-A' 1.0 /
'WELL-A2' 1.0 /
'WELL-B' 1.0 /
'WELL-C' 1.0 /
/
-- Group controls
-- FIELD has overall oil rate target
GCONPROD
'FIELD' 'ORAT' 10000 1* 1* 1* 'RATE' /
/
-- PLAT is under FLD control with explicit guide rate
GCONPROD
'PLAT' 'FLD' 1* 1* 1* 1* 'RATE' 'YES' 9500 'OIL' /
/
-- PLAT-2 is under FLD control with explicit guide rate
GCONPROD
'PLAT-2' 'FLD' 1* 1* 1* 1* 'RATE' 'YES' 2550 'OIL' /
/
-- MANI is under FLD control with explicit guide rate (autochoke group)
-- Guide rate = E_MANI * (WELL-A + WELL-A2) = 0.8 * 10000 = 8000
GCONPROD
'MANI' 'FLD' 1* 1* 1* 1* 'RATE' 'YES' 8000 'OIL' /
/
-- MANI-2 is under FLD control with explicit guide rate
GCONPROD
'MANI-2' 'FLD' 1* 1* 1* 1* 'RATE' 'YES' 1500 'OIL' /
/
-- Guide rate formula for wells (guide_rate = oil_potential)
GUIDERAT
-- min.int type A B C D E F allow.inc damp.factor use.free.gas
0 'OIL' 1.0 1.0 0.0 0.0 0.0 0.0 'YES' 1.0 'NO' /
-- Wells under group control (NODEPROP will switch MANI wells to THP)
WCONPROD
'WELL-A' 'OPEN' 'GRUP' 1* 1* 1* 1* 1* 100.0 /
'WELL-A2' 'OPEN' 'GRUP' 1* 1* 1* 1* 1* 100.0 /
'WELL-B' 'OPEN' 'GRUP' 1* 1* 1* 1* 1* 100.0 /
'WELL-C' 'OPEN' 'GRUP' 1* 1* 1* 1* 1* 100.0 /
/
-- Extended network branches (all VFP=9999: no pressure drop)
BRANPROP
-- Downtree Uptree VFP ALQ
'MANI' 'PLAT' 9999 1* /
'MANI-2' 'PLAT' 9999 1* /
'PLAT' 'FIELD' 9999 1* /
'PLAT-2' 'FIELD' 9999 1* /
/
-- Extended network node properties
-- FIELD: terminal node with fixed pressure (200 barsa)
-- MANI: sub-sea manifold (autochoke group, as_choke=YES)
NODEPROP
-- Node Pressure autoChoke addGasLift
'FIELD' 200 'NO' 'NO' /
'MANI' 1* 'YES' 'NO' /
/
TSTEP
1 /
END
+291 -4
View File
@@ -62,6 +62,7 @@
#include <opm/simulators/wells/GroupState.hpp>
#include <opm/simulators/wells/FractionCalculator.hpp>
#include <opm/simulators/wells/TargetCalculator.hpp>
#include <opm/simulators/wells/WellGroupControls.hpp>
#include <boost/test/unit_test.hpp>
@@ -92,12 +93,20 @@ struct GroupConstraintTestFixture : ToleranceAndUnitFixture
bool plat_has_guide_rate,
Opm::Group::ProductionCMode mani_control,
Opm::Well::ProducerCMode well_a_control,
Opm::Well::ProducerCMode well_a2_control)
Opm::Well::ProducerCMode well_a2_control,
Opm::Group::ProductionCMode mani2_control = Opm::Group::ProductionCMode::FLD,
const std::string& data_file = "GROUP_HIGHER_CONSTRAINTS.DATA",
double well_a_rate_metric = 6000.0,
double well_a2_rate_metric = 4000.0)
{
plat_has_guide_rate_ = plat_has_guide_rate;
mani_control_ = mani_control;
mani2_control_ = mani2_control;
well_a_control_ = well_a_control;
well_a2_control_ = well_a2_control;
data_file_ = data_file;
well_a_rate_metric_ = well_a_rate_metric;
well_a2_rate_metric_ = well_a2_rate_metric;
setupSimulator_();
setupWellRates_();
@@ -208,6 +217,12 @@ private:
//! Control mode for MANI group (ORAT or FLD)
Opm::Group::ProductionCMode mani_control_{Opm::Group::ProductionCMode::ORAT};
//! Control mode for MANI-2 group (FLD or ORAT)
Opm::Group::ProductionCMode mani2_control_{Opm::Group::ProductionCMode::FLD};
//! DATA file to load
std::string data_file_{"GROUP_HIGHER_CONSTRAINTS.DATA"};
// Well rates (SM3/day, metric units for readability)
double well_a_rate_metric_{6000.0};
double well_a2_rate_metric_{4000.0};
@@ -228,8 +243,7 @@ private:
void setupSimulator_()
{
const std::string filename = "GROUP_HIGHER_CONSTRAINTS.DATA";
simulator_ = Opm::initSimulator<TypeTag>(filename.data());
simulator_ = Opm::initSimulator<TypeTag>(data_file_.data());
simulator_->model().applyInitialSolution();
simulator_->setEpisodeIndex(-1);
@@ -339,7 +353,7 @@ private:
// Set group controls
gs.production_control("MANI", mani_control_);
gs.production_control("MANI-2", Opm::Group::ProductionCMode::FLD);
gs.production_control("MANI-2", mani2_control_);
gs.production_control("PLAT", Opm::Group::ProductionCMode::FLD);
gs.production_control("PLAT-2", Opm::Group::ProductionCMode::FLD);
gs.production_control("FIELD", Opm::Group::ProductionCMode::ORAT);
@@ -971,4 +985,277 @@ BOOST_AUTO_TEST_CASE(TestWellGroupTargetProducerWithSiblingIndividualControl)
checkAlgo(metric_rate(target), expected_target_metric);
}
//! \brief Test getAutoChokeGroupProductionTargetRate() for autochoke group MANI
//!
//! This test exercises the autochoke target calculation which uses a simpler reduction loop
//! than applyReductionsAndFractions_(): it omits both local_reduction_level gating and addback.
//! With MANI-2 under individual ORAT control, a non-zero reduction exists at the PLAT level,
//! exposing the code-path difference.
//!
//! Code-path difference vs applyReductionsAndFractions_():
//! - Standard algorithm: applies reduction only at ii >= local_reduction_level, plus addback
//! - Autochoke loop: applies reduction at ii==0 OR where guideRate->has(chain[ii]) is true
//! In this scenario both produce the same result because:
//! (a) The autochoke group (MANI) has no individual wells, so no addback is needed
//! (b) PLAT has a guide rate, so both algorithms apply the PLAT-level reduction
//!
//! \code
//! FIELD (ORAT = 10000)
//! ├── PLAT (E=0.9, FLD, GUIDERATE=9500, GCW=2)
//! │ ├── MANI (E=0.8, FLD, GUIDERATE=8000, GCW=2, autochoke group)
//! │ │ ├── WELL-A (RATE=6000, POT=6000, GRUP)
//! │ │ └── WELL-A2 (RATE=4000, POT=4000, GRUP)
//! │ └── MANI-2 (E=0.75, IDV, ORAT, GUIDERATE=1500)
//! │ └── WELL-B (RATE=2000, POT=2000, GRUP)
//! └── PLAT-2 (E=0.85, FLD, GUIDERATE=2550, GCW=1)
//! └── WELL-C (RATE=3000, POT=3000, GRUP)
//! \endcode
BOOST_AUTO_TEST_CASE(TestAutoChokeGroupProductionTargetRate)
{
// Configuration: MANI under FLD control (autochoke group follows parent),
// both wells under GRUP control, MANI-2 under individual ORAT control.
// MANI-2 under ORAT creates a non-zero reduction at PLAT level (MANI-2's
// wells contribute) which is the key ingredient that differentiates the
// autochoke loop from applyReductionsAndFractions_().
commonSetupForAllTests(
/*plat_has_guide_rate=*/true,
/*mani_control =*/Opm::Group::ProductionCMode::FLD,
/*well_a_control =*/Opm::Well::ProducerCMode::GRUP,
/*well_a2_control =*/Opm::Well::ProducerCMode::GRUP,
/*mani2_control =*/Opm::Group::ProductionCMode::ORAT
);
auto& gs = groupState();
auto& gsh = groupStateHelper();
// Verify MANI-2 is under individual ORAT control
BOOST_CHECK(gs.production_control("MANI-2") == Opm::Group::ProductionCMode::ORAT);
BOOST_CHECK(gs.production_control("MANI") == Opm::Group::ProductionCMode::FLD);
// Verify reduction rates:
// - PLAT reduction: MANI is FLD with guide rate → skip.
// MANI-2 is ORAT (individual) → add E_MANI2 * wellB = 0.75 * 2000 = 1500
// - FIELD reduction: PLAT has guide rate and GCW > 0 → skip. PLAT-2 likewise → 0
{
const auto& plat_red = gs.production_reduction_rates("PLAT");
checkRate(metric_rate(plat_red[oilPhasePos()]), eMani2() * mani2TotalRateMetric()); // 1500
const auto& field_red = gs.production_reduction_rates("FIELD");
checkRate(metric_rate(field_red[oilPhasePos()]), 0.0);
}
// Verify groupControlledWells:
// MANI-2 is under individual ORAT → its wells don't count as group-controlled
// for the parent, but MANI-2 itself is excluded from PLAT's FLD children
{
const auto always_included = std::string("");
const auto is_prod = true;
const auto phase = Opm::Phase::OIL;
BOOST_CHECK_EQUAL(gsh.groupControlledWells("MANI", always_included, is_prod, phase), 2);
BOOST_CHECK_EQUAL(gsh.groupControlledWells("PLAT", always_included, is_prod, phase), 2);
BOOST_CHECK_EQUAL(gsh.groupControlledWells("FIELD", always_included, is_prod, phase), 3);
}
// Call getAutoChokeGroupProductionTargetRate()
// This is how it's called in BlackoilWellModelNetwork:
// getAutoChokeGroupProductionTargetRate(bottom_group=MANI, group=MANI, gsh, resv_coeff, 1.0)
// The function recurses upward from MANI (FLD) → PLAT (FLD) → FIELD (ORAT=controlling).
// During recursion: efficiencyFactor accumulates E_MANI * E_PLAT = 0.8 * 0.9 = 0.72.
auto resv_coeff = resvCoeff();
using WGC = Opm::WellGroupControls<double, IndexTraits>;
auto [target_rate_si, control_mode] = WGC::getAutoChokeGroupProductionTargetRate(
maniGroup(), // bottom_group
maniGroup(), // group (starts recursion here, recurses up to controlling group)
gsh,
resv_coeff,
/*efficiencyFactor=*/1.0
);
// Verify control mode is ORAT (from FIELD, the controlling group)
BOOST_CHECK(control_mode == Opm::Group::ProductionCMode::ORAT);
// ========================================================================
// EXPECTED COMPUTATION
// ========================================================================
//
// The function recurses from MANI (FLD) → PLAT (FLD) → FIELD (ORAT).
// During recursion, efficiencyFactor accumulates:
// 1.0 * E_MANI * E_PLAT = 0.8 * 0.9 = 0.72
// (Both MANI and PLAT multiply into efficiencyFactor before reaching FIELD.)
//
// chain = groupChainTopBot("MANI", "FIELD") = ["FIELD", "PLAT", "MANI"]
// num_ancestors = 2
// orig_target = 10000 SM3/day (FIELD's ORAT target)
//
// Loop ii=0 (FIELD):
// (ii==0) → apply reduction
// localReduction("FIELD") = 0 (no reduction at FIELD level)
// target = 10000 - 0 = 10000
// localFraction("PLAT"): PLAT=9500, PLAT-2=2550, both FLD → 9500/12050
// target = 10000 * 9500/12050 ≈ 7884.65
//
// Loop ii=1 (PLAT):
// guideRate->has("PLAT") = true → apply reduction
// localReduction("PLAT") = E_MANI2 * wellB_rate = 0.75 * 2000 = 1500
// target = 7884.65 - 1500 = 6384.65
// localFraction("MANI"): MANI is FLD with GCW=2, MANI-2 is ORAT (excluded) → 1.0
// target = 6384.65 * 1.0 = 6384.65
//
// Final: target / efficiencyFactor = 6384.65 / 0.72 ≈ 8867.57 SM3/day
//
// Code-path difference: in applyReductionsAndFractions_(), local_reduction_level
// gating and addback would apply at the intermediate PLAT level. Here, the autochoke
// loop applies reduction wherever guideRate->has() is true (PLAT), without addback.
// Both algorithms produce the same result here because MANI has no individual wells
// (reduction and addback would cancel if present).
//
const double eff = eMani() * ePlat(); // 0.72
const double plat_fraction = platGuideRateMetric()
/ (platGuideRateMetric() + plat2GuideRateMetric()); // 9500/12050
const double plat_reduction_metric = eMani2() * mani2TotalRateMetric(); // 1500
const double field_target_metric = 10000.0;
const double target_after_field = field_target_metric * plat_fraction; // ≈ 7884.65
const double target_after_plat = target_after_field - plat_reduction_metric; // ≈ 6384.65
const double mani_fraction = 1.0; // MANI is only FLD child at PLAT level
const double expected_target_metric = (target_after_plat * mani_fraction)
/ eff; // ≈ 8867.57
const double target_rate_metric = metric_rate(target_rate_si);
checkAlgo(target_rate_metric, expected_target_metric);
}
//! \brief Test getAutoChokeGroupProductionTargetRate() with GCW=0 (underperformance scenario)
//!
//! Uses the NETWORK DATA file where MANI is a sub-sea manifold (as_choke=true via NODEPROP).
//! NODEPROP switches MANI's wells to THP control and disables their guide rate availability.
//!
//! MANI's well rates are set low enough (total=5000) that isAutoChokeGroupUnderperforming_()
//! detects underperformance and updateGroupControlledWells() naturally sets MANI GCW=0.
//! With GCW=0 at MANI and PLAT, updateGroupTargetReductionRecursive_() uses case 1 (total
//! production propagated up) instead of case 3 (guide rate distribution). The
//! local_reduction_level gating in getAutoChokeGroupProductionTargetRate() ensures that
//! the PLAT-level reduction is skipped (since PLAT has GCW=0 and therefore
//! local_reduction_level=0 < ii=1), avoiding double-counting of reductions that were
//! already propagated into FIELD's reduction via case 1.
//!
//! \code
//! FIELD (ORAT = 10000, GCW=1)
//! ├── PLAT (E=0.9, FLD, GUIDERATE=9500, GCW=0)
//! │ ├── MANI (E=0.8, FLD, GUIDERATE=8000, GCW=0, as_choke, underperforming)
//! │ │ ├── WELL-A (RATE=3000, POT=3000, THP)
//! │ │ └── WELL-A2 (RATE=2000, POT=2000, THP)
//! │ └── MANI-2 (E=0.75, IDV, ORAT, GUIDERATE=1500, GCW=1)
//! │ └── WELL-B (RATE=2000, POT=2000, GRUP)
//! └── PLAT-2 (E=0.85, FLD, GUIDERATE=2550, GCW=1)
//! └── WELL-C (RATE=3000, POT=3000, GRUP)
//! \endcode
BOOST_AUTO_TEST_CASE(TestAutoChokeGroupTargetRateWithUnderperformance)
{
// Uses NETWORK DATA file: MANI has as_choke()=true from NODEPROP,
// wells under MANI are switched to THP control by the NODEPROP handler.
// Lower MANI well rates (3000+2000=5000) trigger underperformance detection
// in isAutoChokeGroupUnderperforming_(), which naturally sets MANI GCW=0.
commonSetupForAllTests(
/*plat_has_guide_rate=*/true,
/*mani_control =*/Opm::Group::ProductionCMode::FLD,
/*well_a_control =*/Opm::Well::ProducerCMode::THP,
/*well_a2_control =*/Opm::Well::ProducerCMode::THP,
/*mani2_control =*/Opm::Group::ProductionCMode::ORAT,
/*data_file =*/"GROUP_HIGHER_CONSTRAINTS_NETWORK.DATA",
/*well_a_rate_metric =*/3000.0,
/*well_a2_rate_metric=*/2000.0
);
// Verify NODEPROP correctly set as_choke() on MANI
BOOST_CHECK(maniGroup().as_choke());
BOOST_CHECK(!platGroup().as_choke());
auto& gs = groupState();
auto& gsh = groupStateHelper();
// finalizeSetup_() called updateGroupControlledWells() which runs
// isAutoChokeGroupUnderperforming_(): MANI (total=5000) is below its target
// share, so all wells are excluded → MANI GCW=0.
// With MANI GCW=0 and MANI-2 under ORAT (not FLD), PLAT GCW also becomes 0.
//
// Verify GCW values (naturally 0 from underperformance detection)
{
const auto always_included = std::string("");
const auto is_prod = true;
const auto phase = Opm::Phase::OIL;
BOOST_CHECK_EQUAL(gsh.groupControlledWells("MANI", always_included, is_prod, phase), 0);
BOOST_CHECK_EQUAL(gsh.groupControlledWells("PLAT", always_included, is_prod, phase), 0);
BOOST_CHECK_EQUAL(gsh.groupControlledWells("FIELD", always_included, is_prod, phase), 1);
}
// finalizeSetup_() called updateGroupTargetReduction() with
// GCW=0 at MANI and PLAT. updateGroupTargetReductionRecursive_() uses
// case 1 (total production propagated up) instead of case 3 (nothing):
//
// reduction(MANI) = 0 (wells are THP, and !group.as_choke() is false)
//
// reduction(PLAT):
// MANI (FLD, GCW=0): case 1 → E_MANI * sumWellSurfaceRates(MANI) = 0.8 * 5000 = 4000
// MANI-2 (ORAT): case 1 → E_MANI2 * sumWellSurfaceRates(MANI-2) = 0.75 * 2000 = 1500
// reduction(PLAT) = 5500
//
// reduction(FIELD):
// PLAT (FLD, GCW=0): case 1 → E_PLAT * sumWellSurfaceRates(PLAT) = 0.9 * 5500 = 4950
// PLAT-2 (FLD, GCW>0, guide rate): case 3 → nothing
// reduction(FIELD) = 4950
//
// Verify reduction rates
{
const auto& plat_red = gs.production_reduction_rates("PLAT");
const double plat_red_metric = eMani() * maniTotalRateMetric()
+ eMani2() * mani2TotalRateMetric(); // 4000 + 1500 = 5500
checkRate(metric_rate(plat_red[oilPhasePos()]), plat_red_metric);
const auto& field_red = gs.production_reduction_rates("FIELD");
const double field_red_metric = ePlat() * platTotalRateMetric(); // 0.9 * 5500 = 4950
checkRate(metric_rate(field_red[oilPhasePos()]), field_red_metric);
}
// Call getAutoChokeGroupProductionTargetRate
auto resv_coeff = resvCoeff();
using WGC = Opm::WellGroupControls<double, IndexTraits>;
auto [target_rate_si, control_mode] = WGC::getAutoChokeGroupProductionTargetRate(
maniGroup(), // bottom_group
maniGroup(), // group (starts recursion here)
gsh,
resv_coeff,
/*efficiencyFactor=*/1.0
);
BOOST_CHECK(control_mode == Opm::Group::ProductionCMode::ORAT);
// ========================================================================
// EXPECTED COMPUTATION
// ========================================================================
//
// chain = [FIELD, PLAT, MANI], efficiencyFactor = E_MANI * E_PLAT = 0.72
// local_reduction_level = 0 (PLAT has guide rate but GCW=0 → doesn't qualify)
//
// ii=0 (FIELD):
// local_reduction_level(0) >= ii(0) → true → apply reduction
// reduction(FIELD) = 4950
// target = 10000 - 4950 = 5050
// localFraction("PLAT") = 1.0 (PLAT GCW=0 → excluded from sum,
// but PLAT-2 is the only active sibling → num_active=1 → return 1.0)
// target = 5050 * 1.0 = 5050
//
// ii=1 (PLAT):
// local_reduction_level(0) >= ii(1) → false → skip reduction
// (PLAT's reduction was already propagated into FIELD's via case 1)
// localFraction("MANI") = 1.0 (MANI GCW=0, MANI-2 ORAT → num_active=0,
// total=0, potentials=0 → return 1.0)
// target = 5050 * 1.0 = 5050
//
// Final: max(0, 5050 / 0.72) ≈ 7013.89
//
const double target_rate_metric = metric_rate(target_rate_si);
const double expected_target = 5050.0 / 0.72; // ≈ 7013.89
checkAlgo(target_rate_metric, expected_target);
}
BOOST_AUTO_TEST_SUITE_END()