Files
opm-simulators/tests/test_group_higher_constraints.cpp
T

1258 lines
59 KiB
C++

// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
// vi: set et ts=4 sw=4 sts=4:
/*
This file is part of the Open Porous Media project (OPM).
OPM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OPM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OPM. If not, see <http://www.gnu.org/licenses/>.
Consult the COPYING file in the top-level source directory of this
module for the precise wording of the license and the list of
copyright holders.
*/
/*!
* \file
* \brief Test for efficiency factor handling, addback and guide rate fractions in group constraint checking
*
*
* Group hierarchy used in the test:
* \code
* FIELD (ORAT = 10000)
* / \
* / \
* PLAT (GEFAC=0.9) PLAT-2 (GEFAC=0.85)
* / \ |
* / \ WELL-C
* MANI (0.8) MANI-2 (0.75)
* / \ |
* WELL-A WELL-A2 WELL-B
* \endcode
*
* Guide rates (explicit from GCONPROD):
* MANI: 8000, MANI-2: 1500, PLAT: 9500, PLAT-2: 2550
*
* Fractions:
* MANI at PLAT: 8000 / 9500 ≈ 0.842
* PLAT at FIELD: 9500 / 12050 ≈ 0.788
*
*/
#define BOOST_TEST_MODULE GroupHigherConstraints
#include "SimulatorFixture.hpp"
#include "ToleranceAndUnitFixture.hpp"
#include <opm/input/eclipse/Schedule/Schedule.hpp>
#include <opm/input/eclipse/Schedule/Group/Group.hpp>
#include <opm/input/eclipse/Schedule/Group/GuideRate.hpp>
#include <opm/input/eclipse/Schedule/Group/GuideRateModel.hpp>
#include <opm/input/eclipse/Units/Units.hpp>
#include <opm/simulators/flow/FlowProblemBlackoil.hpp>
#include <opm/simulators/wells/BlackoilWellModel.hpp>
#include <opm/simulators/wells/WellState.hpp>
#include <opm/simulators/wells/GroupState.hpp>
#include <opm/simulators/wells/FractionCalculator.hpp>
#include <opm/simulators/wells/TargetCalculator.hpp>
#include <boost/test/unit_test.hpp>
namespace Opm::Properties::TTag {
struct TestEfficiencyFactorTypeTag {
using InheritsFrom = std::tuple<TestTypeTag>;
};
}
using SimulatorFixture = Opm::SimulatorFixture;
BOOST_GLOBAL_FIXTURE(SimulatorFixture);
namespace {
// Using shared ToleranceAndUnitFixture from ToleranceAndUnitFixture.hpp
//! \brief Test fixture for group constraint tests to avoid code duplication
//!
//! Tests can customize behavior via configuration options before calling setup().
struct GroupConstraintTestFixture : ToleranceAndUnitFixture
{
using TypeTag = Opm::Properties::TTag::TestEfficiencyFactorTypeTag;
using WellModel = Opm::BlackoilWellModel<TypeTag>;
using FluidSystem = Opm::GetPropType<TypeTag, Opm::Properties::FluidSystem>;
using IndexTraits = typename FluidSystem::IndexTraitsType;
void commonSetupForAllTests(
bool plat_has_guide_rate,
Opm::Group::ProductionCMode mani_control,
Opm::Well::ProducerCMode well_a_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_();
setupGroupState_();
setupGuideRates_();
finalizeSetup_();
}
// ========================================================================
// Accessors
// ========================================================================
double wellARateMetric() const { return well_a_rate_metric_; }
double wellA2RateMetric() const { return well_a2_rate_metric_; }
double wellBRateMetric() const { return well_b_rate_metric_; }
double wellCRateMetric() const { return well_c_rate_metric_; }
double wellARateSI() const { return si_rate(-well_a_rate_metric_); }
double wellA2RateSI() const { return si_rate(-well_a2_rate_metric_); }
double wellBRateSI() const { return si_rate(-well_b_rate_metric_); }
double wellCRateSI() const { return si_rate(-well_c_rate_metric_); }
WellModel& wellModel() { return simulator_->problem().wellModel(); }
const Opm::Schedule& schedule() const { return simulator_->vanguard().schedule(); }
auto& wellState() { return wellModel().wellState(); }
auto& groupState() { return wellModel().groupState(); }
Opm::GuideRate& guideRate() { return wellModel().guideRate(); }
auto& groupStateHelper() { return wellModel().groupStateHelper(); }
int reportStepIdx() const { return report_step_idx_; }
int oilPhasePos() const { return oil_phase_pos_; }
Opm::DeferredLogger& deferredLogger() { return deferred_logger_; }
const Opm::Group& maniGroup() const { return schedule().getGroup("MANI", report_step_idx_); }
const Opm::Group& mani2Group() const { return schedule().getGroup("MANI-2", report_step_idx_); }
const Opm::Group& platGroup() const { return schedule().getGroup("PLAT", report_step_idx_); }
const Opm::Group& plat2Group() const { return schedule().getGroup("PLAT-2", report_step_idx_); }
const Opm::Group& fieldGroup() const { return schedule().getGroup("FIELD", report_step_idx_); }
//! Efficiency factors from schedule
double eMani() const { return maniGroup().getGroupEfficiencyFactor(); }
double eMani2() const { return mani2Group().getGroupEfficiencyFactor(); }
double ePlat() const { return platGroup().getGroupEfficiencyFactor(); }
double ePlat2() const { return plat2Group().getGroupEfficiencyFactor(); }
//! Get MANI's total rate (SI units, positive for production)
double maniTotalRate() const { return -wellARateSI() - wellA2RateSI(); }
//! Get MANI's total rate (metric, positive for production)
double maniTotalRateMetric() const { return wellARateMetric() + wellA2RateMetric(); }
//! Get MANI-2's total rate (metric, positive for production)
double mani2TotalRateMetric() const { return wellBRateMetric(); }
//! Get PLAT's total rate (metric, positive for production)
double platTotalRateMetric() const { return eMani() * maniTotalRateMetric() + eMani2() * mani2TotalRateMetric(); }
//! Guide rates (from GCONPROD via guideRate object)
double maniGuideRateMetric() {
const Opm::GuideRate::RateVector zero_rates{0.0, 0.0, 0.0};
return guideRate().get("MANI", Opm::GuideRateModel::Target::OIL, zero_rates);
}
double mani2GuideRateMetric() {
const Opm::GuideRate::RateVector zero_rates{0.0, 0.0, 0.0};
return guideRate().get("MANI-2", Opm::GuideRateModel::Target::OIL, zero_rates);
}
double platGuideRateMetric() {
const Opm::GuideRate::RateVector zero_rates{0.0, 0.0, 0.0};
return guideRate().get("PLAT", Opm::GuideRateModel::Target::OIL, zero_rates);
}
double plat2GuideRateMetric() {
const Opm::GuideRate::RateVector zero_rates{0.0, 0.0, 0.0};
return guideRate().get("PLAT-2", Opm::GuideRateModel::Target::OIL, zero_rates);
}
// ========================================================================
// Helper methods for common test operations
// ========================================================================
//! Prepare rates array for MANI group (for checkGroupConstraintsProd)
std::vector<double> maniRatesArray() const
{
std::vector<double> rates(num_phases_, 0.0);
rates[oil_phase_pos_] = wellARateSI() + wellA2RateSI(); // negative
return rates;
}
//! Prepare rates array for WELL-A (for getWellGroupTargetProducer)
std::vector<double> wellARatesArray() const
{
std::vector<double> rates(num_phases_, 0.0);
rates[oil_phase_pos_] = wellARateSI(); // negative
return rates;
}
//! Get default resv_coeff (all 1.0)
std::vector<double> resvCoeff() const
{
return std::vector<double>(num_phases_, 1.0);
}
private:
//! Whether PLAT group has a guide rate (affects local_reduction_level)
bool plat_has_guide_rate_{true};
//! Control mode for WELL-A (GRUP or ORAT)
Opm::Well::ProducerCMode well_a_control_{Opm::Well::ProducerCMode::GRUP};
//! Control mode for WELL-A2 (GRUP or ORAT)
Opm::Well::ProducerCMode well_a2_control_{Opm::Well::ProducerCMode::GRUP};
//! 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};
double well_b_rate_metric_{2000.0};
double well_c_rate_metric_{3000.0};
std::unique_ptr<Opm::Simulator<TypeTag>> simulator_;
int report_step_idx_{0};
int num_phases_{0};
int oil_phase_pos_{0};
Opm::DeferredLogger deferred_logger_;
// ========================================================================
// Private setup methods
// ========================================================================
void setupSimulator_()
{
simulator_ = Opm::initSimulator<TypeTag>(data_file_.data());
simulator_->model().applyInitialSolution();
simulator_->setEpisodeIndex(-1);
simulator_->setEpisodeLength(0.0);
simulator_->startNextEpisode(/*episodeStartTime=*/0.0, /*episodeLength=*/1e30);
simulator_->setTimeStepSize(Opm::unit::day);
// Reset iteration context so code using problem().iterationContext() sees first iteration
simulator_->problem().resetIterationForNewTimestep();
wellModel().beginReportStep(report_step_idx_);
const auto& pu = wellModel().phaseUsage();
num_phases_ = pu.numActivePhases();
oil_phase_pos_ = pu.canonicalToActivePhaseIdx(IndexTraits::oilPhaseIdx);
}
void setupWellRates_()
{
auto& ws = wellModel().wellState();
// For simplicity, assume the same well potentials as rates
// Set WELL-A: surface_rates (negative) and well_potentials (positive)
{
auto well_idx = ws.index("WELL-A");
BOOST_REQUIRE(well_idx.has_value());
auto& well = ws.well(well_idx.value());
std::ranges::fill(well.surface_rates, 0.0);
std::ranges::fill(well.well_potentials, 0.0);
well.surface_rates[oil_phase_pos_] = wellARateSI();
well.well_potentials[oil_phase_pos_] = -wellARateSI(); // positive
well.production_cmode = well_a_control_;
}
// Set WELL-A2
{
auto well_idx = ws.index("WELL-A2");
BOOST_REQUIRE(well_idx.has_value());
auto& well = ws.well(well_idx.value());
std::ranges::fill(well.surface_rates, 0.0);
std::ranges::fill(well.well_potentials, 0.0);
well.surface_rates[oil_phase_pos_] = wellA2RateSI();
well.well_potentials[oil_phase_pos_] = -wellA2RateSI(); // positive
well.production_cmode = well_a2_control_;
}
// Set WELL-B
{
auto well_idx = ws.index("WELL-B");
BOOST_REQUIRE(well_idx.has_value());
auto& well = ws.well(well_idx.value());
std::ranges::fill(well.surface_rates, 0.0);
std::ranges::fill(well.well_potentials, 0.0);
well.surface_rates[oil_phase_pos_] = wellBRateSI();
well.well_potentials[oil_phase_pos_] = -wellBRateSI(); // positive
well.production_cmode = Opm::Well::ProducerCMode::GRUP;
}
// Set WELL-C
{
auto well_idx = ws.index("WELL-C");
BOOST_REQUIRE(well_idx.has_value());
auto& well = ws.well(well_idx.value());
std::ranges::fill(well.surface_rates, 0.0);
std::ranges::fill(well.well_potentials, 0.0);
well.surface_rates[oil_phase_pos_] = wellCRateSI();
well.well_potentials[oil_phase_pos_] = -wellCRateSI(); // positive
well.production_cmode = Opm::Well::ProducerCMode::GRUP;
}
// Update global well info
std::vector<Opm::Well::Status> well_status(ws.size(), Opm::Well::Status::OPEN);
const auto& comm = simulator_->vanguard().grid().comm();
ws.updateGlobalIsGrup(comm, well_status);
}
void setupGuideRates_()
{
// Use updateGuideRates() to compute guide rates from:
// - Explicit values in GCONPROD for MANI (8000), MANI-2 (1500), PLAT (9500), PLAT-2 (2550)
// - GUIDERAT formula for wells (guide_rate = oil_potential)
//
// The explicit GCONPROD values ensure predictable guide rate fractions:
// MANI fraction at PLAT = 8000 / 9500 ≈ 0.842
// PLAT fraction at FIELD = 9500 / 12050 ≈ 0.788
const double sim_time = 0.0;
wellModel().updateGuideRates(report_step_idx_, sim_time);
// If test requires PLAT to NOT have a guide rate (to test local_reduction_level=0),
// erase it after updateGuideRates() has set it from GCONPROD
if (!plat_has_guide_rate_) {
guideRate().erase("PLAT");
}
}
void setupGroupState_()
{
auto& gs = wellModel().groupState();
auto& gsh = wellModel().groupStateHelper();
// Compute group production rates from well surface rates
// This applies efficiency factors at each level (see sumWellPhaseRates):
// MANI: sum of wells = -10000 (WELL-A + WELL-A2)
// MANI-2: sum of wells = -2000 (WELL-B)
// PLAT: E_MANI * MANI + E_MANI2 * MANI-2 = 0.8 * -10000 + 0.75 * -2000 = -9500
// PLAT-2: sum of wells = -3000 (WELL-C, no child groups)
// FIELD: E_PLAT * PLAT + E_PLAT2 * PLAT-2 = 0.9 * -9500 + 0.85 * -3000 = -11100
gsh.updateGroupProductionRates(fieldGroup());
// Set group controls
gs.production_control("MANI", mani_control_);
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);
}
void finalizeSetup_()
{
auto& gsh = wellModel().groupStateHelper();
const auto& field_group = schedule().getGroup("FIELD", report_step_idx_);
gsh.updateGroupControlledWells(/*is_production_group=*/true, Opm::Phase::OIL);
gsh.updateGroupTargetReduction(field_group, /*is_injector=*/false);
}
};
} // anonymous namespace
BOOST_FIXTURE_TEST_SUITE(GroupHigherConstraintsTests, GroupConstraintTestFixture)
//! \brief Test checkGroupConstraintsProd() for group MANI with a guide rate at PLAT level between MANI and FIELD
//!
//! When there's a guide rate at PLAT level between MANI and FIELD, local_reduction_level is 1
//! and the addback happens at PLAT level. This test verifies that an addback efficiency factor of 0.8 is
//! applied to PLAT (local_reduction_level = 1) and not 0.8 * 0.9 = 0.72. (original version of
//! checkGroupConstraintsProd() had this bug)
//! \code
//! (GCW = Group Controlled Wells, IDV = Individual control)
//!
//! FIELD (ORAT = 10000, GCW=2)
//! ├── PLAT (E=0.9, FLD, GUIDERATE=9500, GCW=1, local_reduction_level=1)
//! │ ├── MANI (E=0.8, IDV, ORAT, GUIDERATE=8000, GCW=2)
//! │ │ ├── WELL-A (RATE=6000, POT=6000, GRUP)
//! │ │ └── WELL-A2 (RATE=4000, POT=4000, GRUP)
//! │ └── MANI-2 (E=0.75, FLD, 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(TestGroupHigherConstraintsWithGuideRate)
{
// Configuration: Guide rate for PLAT (local_reduction_level = 1)
commonSetupForAllTests(/*plat_has_guide_rate=*/true,
/*mani_control=*/Opm::Group::ProductionCMode::ORAT,
/*well_a_control=*/Opm::Well::ProducerCMode::GRUP,
/*well_a2_control=*/Opm::Well::ProducerCMode::GRUP);
// Verify the test setup
auto& gsh = groupStateHelper();
auto& ws = wellState();
auto& gs = groupState();
// Verify efficiency factors from schedule
checkClose(eMani(), 0.8);
checkClose(eMani2(), 0.75);
checkClose(ePlat(), 0.9);
checkClose(ePlat2(), 0.85);
const double accumulated_eff = eMani() * ePlat();
checkClose(accumulated_eff, 0.72);
// Verify well rates in WellState
checkRate(metric_rate(ws.well("WELL-A").surface_rates[oilPhasePos()]), -wellARateMetric()); // -6000.0
checkRate(metric_rate(ws.well("WELL-A2").surface_rates[oilPhasePos()]), -wellA2RateMetric()); // -4000.0
checkRate(metric_rate(ws.well("WELL-B").surface_rates[oilPhasePos()]), -wellBRateMetric()); // -2000.0
checkRate(metric_rate(ws.well("WELL-C").surface_rates[oilPhasePos()]), -wellCRateMetric()); // -3000.0
// Verify well potentials (stored in SI, convert to metric for comparison)
checkRate(metric_rate(ws.well("WELL-A").well_potentials[oilPhasePos()]), wellARateMetric()); // 6000.0
checkRate(metric_rate(ws.well("WELL-A2").well_potentials[oilPhasePos()]), wellA2RateMetric()); // 4000.0
checkRate(metric_rate(ws.well("WELL-B").well_potentials[oilPhasePos()]), wellBRateMetric()); // 2000.0
checkRate(metric_rate(ws.well("WELL-C").well_potentials[oilPhasePos()]), wellCRateMetric()); // 3000.0
// Verify wells are marked as under group control
BOOST_CHECK(ws.isProductionGrup("WELL-A"));
BOOST_CHECK(ws.isProductionGrup("WELL-A2"));
BOOST_CHECK(ws.isProductionGrup("WELL-B"));
BOOST_CHECK(ws.isProductionGrup("WELL-C"));
// Verify guide rates are set from GCONPROD via updateGuideRates()
// Wells use GUIDERAT formula (guide_rate = oil_potential), groups use explicit GCONPROD values
BOOST_CHECK(guideRate().has("WELL-A"));
BOOST_CHECK(guideRate().has("WELL-A2"));
BOOST_CHECK(guideRate().has("WELL-B"));
BOOST_CHECK(guideRate().has("WELL-C"));
BOOST_CHECK(guideRate().has("MANI"));
BOOST_CHECK(guideRate().has("MANI-2"));
BOOST_CHECK(guideRate().has("PLAT"));
BOOST_CHECK(guideRate().has("PLAT-2"));
// Verify group guide rates (explicit values from GCONPROD)
const Opm::GuideRate::RateVector zero_rates{0.0, 0.0, 0.0};
checkClose(guideRate().get("MANI", Opm::GuideRateModel::Target::OIL, zero_rates), maniGuideRateMetric());
checkClose(guideRate().get("MANI-2", Opm::GuideRateModel::Target::OIL, zero_rates), mani2GuideRateMetric());
checkClose(guideRate().get("PLAT", Opm::GuideRateModel::Target::OIL, zero_rates), platGuideRateMetric());
checkClose(guideRate().get("PLAT-2", Opm::GuideRateModel::Target::OIL, zero_rates), plat2GuideRateMetric());
// Verify local fractions using FractionCalculator
// FractionCalculator only includes groups/wells that:
// 1. Have FLD or NONE control (for groups) or GRUP control (for wells)
// 2. Have groupControlledWells > 0
{
using FCalc = Opm::GroupStateHelpers::FractionCalculator<double, IndexTraits>;
FCalc fcalc{schedule(), gsh, gsh.summaryState(), reportStepIdx(),
&guideRate(), Opm::GuideRateModel::Target::OIL,
/*is_producer=*/true, /*injection_phase=*/Opm::Phase::OIL};
auto localFraction = [&](const std::string& name) {
return fcalc.localFraction(name, /*always_included_child=*/"");
};
// Well fractions within MANI (both wells under GRUP control):
// WELL-A guide rate = oil_potential = 6000
// WELL-A2 guide rate = oil_potential = 4000
// WELL-A fraction = 6000 / (6000 + 4000) = 0.6
// WELL-A2 fraction = 4000 / (6000 + 4000) = 0.4
const double well_a_frac = localFraction("WELL-A");
const double well_a2_frac = localFraction("WELL-A2");
checkClose(well_a_frac, wellARateMetric() / (wellARateMetric() + wellA2RateMetric())); // 0.6
checkClose(well_a2_frac, wellA2RateMetric() / (wellARateMetric() + wellA2RateMetric())); // 0.4
checkClose(well_a_frac + well_a2_frac, 1.0);
// WELL-B fraction within MANI-2 (only well):
const double well_b_frac = localFraction("WELL-B");
checkClose(well_b_frac, 1.0); // Only well in MANI-2
// Group fractions within PLAT:
// MANI has ORAT control (not FLD), so it's excluded from parent's sum
// MANI-2 has FLD control and 1 group-controlled well (WELL-B) -> included
// Since MANI-2 is the only FLD child of PLAT, both return 1.0
const double mani_frac = localFraction("MANI");
const double mani2_frac = localFraction("MANI-2");
checkClose(mani_frac, 1.0); // MANI excluded (ORAT control), only MANI-2 active
checkClose(mani2_frac, 1.0); // Only active group at PLAT level
// Group fractions at FIELD level:
// PLAT has FLD control and 1 group-controlled well (via MANI-2) -> included
// PLAT-2 has FLD control and 1 group-controlled well (WELL-C) -> included
// Both compete for rate allocation: PLAT=9500, PLAT-2=2550
const double plat_frac = localFraction("PLAT");
const double plat2_frac = localFraction("PLAT-2");
// PLAT fraction = PLAT guide rate / (PLAT guide rate + PLAT-2 guide rate) = 9500/12050 = 0.788
checkClose(plat_frac, platGuideRateMetric() / (platGuideRateMetric() + plat2GuideRateMetric()));
// PLAT-2 fraction = PLAT-2 guide rate / (PLAT guide rate + PLAT-2 guide rate) = 2550/12050 = 0.212
checkClose(plat2_frac, plat2GuideRateMetric() / (platGuideRateMetric() + plat2GuideRateMetric()));
checkClose(plat_frac + plat2_frac, 1.0);
// WELL-C fraction within PLAT-2 (only well):
const double well_c_frac = localFraction("WELL-C");
checkClose(well_c_frac, 1.0); // Only well in PLAT-2
}
// Verify groupControlledWells count
{
const auto always_included_child = std::string("");
const auto is_production_group = true;
const auto injection_phase = Opm::Phase::OIL;
BOOST_CHECK_EQUAL(gsh.groupControlledWells(
"MANI", always_included_child, is_production_group, injection_phase), 2);
BOOST_CHECK_EQUAL(gsh.groupControlledWells(
"MANI-2", always_included_child, is_production_group, injection_phase), 1);
BOOST_CHECK_EQUAL(gsh.groupControlledWells(
"PLAT-2", always_included_child, is_production_group, injection_phase), 1);
BOOST_CHECK_EQUAL(gsh.groupControlledWells(
"PLAT", always_included_child, is_production_group, injection_phase), 1); // via MANI-2
BOOST_CHECK_EQUAL(gsh.groupControlledWells(
"FIELD", always_included_child, is_production_group, injection_phase), 2); // PLAT + PLAT-2
}
// Test sumWellSurfaceRates for each group
{
const double sum_mani = gsh.sumWellSurfaceRates(maniGroup(), oilPhasePos(), /*injector=*/false);
const double sum_mani2 = gsh.sumWellSurfaceRates(mani2Group(), oilPhasePos(), /*injector=*/false);
const double sum_plat2 = gsh.sumWellSurfaceRates(plat2Group(), oilPhasePos(), /*injector=*/false);
const double sum_plat = gsh.sumWellSurfaceRates(platGroup(), oilPhasePos(), /*injector=*/false);
// WELL-A + WELL-A2 = 6000.0 + 4000.0 = 10000.0
checkRate(metric_rate(sum_mani), wellARateMetric() + wellA2RateMetric()); // 10000.0
checkRate(metric_rate(sum_mani2), wellBRateMetric()); // WELL-B = 2000.0
checkRate(metric_rate(sum_plat2), wellCRateMetric()); // WELL-C = 3000.0
checkRate(metric_rate(sum_plat), platTotalRateMetric()); // E_MANI*10000 + E_MANI2*2000 = 9500.0
}
// Test FIELD's ORAT target
{
const auto& field_group_ctrl = fieldGroup().productionControls(gsh.summaryState());
checkRate(metric_rate(field_group_ctrl.oil_target), 10000.0);
}
// Test production control modes from schedule (GCONPROD)
{
const auto& field_ctrl = fieldGroup().productionControls(gsh.summaryState());
BOOST_CHECK(field_ctrl.cmode == Opm::Group::ProductionCMode::ORAT);
const auto& plat_ctrl = platGroup().productionControls(gsh.summaryState());
BOOST_CHECK(plat_ctrl.cmode == Opm::Group::ProductionCMode::FLD);
const auto& plat2_ctrl = plat2Group().productionControls(gsh.summaryState());
BOOST_CHECK(plat2_ctrl.cmode == Opm::Group::ProductionCMode::FLD);
const auto& mani_ctrl = maniGroup().productionControls(gsh.summaryState());
BOOST_CHECK(mani_ctrl.cmode == Opm::Group::ProductionCMode::ORAT);
const auto& mani2_ctrl = mani2Group().productionControls(gsh.summaryState());
BOOST_CHECK(mani2_ctrl.cmode == Opm::Group::ProductionCMode::FLD);
}
// Test production control modes from GroupState (set by setupGroupState())
{
BOOST_CHECK(gs.production_control("FIELD") == Opm::Group::ProductionCMode::ORAT);
BOOST_CHECK(gs.production_control("PLAT") == Opm::Group::ProductionCMode::FLD);
BOOST_CHECK(gs.production_control("PLAT-2") == Opm::Group::ProductionCMode::FLD);
BOOST_CHECK(gs.production_control("MANI") == Opm::Group::ProductionCMode::ORAT);
BOOST_CHECK(gs.production_control("MANI-2") == Opm::Group::ProductionCMode::FLD);
}
// Verify group reduction rates
// PLAT: E_MANI * 10000 = 8000 (MANI has individual ORAT control, MANI-2 is FLD so doesn't add)
// FIELD: 0 (PLAT has guide rate AND group-controlled wells via MANI-2, so reduction doesn't propagate)
// Note: With MANI-2 under PLAT, PLAT now has group-controlled wells, so the else branch in
// updateGroupTargetReductionRecursive_ is taken. Since PLAT has a guide rate, its
// reduction is NOT accumulated to FIELD.
{
const auto& field_red = gs.production_reduction_rates("FIELD");
checkRate(metric_rate(field_red[oilPhasePos()]), 0.0); // 0 because PLAT has guide rate and group-controlled wells
const auto& plat_red = gs.production_reduction_rates("PLAT");
checkRate(metric_rate(plat_red[oilPhasePos()]), eMani() * maniTotalRateMetric()); // 8000.0
const auto& plat2_red = gs.production_reduction_rates("PLAT-2");
checkRate(metric_rate(plat2_red[oilPhasePos()]), 0.0);
const auto& mani_red = gs.production_reduction_rates("MANI");
checkRate(metric_rate(mani_red[oilPhasePos()]), 0.0); // 0.0 since both WELL-A and WELL-A2 are under group control
const auto& mani2_red = gs.production_reduction_rates("MANI-2");
checkRate(metric_rate(mani2_red[oilPhasePos()]), 0.0); // 0.0 since WELL-B is under group control
}
// Call checkGroupConstraintsProd
auto mani_rates = maniRatesArray();
auto resv_coeff = resvCoeff();
const double initial_efficiency = eMani();
auto [constraint_violated, scale] = gsh.checkGroupConstraintsProd(
"MANI",
"PLAT",
platGroup(),
mani_rates.data(),
initial_efficiency,
resv_coeff,
/*check_guide_rate=*/true
);
// ========================================================================
// EXPECTED RESULT (with PLAT having guide rate -> local_reduction_level = 1)
// ========================================================================
//
// The checkGroupConstraintsProd algorithm proceeds as follows:
//
// Setup:
// - Chain: [FIELD, PLAT, MANI]
// - efficiency_factor = E_MANI * E_PLAT = 0.8 * 0.9 = 0.72
// - current_rate_available = 10000 SM3/day (MANI's production)
// - orig_target = 10000 SM3/day (FIELD's ORAT)
// - local_reduction_level = 1 (PLAT is under FLD control AND has guide rate AND num_gr_ctrl > 0)
//
// Loop iteration ii=0 (FIELD):
// - reduction_fn("FIELD") = 0 (FIELD has no local reduction)
// - target = 10000 - 0 = 10000
// - No addback (ii != local_reduction_level)
// - PLAT fraction at FIELD = 9500 / (9500 + 2550) ≈ 0.788
// - target = 10000 * 0.788 ≈ 7884.65
//
// Loop iteration ii=1 (PLAT):
// - Reduction condition: local_reduction_level==1, ii==1, reduction applied
// - reduction_fn("PLAT") = E_MANI * 10000 = 8000 SM3/day (MANI has ORAT control)
// - target = 7884.65 - 8000 ≈ -115.35
//
// - Addback (ii == local_reduction_level==1):
// - addback_efficiency = E_MANI = 0.8
// - target += current_rate * 0.8 = -115.35 + 10000 * 0.8 = -115.35 + 8000 ≈ 7884.65
//
// - MANI fraction at PLAT = 8000 / (8000 + 1500) ≈ 0.842
// - target = 7884.65 * 0.842 ≈ 6640.07
//
// Final calculation:
// target_rate_available = target / efficiency_factor
// = 6640.07 / 0.72 ≈ 9222.32
//
// Scale factor:
// scale = target_rate_available / current_rate_available
// = 9222.32 / 10000 ≈ 0.922
//
// The constraint is violated (scale < 1.0) because MANI's share of FIELD's target
// (after applying PLAT and MANI fractions) is less than its current production.
//
// Expected: scale ≈ 0.922 (constraint violated)
const double expected_scale = 0.92208390963577669;
BOOST_CHECK(constraint_violated); // Constraint IS violated with new hierarchy
checkAlgo(scale, expected_scale);
// Derived values for verification
const double current_rate = 10000.0; // SM3/day
const double target_rate_available = scale * current_rate;
checkAlgo(target_rate_available, expected_scale * current_rate);
}
//! \brief Test checkGroupConstraintsProd() for group MANI with no guide rate at PLAT level between MANI and FIELD
//!
//! In this test, since PLAT has no guide rate and not zero group-controlled wells (it has 1 group-controlled well,
//! namely WELL-B via MANI-2), local_reduction_level stays at 0 and the addback happens at FIELD level instead of
//! at PLAT level.
//! \code
//! FIELD (ORAT = 10000, local_reduction_level=0)
//! ├── PLAT (E=0.9, FLD, NO GUIDERATE, GCW=1)
//! │ ├── MANI (E=0.8, IDV, ORAT, GUIDERATE=8000, GCW=2)
//! │ │ ├── WELL-A (RATE=6000, POT=6000, GRUP)
//! │ │ └── WELL-A2 (RATE=4000, POT=4000, GRUP)
//! │ └── MANI-2 (E=0.75, FLD, 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(TestGroupHigherConstraintsProdWithoutGuideRate)
{
// Configuration: No guide rate for PLAT (local_reduction_level = 0)
commonSetupForAllTests(
/*plat_has_guide_rate=*/false,
/*mani_control =*/Opm::Group::ProductionCMode::ORAT,
/*well_a_control =*/Opm::Well::ProducerCMode::GRUP,
/*well_a2_control =*/Opm::Well::ProducerCMode::GRUP
);
auto& gsh = groupStateHelper();
auto& gs = groupState();
// Verify PLAT has no guide rate
BOOST_CHECK(!guideRate().has("PLAT"));
// Verify reduction rates
// Without guide rate at PLAT, MANI's reduction bubbles up to FIELD
// But PLAT still has group-controlled wells via MANI-2 (FLD control)
// -> So PLAT goes to else branch in updateGroupTargetReductionRecursive_()
// -> Since PLAT has no guide rate, MANI's reduction propagates to FIELD
const auto& field_red = gs.production_reduction_rates("FIELD");
// FIELD reduction = E_PLAT * E_MANI * 10000 (MANI has individual control)
const double expected_field_reduction = ePlat() * eMani() * maniTotalRateMetric();
checkRate(metric_rate(field_red[oilPhasePos()]), expected_field_reduction);
// Call checkGroupConstraintsProd()
auto mani_rates = maniRatesArray();
auto resv_coeff = resvCoeff();
const double initial_efficiency = eMani();
auto [constraint_violated, scale] = gsh.checkGroupConstraintsProd(
"MANI",
"PLAT",
platGroup(),
mani_rates.data(),
initial_efficiency,
resv_coeff,
/*check_guide_rate=*/false
);
// With local_reduction_level = 0 (no guide rate at PLAT):
// - Chain: [FIELD, PLAT, MANI]
// - MANI total = 10000 SM3/day (WELL-A=6000 + WELL-A2=4000)
// - efficiency_factor = E_MANI * E_PLAT = 0.8 * 0.9 = 0.72
//
// At FIELD (ii=0):
// - reduction = E_PLAT * E_MANI * 10000 = 0.9 * 0.8 * 10000 = 7200
// - addback = current_rate * E_MANI * E_PLAT = 10000 * 0.72 = 7200
// - target = 10000 - 7200 + 7200 = 10000
// - PLAT fraction: Without guide rate, PLAT uses production-based fraction
// PLAT production = 9500, PLAT-2 production = 2550 (via E_PLAT2 * 3000)
//
// The algorithm produces scale = 0.9009 and constraint_violated = true.
//
// This differs from test TestGroupHigherConstraintsProdWithGuideRate (scale = 0.922) because:
// - Without PLAT guide rate, the fraction calculation changes
// - PLAT-2 still has guide rate (2550), so it participates in FIELD fraction
// - PLAT without guide rate may get a different fraction or be excluded
//
const double expected_scale = 0.90090090090090058;
BOOST_CHECK(constraint_violated); // scale < 1.0 means constraint violated
checkAlgo(scale, expected_scale);
}
//! \brief Test getWellGroupTargetProducer() for a WELL-A (under individual control)
//!
//! In this test, WELL-A is under individual ORAT control, so the addback is applied because the well's rate
//! is included in the local reduction. The controlling group is FIELD (the first parent with individual control)
//! and the addback happens at MANI level.
//! \code
//! FIELD (ORAT = 10000)
//! ├── PLAT (E=0.9, GUIDERATE=9500, GCW=1)
//! │ ├── MANI (E=0.8, FLD, GUIDERATE=8000, GCW=2)
//! │ │ ├── WELL-A (RATE=6000, POT=6000, ORAT)
//! │ │ └── WELL-A2 (RATE=4000, POT=4000, GRUP)
//! │ └── MANI-2 (E=0.75, FLD, 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(TestWellGroupTargetProducerIndividualControl)
{
// Configuration: WELL-A under individual ORAT control, MANI follows parent
commonSetupForAllTests(/*plat_has_guide_rate=*/true,
/*mani_control=*/Opm::Group::ProductionCMode::FLD,
/*well_a_control=*/Opm::Well::ProducerCMode::ORAT,
/*well_a2_control=*/Opm::Well::ProducerCMode::GRUP);
auto& gsh = groupStateHelper();
auto& ws = wellState();
// Verify WELL-A is NOT under GRUP control
BOOST_CHECK(!ws.isProductionGrup("WELL-A"));
BOOST_CHECK(ws.isProductionGrup("WELL-A2"));
BOOST_CHECK(ws.isProductionGrup("WELL-C"));
// Get well efficiency factor (default 1.0)
const auto& well_a_ecl = schedule().getWell("WELL-A", reportStepIdx());
const double well_a_eff = well_a_ecl.getEfficiencyFactor();
checkClose(well_a_eff, 1.0);
// Call getWellGroupTargetProducer
auto well_a_rates = wellARatesArray();
auto resv_coeff = resvCoeff();
auto target_opt = gsh.getWellGroupTargetProducer(
"WELL-A",
"MANI",
maniGroup(),
well_a_rates.data(),
well_a_eff,
resv_coeff
);
BOOST_REQUIRE(target_opt.has_value());
// For getWellGroupTargetProducer with WELL-A under individual control:
// - Chain: [FIELD, PLAT, MANI, WELL-A]
// - efficiency_factor accumulates: E_WELL * E_MANI * E_PLAT = 1.0 * 0.8 * 0.9 = 0.72
// - local_reduction_level = 2 (MANI has guide rate AND GCW=1)
// (Both PLAT and MANI have guide rates and GCW > 0, so loop ends at ii=2)
// - should_addback = true (WELL-A is not under GRUP)
//
// Reduction at MANI = E_WELL * 6000 = 6000 (WELL-A is individual, E_WELL=1.0)
// Addback for WELL-A = 6000 * E_WELL = 6000 (at local_reduction_level=2)
// Net effect: reduction - addback = 0 (they cancel out!)
//
// WELL-A fraction in MANI = 6000 / (6000 + 4000) = 0.6
// PLAT fraction at FIELD = 9500 / (9500 + 2550) ≈ 0.788
// MANI fraction at PLAT = 8000 / (8000 + 1500) ≈ 0.842
// (MANI has FLD control in this test, so it competes with MANI-2)
//
// Since reduction and addback cancel out, the result is just:
// Final: target / efficiency = 10000 * (9500/12050) * (8000/9500) * 0.6 / 0.72
// = 10000 * 8000 / 12050 * 0.6 / 0.72 = 5532.50
const double target = target_opt->target_value;
const double plat_fraction = platGuideRateMetric() / (platGuideRateMetric() + plat2GuideRateMetric());
const double mani_fraction = maniGuideRateMetric() / (maniGuideRateMetric() + mani2GuideRateMetric());
const double well_a_fraction = 6.0 / 10.0;
const double expected_target_metric = 10000.0 * plat_fraction * mani_fraction * well_a_fraction / 0.72; // 5532.50
checkAlgo(metric_rate(target), expected_target_metric);
}
//! \brief Test getWellGroupTargetProducer for a well under GRUP control
//!
//! When a well is under GRUP control, no addback is applied because
//! the well's rate is not individually counted in the reduction.
//! In this test, WELL-A is under GRUP control, the controlling group is FIELD, and no addback is applied
//! at the MANI level because the well is under GRUP control.
//! \code
//! FIELD (ORAT = 10000)
//! ├── PLAT (E=0.9, FLD, GUIDERATE=9500, GCW=1)
//! │ ├── MANI (E=0.8, FLD, GUIDERATE=8000, GCW=2)
//! │ │ ├── WELL-A (RATE=6000, POT=6000, GRUP)
//! │ │ └── WELL-A2 (RATE=4000, POT=4000, GRUP)
//! │ └── MANI-2 (E=0.75, FLD, 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(TestWellGroupTargetProducerGrupControl)
{
// Configuration: All wells under GRUP control, MANI follows parent
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
);
auto& gsh = groupStateHelper();
auto& ws = wellState();
// Verify all wells are under GRUP control
BOOST_CHECK(ws.isProductionGrup("WELL-A"));
BOOST_CHECK(ws.isProductionGrup("WELL-A2"));
BOOST_CHECK(ws.isProductionGrup("WELL-C"));
// Get well efficiency factor (default 1.0)
const auto& well_a_ecl = schedule().getWell("WELL-A", reportStepIdx());
const double well_a_eff = well_a_ecl.getEfficiencyFactor();
checkClose(well_a_eff, 1.0);
// Call getWellGroupTargetProducer
auto well_a_rates = wellARatesArray();
auto resv_coeff = resvCoeff();
auto target_opt = gsh.getWellGroupTargetProducer(
"WELL-A",
"MANI",
maniGroup(),
well_a_rates.data(),
well_a_eff,
resv_coeff
);
BOOST_REQUIRE(target_opt.has_value());
const double target = target_opt->target_value;
// For a well under GRUP control:
// - local_reduction_level = 2 (MANI has guide rate AND GCW=2)
// - No addback (should_addback = false because isProductionGrup is true)
// - No reduction either (both WELL-A and WELL-A2 are under GRUP)
// - The fraction used is for the well within its group
//
// With MANI under FLD control, MANI-2 as sibling (also FLD):
// - MANI fraction at PLAT = MANI_guide / (MANI_guide + MANI2_guide) = 8000/9500 ≈ 0.842
// - PLAT fraction at FIELD = 9500 / (9500 + 2550) ≈ 0.788
// - WELL-A fraction in MANI = 6000 / (6000 + 4000) = 0.6
//
// Since reduction = 0 and addback = 0, the result is just:
// Final: target / efficiency = 10000 * (9500/12050) * (8000/9500) * 0.6 / 0.72
// = 10000 * 8000 / 12050 * 0.6 / 0.72 ≈ 5532.50
//
// NOTE: This produces the same result as test TestWellGroupTargetProducerIndividualControl because:
// - TestWellGroupTargetProducerIndividualControl: reduction = 6000, addback = 6000 → net effect = 0
// - TestWellGroupTargetProducerGrupControl: reduction = 0, addback = 0 → net effect = 0
// In both cases, the reduction and addback cancel out,
// so the final target depends only on the fractions and efficiency factor.
const double plat_fraction = platGuideRateMetric() / (platGuideRateMetric() + plat2GuideRateMetric());
const double mani_fraction = maniGuideRateMetric() / (maniGuideRateMetric() + mani2GuideRateMetric());
const double well_a_fraction = 6.0 / 10.0;
const double expected_target_metric = 10000.0 * plat_fraction * mani_fraction * well_a_fraction / 0.72;
checkAlgo(metric_rate(target), expected_target_metric);
}
//! \brief Test getWellGroupTargetProducer with sibling well under individual control
//!
//! When WELL-A is under GRUP control but WELL-A2 is under individual ORAT control,
//! the reduction includes WELL-A2's rate but not WELL-A's. Since WELL-A is under GRUP,
//! no addback is applied. This creates a scenario where reduction != addback, producing
//! a different result than TestWellGroupTargetProducerIndividualControl and
//! TestWellGroupTargetProducerGrupControl.
//! \code
//! FIELD (ORAT = 10000)
//! ├── PLAT (E=0.9, FLD, GUIDERATE=9500, GCW=1)
//! │ ├── MANI (E=0.8, FLD, GUIDERATE=8000, GCW=2)
//! │ │ ├── WELL-A (RATE=6000, POT=6000, GRUP)
//! │ │ └── WELL-A2 (RATE=4000, POT=4000, ORAT)
//! │ └── MANI-2 (E=0.75, FLD, 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(TestWellGroupTargetProducerWithSiblingIndividualControl)
{
// Configuration: WELL-A under GRUP, WELL-A2 under individual ORAT control
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::ORAT);
auto& gsh = groupStateHelper();
auto& ws = wellState();
// Verify control modes
BOOST_CHECK(ws.isProductionGrup("WELL-A")); // WELL-A under GRUP
BOOST_CHECK(!ws.isProductionGrup("WELL-A2")); // WELL-A2 under individual control
BOOST_CHECK(ws.isProductionGrup("WELL-C"));
// Get well efficiency factor (default 1.0)
const auto& well_a_ecl = schedule().getWell("WELL-A", reportStepIdx());
const double well_a_eff = well_a_ecl.getEfficiencyFactor();
checkClose(well_a_eff, 1.0);
// Call getWellGroupTargetProducer for WELL-A
auto well_a_rates = wellARatesArray();
auto resv_coeff = resvCoeff();
auto target_opt = gsh.getWellGroupTargetProducer(
"WELL-A",
"MANI",
maniGroup(),
well_a_rates.data(),
well_a_eff,
resv_coeff
);
BOOST_REQUIRE(target_opt.has_value());
const double target = target_opt->target_value;
// For getWellGroupTargetProducer() with WELL-A under GRUP but WELL-A2 under individual:
// - Chain: [FIELD, PLAT, MANI, WELL-A]
// - efficiency_factor = E_WELL * E_MANI * E_PLAT = 1.0 * 0.8 * 0.9 = 0.72
// - local_reduction_level = 2 (MANI has guide rate AND GCW=1)
// - should_addback = false (WELL-A is under GRUP)
//
// Reduction at MANI = E_WELL * 4000 = 4000 (only WELL-A2 is individual, E_WELL=1.0)
// Addback for WELL-A = 0 (WELL-A is under GRUP, no addback)
//
// Fractions:
// - PLAT fraction at FIELD = 9500 / 12050 ≈ 0.788
// - MANI fraction at PLAT = 8000 / 9500 ≈ 0.842
// - WELL-A fraction at MANI = 1.0 because WELL-A is the always_included_child
// (When is_grup=true, fraction_fn uses chain[ii+1] as always_included_child,
// so WELL-A always gets fraction 1.0 regardless of sibling wells)
//
// Algorithm (with local_reduction_level = 2):
// ii=0 (FIELD): reduction = 0, target = 10000 * 0.788 = 7884.65
// ii=1 (PLAT): reduction = 0, target = 7884.65 * 0.842 = 6640.07
// ii=2 (MANI): reduction = 4000 (WELL-A2 under ORAT)
// target = 6640.07 - 4000 = 2640.07
// No addback (WELL-A is under GRUP)
// target *= 1.0 (WELL-A is always_included_child)
//
// Final: target / efficiency = 2640.07 / 0.72 = 3666.76 ≈ 3665.28
//
// This is DIFFERENT from TestWellGroupTargetProducerIndividualControl (5532.50)
// and TestWellGroupTargetProducerGrupControl (5532.50) because:
// - Reduction at MANI = 4000 (WELL-A2's rate)
// - Addback = 0 (WELL-A is under GRUP)
// - Net effect = -4000 (target reduced)
//
// Expected value derived from actual algorithm output:
const double expected_target_metric = 3665.2835408022129;
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:
// gsh.getAutoChokeGroupProductionTargetRate(bottom_group=MANI, group=MANI, 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();
auto [target_rate_si, control_mode] = gsh.getAutoChokeGroupProductionTargetRate(
maniGroup(), // bottom_group
maniGroup(), // group (starts recursion here, recurses up to controlling group)
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();
auto [target_rate_si, control_mode] = gsh.getAutoChokeGroupProductionTargetRate(
maniGroup(), // bottom_group
maniGroup(), // group (starts recursion here)
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()