From fb0344eb652a52006205244a04589c026ba7b230 Mon Sep 17 00:00:00 2001 From: Kai Bao Date: Fri, 7 Aug 2026 21:36:28 +0200 Subject: [PATCH 1/5] supporting the item 14 of WECON keyword The minimum liquid rate limit was already checked, but it looked up the oil and water rates unconditionally. PhaseUsageInfo::canonicalToActivePhaseIdx throws for an inactive phase, so a run without one of those phases aborted with a std::logic_error instead of applying the limit. Sum only the phases the run actually has, and skip the check when it has neither. The same guard is applied to the minimum oil and gas rate limits (items 2 and 3), which could abort the same way, and item 14 is no longer reported as unsupported. --- .../utils/PartiallySupportedFlowKeywords.cpp | 1 - opm/simulators/wells/WellTest.cpp | 30 +++++++++++++------ opm/simulators/wells/WellTest.hpp | 4 +++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/opm/simulators/utils/PartiallySupportedFlowKeywords.cpp b/opm/simulators/utils/PartiallySupportedFlowKeywords.cpp index 7193cfc9d11..639b346c396 100644 --- a/opm/simulators/utils/PartiallySupportedFlowKeywords.cpp +++ b/opm/simulators/utils/PartiallySupportedFlowKeywords.cpp @@ -673,7 +673,6 @@ partiallySupported() { {11,{true, allow_values {}, "WECON(WCUT2): Feature not supported and should be defaulted"}}, // WELOPEN {13,{true, allow_values {}, "WECON(GLR): Feature not supported and should be defaulted"}}, // GLR - {14,{true, allow_values {}, "WECON(LRAT): Feature not supported and should be defaulted"}}, // LRAT {15,{true, allow_values {}, "WECON(TEMP): Feature not supported and should be defaulted"}}, // TEMP {16,{true, allow_values {}, "WECON(RESV): Feature not supported and should be defaulted"}}, // RESV }, diff --git a/opm/simulators/wells/WellTest.cpp b/opm/simulators/wells/WellTest.cpp index a66f3ce0cca..95ecf085651 100644 --- a/opm/simulators/wells/WellTest.cpp +++ b/opm/simulators/wells/WellTest.cpp @@ -199,7 +199,16 @@ checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, DeferredLogger& deferred_logger) const { const auto& pu = well_.phaseUsage(); - if (econ_production_limits.onMinOilRate()) { + + // A limit on a phase that the run does not have is ignored: there is no rate + // to compare it against, and asking for the index of an inactive phase + // throws. Guarding here also keeps the liquid limit below from reading the + // entry of a phase that is not present. + const bool oil_active = pu.phaseIsActive(IndexTraits::oilPhaseIdx); + const bool gas_active = pu.phaseIsActive(IndexTraits::gasPhaseIdx); + const bool water_active = pu.phaseIsActive(IndexTraits::waterPhaseIdx); + + if (econ_production_limits.onMinOilRate() && oil_active) { const int oil_pos = pu.canonicalToActivePhaseIdx(IndexTraits::oilPhaseIdx); const Scalar oil_rate = rates_or_potentials[oil_pos]; const Scalar min_oil_rate = econ_production_limits.minOilRate(); @@ -208,7 +217,7 @@ checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, } } - if (econ_production_limits.onMinGasRate() ) { + if (econ_production_limits.onMinGasRate() && gas_active) { const int gas_pos = pu.canonicalToActivePhaseIdx(IndexTraits::gasPhaseIdx); const Scalar gas_rate = rates_or_potentials[gas_pos]; const Scalar min_gas_rate = econ_production_limits.minGasRate(); @@ -217,12 +226,15 @@ checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, } } - if (econ_production_limits.onMinLiquidRate() ) { - const int oil_pos = pu.canonicalToActivePhaseIdx(IndexTraits::oilPhaseIdx); - const int water_pos = pu.canonicalToActivePhaseIdx(IndexTraits::waterPhaseIdx); - const Scalar oil_rate = rates_or_potentials[oil_pos]; - const Scalar water_rate = rates_or_potentials[water_pos]; - const Scalar liquid_rate = oil_rate + water_rate; + if (econ_production_limits.onMinLiquidRate() && (oil_active || water_active)) { + // The liquid rate is the sum of the oil and water rates; either phase + // may be absent from the run (e.g. an oil-gas case). + Scalar liquid_rate = 0.0; + for (const auto phase : {IndexTraits::oilPhaseIdx, IndexTraits::waterPhaseIdx}) { + if (pu.phaseIsActive(phase)) { + liquid_rate += rates_or_potentials[pu.canonicalToActivePhaseIdx(phase)]; + } + } const Scalar min_liquid_rate = econ_production_limits.minLiquidRate(); if (std::abs(liquid_rate) < min_liquid_rate) { return true; @@ -379,7 +391,7 @@ updateWellTestStateEconomic(const SingleWellState& ws, return; } - // flag to check if the mim oil/gas rate limit is violated + // flag to check if the min oil/gas/liquid rate limit is violated bool rate_limit_violated = false; const auto& quantity_limit = econ_production_limits.quantityLimit(); diff --git a/opm/simulators/wells/WellTest.hpp b/opm/simulators/wells/WellTest.hpp index aef44cb1958..ab3d363b77e 100644 --- a/opm/simulators/wells/WellTest.hpp +++ b/opm/simulators/wells/WellTest.hpp @@ -160,6 +160,10 @@ class WellTest { const UnitSystem::measure ratio_measure, RatioLimitCheckReport& report) const; + //! \brief Check the minimum surface rate limits (WECON items 2, 3 and 14) + //! against \p rates_or_potentials, which holds the well's surface + //! rates or, when WECON item 10 is POTN, its surface potentials. + //! Limits on a phase that is not active in the run are ignored. bool checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, const std::vector& rates_or_potentials, DeferredLogger& deferred_logger) const; From f89af73235019a7f302428558da476ce8756b8c1 Mon Sep 17 00:00:00 2001 From: Kai Bao Date: Fri, 7 Aug 2026 21:37:48 +0200 Subject: [PATCH 2/5] supporting the item 16 of WECON keyword Convert the surface rates, or the potentials when item 10 is POTN, to a reservoir voidage rate with the same rate converter used for the voidage rates in the well state, and shut the well when it falls below the limit. Replaces the warning that the limit was not supported. The check is a further case in checkRateEconLimits(), so it inherits the POTN double-check against the actual rates and the zero group target guard that already apply to the surface rate limits. --- .../utils/PartiallySupportedFlowKeywords.cpp | 1 - .../wells/WellInterfaceFluidSystem.cpp | 17 ++++++++++++++ .../wells/WellInterfaceFluidSystem.hpp | 2 ++ opm/simulators/wells/WellInterfaceGeneric.hpp | 6 +++++ opm/simulators/wells/WellTest.cpp | 22 +++++++++++-------- opm/simulators/wells/WellTest.hpp | 13 ++++++----- 6 files changed, 45 insertions(+), 16 deletions(-) diff --git a/opm/simulators/utils/PartiallySupportedFlowKeywords.cpp b/opm/simulators/utils/PartiallySupportedFlowKeywords.cpp index 639b346c396..d3b9f755460 100644 --- a/opm/simulators/utils/PartiallySupportedFlowKeywords.cpp +++ b/opm/simulators/utils/PartiallySupportedFlowKeywords.cpp @@ -674,7 +674,6 @@ partiallySupported() {11,{true, allow_values {}, "WECON(WCUT2): Feature not supported and should be defaulted"}}, // WELOPEN {13,{true, allow_values {}, "WECON(GLR): Feature not supported and should be defaulted"}}, // GLR {15,{true, allow_values {}, "WECON(TEMP): Feature not supported and should be defaulted"}}, // TEMP - {16,{true, allow_values {}, "WECON(RESV): Feature not supported and should be defaulted"}}, // RESV }, }, { diff --git a/opm/simulators/wells/WellInterfaceFluidSystem.cpp b/opm/simulators/wells/WellInterfaceFluidSystem.cpp index 5eef2885654..939f56b1f91 100644 --- a/opm/simulators/wells/WellInterfaceFluidSystem.cpp +++ b/opm/simulators/wells/WellInterfaceFluidSystem.cpp @@ -40,6 +40,8 @@ #include #include +#include + namespace Opm { @@ -151,6 +153,21 @@ calculateReservoirRates(const bool use_well_bhp_temperature, SingleWellState +typename FluidSystem::Scalar +WellInterfaceFluidSystem:: +totalReservoirVoidageRate(const std::vector& surface_rates) const +{ + std::vector voidage_rates(surface_rates.size(), 0.0); + this->rateConverter_ + .calcReservoirVoidageRates(/*fipreg*/ 0, + this->pvtRegionIdx_, + surface_rates, + voidage_rates); + + return std::accumulate(voidage_rates.begin(), voidage_rates.end(), Scalar{0.0}); +} + template bool WellInterfaceFluidSystem:: diff --git a/opm/simulators/wells/WellInterfaceFluidSystem.hpp b/opm/simulators/wells/WellInterfaceFluidSystem.hpp index 68d4313a32d..8eb25501ad2 100644 --- a/opm/simulators/wells/WellInterfaceFluidSystem.hpp +++ b/opm/simulators/wells/WellInterfaceFluidSystem.hpp @@ -70,6 +70,8 @@ class WellInterfaceFluidSystem : public WellInterfaceGeneric& surface_rates) const override; + protected: WellInterfaceFluidSystem(const Well& well, const ParallelWellInfo& parallel_well_info, diff --git a/opm/simulators/wells/WellInterfaceGeneric.hpp b/opm/simulators/wells/WellInterfaceGeneric.hpp index 4033be9b414..f31fbc8b479 100644 --- a/opm/simulators/wells/WellInterfaceGeneric.hpp +++ b/opm/simulators/wells/WellInterfaceGeneric.hpp @@ -224,6 +224,12 @@ class WellInterfaceGeneric { virtual Scalar connectionDensity(const int globalConnIdx, const int openConnIdx) const = 0; + //! \brief The total reservoir voidage rate corresponding to the given + //! vector of surface phase rates (or potentials), converted with + //! the region average PVT properties also used for the voidage + //! rates in the well state. + virtual Scalar totalReservoirVoidageRate(const std::vector& surface_rates) const = 0; + void addPerforations(const std::vector& perfs); protected: diff --git a/opm/simulators/wells/WellTest.cpp b/opm/simulators/wells/WellTest.cpp index 95ecf085651..5e806962580 100644 --- a/opm/simulators/wells/WellTest.cpp +++ b/opm/simulators/wells/WellTest.cpp @@ -195,8 +195,7 @@ checkMaxRatioLimit(const SingleWellState& ws, template bool WellTest:: checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, - const std::vector& rates_or_potentials, - DeferredLogger& deferred_logger) const + const std::vector& rates_or_potentials) const { const auto& pu = well_.phaseUsage(); @@ -242,7 +241,14 @@ checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, } if (econ_production_limits.onMinReservoirFluidRate()) { - deferred_logger.warning("NOT_SUPPORTING_MIN_RESERVOIR_FLUID_RATE", "Minimum reservoir fluid production rate limit is not supported yet"); + // The net reservoir fluid (voidage) production rate corresponding to the + // surface rates or potentials, converted with the same region average + // PVT properties as the voidage rates in the well state. + const Scalar voidage_rate = + std::abs(well_.totalReservoirVoidageRate(rates_or_potentials)); + if (voidage_rate < econ_production_limits.minReservoirFluidRate()) { + return true; + } } return false; @@ -391,28 +397,26 @@ updateWellTestStateEconomic(const SingleWellState& ws, return; } - // flag to check if the min oil/gas/liquid rate limit is violated + // flag to check if the min oil/gas/liquid/reservoir-fluid rate limit is violated bool rate_limit_violated = false; const auto& quantity_limit = econ_production_limits.quantityLimit(); if (econ_production_limits.onAnyRateLimit()) { if (quantity_limit == WellEconProductionLimits::QuantityLimit::POTN) { rate_limit_violated = this->checkRateEconLimits(econ_production_limits, - ws.well_potentials, - deferred_logger); + ws.well_potentials); // Due to instability of the bhpFromThpLimit code the potentials are sometimes wrong // this can lead to premature shutting of wells due to rate limits of the potentials. // Since rates are supposed to be less or equal to the potentials, we double-check // that also the rate limit is violated before shutting the well. if (rate_limit_violated) rate_limit_violated = this->checkRateEconLimits(econ_production_limits, - ws.surface_rates, - deferred_logger); + ws.surface_rates); } else { if (!zero_group_target) { rate_limit_violated - = this->checkRateEconLimits(econ_production_limits, ws.surface_rates, deferred_logger); + = this->checkRateEconLimits(econ_production_limits, ws.surface_rates); } } } diff --git a/opm/simulators/wells/WellTest.hpp b/opm/simulators/wells/WellTest.hpp index ab3d363b77e..91e85b756d0 100644 --- a/opm/simulators/wells/WellTest.hpp +++ b/opm/simulators/wells/WellTest.hpp @@ -160,13 +160,14 @@ class WellTest { const UnitSystem::measure ratio_measure, RatioLimitCheckReport& report) const; - //! \brief Check the minimum surface rate limits (WECON items 2, 3 and 14) - //! against \p rates_or_potentials, which holds the well's surface - //! rates or, when WECON item 10 is POTN, its surface potentials. - //! Limits on a phase that is not active in the run are ignored. + //! \brief Check the minimum production rate limits (WECON items 2, 3, 14 + //! and 16) against \p rates_or_potentials, which holds the well's + //! surface rates or, when WECON item 10 is POTN, its surface + //! potentials. Item 16 applies to the reservoir voidage rate these + //! surface rates correspond to. Limits on a phase that is not active + //! in the run are ignored. bool checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, - const std::vector& rates_or_potentials, - DeferredLogger& deferred_logger) const; + const std::vector& rates_or_potentials) const; //! \brief Check all active ratio limits, ignoring \p excluded_completions //! (completions already closed by the ongoing workover event). From 224f931c8662d03779ce31a5903ea51261e898f2 Mon Sep 17 00:00:00 2001 From: Kai Bao Date: Fri, 7 Aug 2026 21:40:19 +0200 Subject: [PATCH 3/5] WECON: report the violated rate limit in the closing message The message only said that a rate economic limit was hit. Name the quantity that fell below its limit and print both values, so the log says which of the WECON limits closed the well. The layout now matches the ratio-limit workover messages, and the "at time ... (date = ...)" clause those and the CECON messages build inline moves to a shared helper. --- .../wells/EconomicLimitsMessage.hpp | 15 ++ opm/simulators/wells/WellTest.cpp | 129 ++++++++++++------ opm/simulators/wells/WellTest.hpp | 32 ++++- 3 files changed, 135 insertions(+), 41 deletions(-) diff --git a/opm/simulators/wells/EconomicLimitsMessage.hpp b/opm/simulators/wells/EconomicLimitsMessage.hpp index be4854d3649..8bf45881fc3 100644 --- a/opm/simulators/wells/EconomicLimitsMessage.hpp +++ b/opm/simulators/wells/EconomicLimitsMessage.hpp @@ -22,6 +22,8 @@ #include +#include + #include #include @@ -42,6 +44,19 @@ inline std::string economicLimitDateString(const std::time_t start_time, const d return fmt::format("{:%d-%b-%Y}", fmt::gmtime(cur_time)); } +//! \brief The "at time ... (date = ...)" clause shared by the well (WECON) and +//! connection (CECON) economic-limit closing messages, so that all of +//! them time-stamp the closure the same way. +inline std::string economicLimitWhenString(const UnitSystem& unit_system, + const std::time_t start_time, + const double sim_time) +{ + return fmt::format("at time {:.2f} {} (date = {})", + unit_system.from_si(UnitSystem::measure::time, sim_time), + unit_system.name(UnitSystem::measure::time), + economicLimitDateString(start_time, sim_time)); +} + //! \brief Separator line used to frame economic-limit workover messages. //! Built once and returned by reference (all callers use the same line). inline const std::string& economicLimitMessageSeparator() diff --git a/opm/simulators/wells/WellTest.cpp b/opm/simulators/wells/WellTest.cpp index 5e806962580..ce1f3cc2100 100644 --- a/opm/simulators/wells/WellTest.cpp +++ b/opm/simulators/wells/WellTest.cpp @@ -50,6 +50,19 @@ namespace Opm { +namespace { + +//! \brief The unit of \p measure as a message suffix, i.e. preceded by a space, +//! or an empty string for the dimensionless quantities (e.g. water cut). +std::string unitSuffix(const UnitSystem& unit_system, + const UnitSystem::measure measure) +{ + const std::string_view unit = unit_system.name(measure); + return unit.empty() ? std::string{} : fmt::format(" {}", unit); +} + +} // Anonymous namespace + template template bool WellTest:: @@ -195,7 +208,8 @@ checkMaxRatioLimit(const SingleWellState& ws, template bool WellTest:: checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, - const std::vector& rates_or_potentials) const + const std::vector& rates_or_potentials, + RateLimitCheckReport& report) const { const auto& pu = well_.phaseUsage(); @@ -207,21 +221,37 @@ checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, const bool gas_active = pu.phaseIsActive(IndexTraits::gasPhaseIdx); const bool water_active = pu.phaseIsActive(IndexTraits::waterPhaseIdx); + // Record the violated limit for the closing message. The rates are stored + // with production negative, so the magnitude is what the limit applies to. + auto violates = [&report](const std::string_view quantity_name, + const UnitSystem::measure rate_measure, + const Scalar rate_value, + const Scalar rate_limit) + { + report.quantity_name = quantity_name; + report.rate_measure = rate_measure; + report.rate_value = rate_value; + report.rate_limit = rate_limit; + return true; + }; + if (econ_production_limits.onMinOilRate() && oil_active) { const int oil_pos = pu.canonicalToActivePhaseIdx(IndexTraits::oilPhaseIdx); - const Scalar oil_rate = rates_or_potentials[oil_pos]; + const Scalar oil_rate = std::abs(rates_or_potentials[oil_pos]); const Scalar min_oil_rate = econ_production_limits.minOilRate(); - if (std::abs(oil_rate) < min_oil_rate) { - return true; + if (oil_rate < min_oil_rate) { + return violates("oil", UnitSystem::measure::liquid_surface_rate, + oil_rate, min_oil_rate); } } if (econ_production_limits.onMinGasRate() && gas_active) { const int gas_pos = pu.canonicalToActivePhaseIdx(IndexTraits::gasPhaseIdx); - const Scalar gas_rate = rates_or_potentials[gas_pos]; + const Scalar gas_rate = std::abs(rates_or_potentials[gas_pos]); const Scalar min_gas_rate = econ_production_limits.minGasRate(); - if (std::abs(gas_rate) < min_gas_rate) { - return true; + if (gas_rate < min_gas_rate) { + return violates("gas", UnitSystem::measure::gas_surface_rate, + gas_rate, min_gas_rate); } } @@ -234,9 +264,11 @@ checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, liquid_rate += rates_or_potentials[pu.canonicalToActivePhaseIdx(phase)]; } } + liquid_rate = std::abs(liquid_rate); const Scalar min_liquid_rate = econ_production_limits.minLiquidRate(); - if (std::abs(liquid_rate) < min_liquid_rate) { - return true; + if (liquid_rate < min_liquid_rate) { + return violates("liquid", UnitSystem::measure::liquid_surface_rate, + liquid_rate, min_liquid_rate); } } @@ -246,8 +278,10 @@ checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, // PVT properties as the voidage rates in the well state. const Scalar voidage_rate = std::abs(well_.totalReservoirVoidageRate(rates_or_potentials)); - if (voidage_rate < econ_production_limits.minReservoirFluidRate()) { - return true; + const Scalar min_voidage_rate = econ_production_limits.minReservoirFluidRate(); + if (voidage_rate < min_voidage_rate) { + return violates("reservoir fluid", UnitSystem::measure::rate, + voidage_rate, min_voidage_rate); } } @@ -399,24 +433,35 @@ updateWellTestStateEconomic(const SingleWellState& ws, // flag to check if the min oil/gas/liquid/reservoir-fluid rate limit is violated bool rate_limit_violated = false; + // The violated limit, reported in the closing message below. Only meaningful + // when rate_limit_violated is true. + RateLimitCheckReport rate_report; - const auto& quantity_limit = econ_production_limits.quantityLimit(); + const bool limits_on_potentials = + (econ_production_limits.quantityLimit() == WellEconProductionLimits::QuantityLimit::POTN); if (econ_production_limits.onAnyRateLimit()) { - if (quantity_limit == WellEconProductionLimits::QuantityLimit::POTN) { + if (limits_on_potentials) { rate_limit_violated = this->checkRateEconLimits(econ_production_limits, - ws.well_potentials); + ws.well_potentials, + rate_report); // Due to instability of the bhpFromThpLimit code the potentials are sometimes wrong // this can lead to premature shutting of wells due to rate limits of the potentials. // Since rates are supposed to be less or equal to the potentials, we double-check // that also the rate limit is violated before shutting the well. - if (rate_limit_violated) + // The message reports the potentials, which are the quantity the + // limits are applied to, so the double-check uses its own report. + if (rate_limit_violated) { + RateLimitCheckReport rate_check; rate_limit_violated = this->checkRateEconLimits(econ_production_limits, - ws.surface_rates); + ws.surface_rates, + rate_check); + } } else { if (!zero_group_target) { rate_limit_violated - = this->checkRateEconLimits(econ_production_limits, ws.surface_rates); + = this->checkRateEconLimits(econ_production_limits, ws.surface_rates, + rate_report); } } } @@ -432,18 +477,17 @@ updateWellTestStateEconomic(const SingleWellState& ws, well_test_state.close_well(well_.name(), WellTestConfig::Reason::ECONOMIC, simulation_time); if (write_message_to_opmlog) { - // State when the well stops flowing, as the ratio-limit and CECON - // messages below already do. That instant is the end of the time - // step the limit was detected on, not its start. + // Same layout as the ratio-limit workover messages below: the well, + // the action taken, when it happens and which limit caused it. const std::string_view action = well_.wellEcl().getAutomaticShutIn() ? "shut" : "stopped"; + const std::string& sep = economicLimitMessageSeparator(); deferred_logger.info( - fmt::format("well {} will be {} due to rate economic limit " - "at time {:.2f} {} (date = {})", - well_.name(), action, - unit_system.from_si(UnitSystem::measure::time, simulation_time), - unit_system.name(UnitSystem::measure::time), - economicLimitDateString(start_time, simulation_time))); + fmt::format("{}\nWell {} will be {} {},\nBecause {}.\n{}", + sep, well_.name(), action, + economicLimitWhenString(unit_system, start_time, simulation_time), + rateViolationReason(unit_system, rate_report, limits_on_potentials), + sep)); } // the well is closed, not need to check other limits return; @@ -471,12 +515,7 @@ updateWellTestStateEconomic(const SingleWellState& ws, report.ratio_value, report.ratio_limit); }; if (write_message_to_opmlog) { - when = fmt::format( - "at time {:.2f} {} (date = {})", - unit_system.from_si(UnitSystem::measure::time, simulation_time), - unit_system.name(UnitSystem::measure::time), - economicLimitDateString(start_time, simulation_time)); - + when = economicLimitWhenString(unit_system, start_time, simulation_time); reason = make_reason(ratio_report); } @@ -708,11 +747,8 @@ updateWellTestStateCECON(const SingleWellState& ws, // Build the "at time ... (date = ...)" and ratio-violation clauses that // are shared by all CECON workover messages below. - const std::string when = fmt::format( - "at time {:.2f} {} (date = {})", - unit_system.from_si(UnitSystem::measure::time, simulation_time), - unit_system.name(UnitSystem::measure::time), - economicLimitDateString(start_time, simulation_time)); + const std::string when = + economicLimitWhenString(unit_system, start_time, simulation_time); const std::string reason = ratioViolationReason(unit_system, ratio_name, ratio_measure, ratio_value, ratio_limit); @@ -853,6 +889,21 @@ closeOffendingCompletion(const int offending_completion, return allCompletionsClosed; } +template +std::string WellTest:: +rateViolationReason(const UnitSystem& unit_system, + const RateLimitCheckReport& report, + const bool on_potentials) +{ + const std::string unit_suffix = unitSuffix(unit_system, report.rate_measure); + return fmt::format( + "the {} production {} {:.4e}{} is below the limit {:.4e}{}", + report.quantity_name, + on_potentials ? "potential" : "rate", + unit_system.from_si(report.rate_measure, report.rate_value), unit_suffix, + unit_system.from_si(report.rate_measure, report.rate_limit), unit_suffix); +} + template std::string WellTest:: ratioViolationReason(const UnitSystem& unit_system, @@ -861,9 +912,7 @@ ratioViolationReason(const UnitSystem& unit_system, const Scalar ratio_value, const Scalar ratio_limit) { - const std::string ratio_unit = unit_system.name(ratio_measure); - const std::string unit_suffix = ratio_unit.empty() ? std::string{} - : " " + ratio_unit; + const std::string unit_suffix = unitSuffix(unit_system, ratio_measure); if (std::isinf(ratio_value)) { return fmt::format( "{} is infinite and exceeds the limit {:.4e}{}", diff --git a/opm/simulators/wells/WellTest.hpp b/opm/simulators/wells/WellTest.hpp index 91e85b756d0..e0bde972eec 100644 --- a/opm/simulators/wells/WellTest.hpp +++ b/opm/simulators/wells/WellTest.hpp @@ -100,6 +100,32 @@ class WellTest { Scalar ratio_limit = 0.0; }; + //! \brief Records which minimum rate limit closed the well, together with the + //! offending quantity and the limit it fell below, so that the closing + //! message can name them. Filled in by checkRateEconLimits(), which + //! reports whether a limit was violated through its return value. + struct RateLimitCheckReport { + //! \brief Name of the produced quantity ("oil", "gas", "liquid" or + //! "reservoir fluid"). Always a string literal. + std::string_view quantity_name{}; + UnitSystem::measure rate_measure = UnitSystem::measure::identity; + //! \brief Magnitude of the produced quantity, i.e. the value compared + //! against \c rate_limit. Taken from the well potentials rather + //! than the rates when WECON item 10 is POTN. + Scalar rate_value = 0.0; + Scalar rate_limit = 0.0; + }; + + //! \brief Format the " production rate ... is below the limit ..." + //! clause of the WECON rate-limit closing message. + //! + //! \param on_potentials true when the limits are checked against the well + //! potentials (WECON item 10 is POTN), which the message spells out + //! as a "production potential" instead of a "production rate". + static std::string rateViolationReason(const UnitSystem& unit_system, + const RateLimitCheckReport& report, + const bool on_potentials); + //! \brief Format the " ... exceeds the limit ..." clause shared by the //! WECON and CECON workover messages. //! @@ -166,8 +192,12 @@ class WellTest { //! potentials. Item 16 applies to the reservoir voidage rate these //! surface rates correspond to. Limits on a phase that is not active //! in the run are ignored. + //! + //! \param report describes the first violated limit found; left untouched + //! when no limit is violated. bool checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, - const std::vector& rates_or_potentials) const; + const std::vector& rates_or_potentials, + RateLimitCheckReport& report) const; //! \brief Check all active ratio limits, ignoring \p excluded_completions //! (completions already closed by the ongoing workover event). From d1701f6dcd458aff8bd1f5a260498003ce96e957 Mon Sep 17 00:00:00 2001 From: Kai Bao Date: Fri, 7 Aug 2026 22:46:07 +0200 Subject: [PATCH 4/5] adding regressionTests WECON-02 to test the item 14 of WECON --- regressionTests.cmake | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/regressionTests.cmake b/regressionTests.cmake index aea8bd8d653..02da783b5a9 100644 --- a/regressionTests.cmake +++ b/regressionTests.cmake @@ -1056,6 +1056,25 @@ add_test_compareECLFiles( --enable-tuning=true ) +add_test_compareECLFiles( + CASENAME + wecon_item_14 + FILENAME + WECON-02 + SIMULATOR + flow + DEV_SIMULATOR + flow_blackoil + ABS_TOL + ${abs_tol} + REL_TOL + ${rel_tol} + DIR + wecon_wtest + TEST_ARGS + --enable-tuning=true +) + add_test_compareECLFiles( CASENAME gconinje_resv_gas_01 From 14daa3a5dd48fe0ce3f3b1ada49d7378335d8dcc Mon Sep 17 00:00:00 2001 From: Kai Bao Date: Sat, 8 Aug 2026 00:14:07 +0200 Subject: [PATCH 5/5] WTEST: report when a well test does not re-open a well A test that does not re-open the well discarded the tested state without a word, so the log showed the well being tested and then nothing. Report the outcome from every exit -- unsolvable, inoperable, potentials that could not be computed, or a limit that closed the well again -- with the reason left to the debug log. The exception text of a failed potential calculation was also passed to a format string that had no placeholder for it, and was lost. A shut well is re-tested every interval, so that message stays a plain line, while the one-off re-opening is framed like the shut-in it reverses. Log output only, the simulation results are unchanged. --- .../wells/EconomicLimitsMessage.hpp | 3 +- opm/simulators/wells/WellInterfaceGeneric.cpp | 6 +- opm/simulators/wells/WellInterfaceGeneric.hpp | 13 +-- opm/simulators/wells/WellInterface_impl.hpp | 39 ++++++--- opm/simulators/wells/WellTest.cpp | 85 ++++++++++--------- opm/simulators/wells/WellTest.hpp | 45 +++++----- 6 files changed, 111 insertions(+), 80 deletions(-) diff --git a/opm/simulators/wells/EconomicLimitsMessage.hpp b/opm/simulators/wells/EconomicLimitsMessage.hpp index 8bf45881fc3..71a2217078e 100644 --- a/opm/simulators/wells/EconomicLimitsMessage.hpp +++ b/opm/simulators/wells/EconomicLimitsMessage.hpp @@ -45,8 +45,7 @@ inline std::string economicLimitDateString(const std::time_t start_time, const d } //! \brief The "at time ... (date = ...)" clause shared by the well (WECON) and -//! connection (CECON) economic-limit closing messages, so that all of -//! them time-stamp the closure the same way. +//! connection (CECON) economic-limit messages. inline std::string economicLimitWhenString(const UnitSystem& unit_system, const std::time_t start_time, const double sim_time) diff --git a/opm/simulators/wells/WellInterfaceGeneric.cpp b/opm/simulators/wells/WellInterfaceGeneric.cpp index 66c537b45e8..60a2a17a034 100644 --- a/opm/simulators/wells/WellInterfaceGeneric.cpp +++ b/opm/simulators/wells/WellInterfaceGeneric.cpp @@ -346,14 +346,16 @@ updateWellTestState(const SingleWellState& ws, WellTestState& wellTestState, const UnitSystem& unit_system, const std::time_t start_time, - DeferredLogger& deferred_logger) const + DeferredLogger& deferred_logger, + std::string* closure_reason) const { const WellTest well_test(*this); // updating well test state based on Economic limits for operable wells if (this->isOperableAndSolvable()) { well_test.updateWellTestStateEconomic(ws, simulationTime, writeMessageToOPMLog, during_well_test, wellTestState, - zero_group_target, unit_system, start_time, deferred_logger); + zero_group_target, unit_system, start_time, + deferred_logger, closure_reason); well_test.updateWellTestStateCECON(ws, simulationTime, writeMessageToOPMLog, wellTestState, unit_system, start_time, deferred_logger); } else { diff --git a/opm/simulators/wells/WellInterfaceGeneric.hpp b/opm/simulators/wells/WellInterfaceGeneric.hpp index f31fbc8b479..7df848f0f52 100644 --- a/opm/simulators/wells/WellInterfaceGeneric.hpp +++ b/opm/simulators/wells/WellInterfaceGeneric.hpp @@ -189,6 +189,9 @@ class WellInterfaceGeneric { //! which re-solves the well after every completion closure; false for //! the regular timestep update. See //! WellTest::updateWellTestStateEconomic(). + //! \param closure_reason when non-null, receives the reason a limit closed + //! the well instead of it being logged here. Used by the WTEST re-open + //! testing, where no shut-in actually happens. void updateWellTestState(const SingleWellState& ws, const double& simulationTime, const bool& writeMessageToOPMLog, @@ -197,7 +200,8 @@ class WellInterfaceGeneric { WellTestState& wellTestState, const UnitSystem& unit_system, const std::time_t start_time, - DeferredLogger& deferred_logger) const; + DeferredLogger& deferred_logger, + std::string* closure_reason = nullptr) const; bool isPressureControlled(const WellStateType& well_state) const; @@ -224,10 +228,9 @@ class WellInterfaceGeneric { virtual Scalar connectionDensity(const int globalConnIdx, const int openConnIdx) const = 0; - //! \brief The total reservoir voidage rate corresponding to the given - //! vector of surface phase rates (or potentials), converted with - //! the region average PVT properties also used for the voidage - //! rates in the well state. + //! \brief The total reservoir voidage rate for the given surface phase rates + //! (or potentials), using the region average PVT properties that also + //! produce the voidage rates in the well state. virtual Scalar totalReservoirVoidageRate(const std::vector& surface_rates) const = 0; void addPerforations(const std::vector& perfs); diff --git a/opm/simulators/wells/WellInterface_impl.hpp b/opm/simulators/wells/WellInterface_impl.hpp index 1f9e7ac1d98..112696ea296 100644 --- a/opm/simulators/wells/WellInterface_impl.hpp +++ b/opm/simulators/wells/WellInterface_impl.hpp @@ -426,7 +426,7 @@ namespace Opm unit_system.name(UnitSystem::measure::time), economicLimitDateString(start_time, simulation_time)); - deferred_logger.info(fmt::format(" well {} is being tested {}", this->name(), when)); + deferred_logger.info(fmt::format("Well {} is being tested {}.", this->name(), when)); GroupStateHelperType groupStateHelper_copy = groupStateHelper; WellStateType well_state_copy = well_state; @@ -464,6 +464,21 @@ namespace Opm } WellTestState welltest_state_temp; + // Why a limit closed the well again. Empty when there is no reason to give. + std::string closure_reason; + + // The test can fail to re-open the well at several points below, and they + // all report it through here. The outcome goes to the PRT; the reason only + // to the debug log, because a shut well is re-tested at every interval and + // this message recurs for as long as it stays shut. + auto notReopened = [this, &when, &deferred_logger](const std::string_view reason) + { + deferred_logger.info( + fmt::format("Well {} is not re-opened {}.", this->name(), when)); + if (!reason.empty()) { + deferred_logger.debug(fmt::format("Because {}.", reason)); + } + }; bool testWell = true; // if a well is closed because all completions are closed, we need to check each completion @@ -473,26 +488,21 @@ namespace Opm const std::size_t original_number_closed_completions = welltest_state_temp.num_closed_completions(); bool converged = solveWellForTesting(simulator, groupStateHelper_copy, well_state_copy); if (!converged) { - const auto msg = fmt::format("WTEST: Well {} is not solvable (physical)", this->name()); - deferred_logger.debug(msg); + notReopened("the well equations could not be solved"); return; } updateWellOperability(simulator, well_state_copy, groupStateHelper_copy); if ( !this->isOperableAndSolvable() ) { - const auto msg = fmt::format("WTEST: Well {} is not operable (physical)", this->name()); - deferred_logger.debug(msg); + notReopened("the well is not operable"); return; } std::vector potentials; try { computeWellPotentials(simulator, well_state_copy, groupStateHelper_copy, potentials); } catch (const std::exception& e) { - const std::string msg = fmt::format("well {}: computeWellPotentials() " - "failed during testing for re-opening: ", - this->name(), e.what()); - deferred_logger.info(msg); + notReopened(fmt::format("computing the well potentials failed: {}", e.what())); return; } const int np = well_state_copy.numPhases(); @@ -508,7 +518,8 @@ namespace Opm welltest_state_temp, simulator.vanguard().eclState().getUnits(), simulator.vanguard().schedule().getStartTime(), - deferred_logger); + deferred_logger, + &closure_reason); this->closeCompletions(welltest_state_temp); // Stop testing if the well is closed or shut due to all completions shut @@ -525,8 +536,10 @@ namespace Opm if (!welltest_state_temp.well_is_closed(this->name())) { well_test_state.open_well(this->name()); + const std::string& sep = economicLimitMessageSeparator(); deferred_logger.info( - fmt::format("well {} is re-opened {}", this->name(), when)); + fmt::format("{}\nWell {} is re-opened {}.\n{}", + sep, this->name(), when, sep)); // also reopen completions for (const auto& completion : this->well_ecl_.getCompletions()) { @@ -536,6 +549,10 @@ namespace Opm well_state = well_state_copy; open_times.try_emplace(this->name(), well_test_state.lastTestTime(this->name())); } + else { + // A limit closed the well again: discard the tested state, stay shut. + notReopened(closure_reason); + } } diff --git a/opm/simulators/wells/WellTest.cpp b/opm/simulators/wells/WellTest.cpp index ce1f3cc2100..09f3a13aebf 100644 --- a/opm/simulators/wells/WellTest.cpp +++ b/opm/simulators/wells/WellTest.cpp @@ -213,16 +213,14 @@ checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, { const auto& pu = well_.phaseUsage(); - // A limit on a phase that the run does not have is ignored: there is no rate - // to compare it against, and asking for the index of an inactive phase - // throws. Guarding here also keeps the liquid limit below from reading the - // entry of a phase that is not present. + // A limit on a phase the run does not have is ignored: there is nothing to + // compare it against, and asking for the index of an inactive phase throws. const bool oil_active = pu.phaseIsActive(IndexTraits::oilPhaseIdx); const bool gas_active = pu.phaseIsActive(IndexTraits::gasPhaseIdx); const bool water_active = pu.phaseIsActive(IndexTraits::waterPhaseIdx); - // Record the violated limit for the closing message. The rates are stored - // with production negative, so the magnitude is what the limit applies to. + // Record the violated limit for the closing message. The values are handed + // in as magnitudes, since production is stored negative. auto violates = [&report](const std::string_view quantity_name, const UnitSystem::measure rate_measure, const Scalar rate_value, @@ -256,8 +254,7 @@ checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, } if (econ_production_limits.onMinLiquidRate() && (oil_active || water_active)) { - // The liquid rate is the sum of the oil and water rates; either phase - // may be absent from the run (e.g. an oil-gas case). + // Oil plus water, either of which may be absent (e.g. an oil-gas case). Scalar liquid_rate = 0.0; for (const auto phase : {IndexTraits::oilPhaseIdx, IndexTraits::waterPhaseIdx}) { if (pu.phaseIsActive(phase)) { @@ -273,9 +270,8 @@ checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, } if (econ_production_limits.onMinReservoirFluidRate()) { - // The net reservoir fluid (voidage) production rate corresponding to the - // surface rates or potentials, converted with the same region average - // PVT properties as the voidage rates in the well state. + // Net voidage rate, converted with the same region average PVT + // properties as the voidage rates in the well state. const Scalar voidage_rate = std::abs(well_.totalReservoirVoidageRate(rates_or_potentials)); const Scalar min_voidage_rate = econ_production_limits.minReservoirFluidRate(); @@ -414,7 +410,8 @@ updateWellTestStateEconomic(const SingleWellState& ws, const bool zero_group_target, const UnitSystem& unit_system, const std::time_t start_time, - DeferredLogger& deferred_logger) const + DeferredLogger& deferred_logger, + std::string* closure_reason) const { if (well_.wellIsStopped()) return; @@ -433,8 +430,8 @@ updateWellTestStateEconomic(const SingleWellState& ws, // flag to check if the min oil/gas/liquid/reservoir-fluid rate limit is violated bool rate_limit_violated = false; - // The violated limit, reported in the closing message below. Only meaningful - // when rate_limit_violated is true. + // The violated limit, for the closing message below. Only meaningful when + // rate_limit_violated is true. RateLimitCheckReport rate_report; const bool limits_on_potentials = @@ -448,8 +445,8 @@ updateWellTestStateEconomic(const SingleWellState& ws, // this can lead to premature shutting of wells due to rate limits of the potentials. // Since rates are supposed to be less or equal to the potentials, we double-check // that also the rate limit is violated before shutting the well. - // The message reports the potentials, which are the quantity the - // limits are applied to, so the double-check uses its own report. + // The message reports the potentials, so this second check fills a + // report of its own, which is then discarded. if (rate_limit_violated) { RateLimitCheckReport rate_check; rate_limit_violated = this->checkRateEconLimits(econ_production_limits, @@ -476,18 +473,12 @@ updateWellTestStateEconomic(const SingleWellState& ws, } well_test_state.close_well(well_.name(), WellTestConfig::Reason::ECONOMIC, simulation_time); - if (write_message_to_opmlog) { - // Same layout as the ratio-limit workover messages below: the well, - // the action taken, when it happens and which limit caused it. - const std::string_view action = - well_.wellEcl().getAutomaticShutIn() ? "shut" : "stopped"; - const std::string& sep = economicLimitMessageSeparator(); - deferred_logger.info( - fmt::format("{}\nWell {} will be {} {},\nBecause {}.\n{}", - sep, well_.name(), action, - economicLimitWhenString(unit_system, start_time, simulation_time), - rateViolationReason(unit_system, rate_report, limits_on_potentials), - sep)); + if (write_message_to_opmlog || closure_reason != nullptr) { + // Same layout as the ratio-limit workover messages below. + this->reportEconomicLimitClosure( + economicLimitWhenString(unit_system, start_time, simulation_time), + rateViolationReason(unit_system, rate_report, limits_on_potentials), + write_message_to_opmlog, closure_reason, deferred_logger); } // the well is closed, not need to check other limits return; @@ -506,7 +497,8 @@ updateWellTestStateEconomic(const SingleWellState& ws, const auto workover = econ_production_limits.workover(); // Shared "at time ..." and ratio-violation clauses for the messages - // below; only built when a message will actually be logged. + // below; only built when they will actually be used. + const bool build_message = write_message_to_opmlog || (closure_reason != nullptr); std::string when; std::string reason; auto make_reason = [&unit_system](const RatioLimitCheckReport& report) @@ -514,7 +506,7 @@ updateWellTestStateEconomic(const SingleWellState& ws, return ratioViolationReason(unit_system, report.ratio_name, report.ratio_measure, report.ratio_value, report.ratio_limit); }; - if (write_message_to_opmlog) { + if (build_message) { when = economicLimitWhenString(unit_system, start_time, simulation_time); reason = make_reason(ratio_report); } @@ -575,7 +567,7 @@ updateWellTestStateEconomic(const SingleWellState& ws, } ratio_report = this->checkRatioEconLimits(econ_production_limits, ws, closed_this_event, deferred_logger); - if (write_message_to_opmlog && ratio_report.ratio_limit_violated) { + if (build_message && ratio_report.ratio_limit_violated) { reason = make_reason(ratio_report); } } @@ -584,14 +576,8 @@ updateWellTestStateEconomic(const SingleWellState& ws, case WellEconProductionLimits::EconWorkover::WELL: { well_test_state.close_well(well_.name(), WellTestConfig::Reason::ECONOMIC, simulation_time); - if (write_message_to_opmlog) { - const std::string action = - well_.wellEcl().getAutomaticShutIn() ? "shut" : "stopped"; - const std::string& sep = economicLimitMessageSeparator(); - deferred_logger.info( - fmt::format("{}\nWell {} will be {} {},\nBecause {}.\n{}", - sep, well_.name(), action, when, reason, sep)); - } + this->reportEconomicLimitClosure(when, reason, write_message_to_opmlog, + closure_reason, deferred_logger); break; } case WellEconProductionLimits::EconWorkover::NONE: @@ -889,6 +875,27 @@ closeOffendingCompletion(const int offending_completion, return allCompletionsClosed; } +template +void WellTest:: +reportEconomicLimitClosure(const std::string& when, + const std::string& reason, + const bool write_message_to_opmlog, + std::string* closure_reason, + DeferredLogger& deferred_logger) const +{ + if (write_message_to_opmlog) { + const std::string_view action = + well_.wellEcl().getAutomaticShutIn() ? "shut" : "stopped"; + const std::string& sep = economicLimitMessageSeparator(); + deferred_logger.info( + fmt::format("{}\nWell {} will be {} {},\nBecause {}.\n{}", + sep, well_.name(), action, when, reason, sep)); + } + else if (closure_reason != nullptr) { + *closure_reason = reason; + } +} + template std::string WellTest:: rateViolationReason(const UnitSystem& unit_system, diff --git a/opm/simulators/wells/WellTest.hpp b/opm/simulators/wells/WellTest.hpp index e0bde972eec..42695728b3d 100644 --- a/opm/simulators/wells/WellTest.hpp +++ b/opm/simulators/wells/WellTest.hpp @@ -66,7 +66,8 @@ class WellTest { bool zero_group_target, const UnitSystem& unit_system, const std::time_t start_time, - DeferredLogger& deferred_logger) const; + DeferredLogger& deferred_logger, + std::string* closure_reason = nullptr) const; void updateWellTestStateCECON(const SingleWellState& ws, const double simulation_time, @@ -100,28 +101,32 @@ class WellTest { Scalar ratio_limit = 0.0; }; - //! \brief Records which minimum rate limit closed the well, together with the - //! offending quantity and the limit it fell below, so that the closing - //! message can name them. Filled in by checkRateEconLimits(), which - //! reports whether a limit was violated through its return value. + //! \brief The violated minimum rate limit, for the closing message. struct RateLimitCheckReport { - //! \brief Name of the produced quantity ("oil", "gas", "liquid" or - //! "reservoir fluid"). Always a string literal. + //! \brief "oil", "gas", "liquid" or "reservoir fluid". Always a string + //! literal, so the view outlives the report. std::string_view quantity_name{}; UnitSystem::measure rate_measure = UnitSystem::measure::identity; - //! \brief Magnitude of the produced quantity, i.e. the value compared - //! against \c rate_limit. Taken from the well potentials rather - //! than the rates when WECON item 10 is POTN. + //! \brief Magnitude compared against \c rate_limit, from the potentials + //! rather than the rates when WECON item 10 is POTN. Scalar rate_value = 0.0; Scalar rate_limit = 0.0; }; + //! \brief Report that an economic limit closed the well: to the PRT during the + //! regular update, or into \p closure_reason when a well test is only + //! trying the well out and no shut-in actually happens. + void reportEconomicLimitClosure(const std::string& when, + const std::string& reason, + const bool write_message_to_opmlog, + std::string* closure_reason, + DeferredLogger& deferred_logger) const; + //! \brief Format the " production rate ... is below the limit ..." //! clause of the WECON rate-limit closing message. //! - //! \param on_potentials true when the limits are checked against the well - //! potentials (WECON item 10 is POTN), which the message spells out - //! as a "production potential" instead of a "production rate". + //! \param on_potentials say "production potential" instead of "production + //! rate" (WECON item 10 is POTN). static std::string rateViolationReason(const UnitSystem& unit_system, const RateLimitCheckReport& report, const bool on_potentials); @@ -186,15 +191,13 @@ class WellTest { const UnitSystem::measure ratio_measure, RatioLimitCheckReport& report) const; - //! \brief Check the minimum production rate limits (WECON items 2, 3, 14 - //! and 16) against \p rates_or_potentials, which holds the well's - //! surface rates or, when WECON item 10 is POTN, its surface - //! potentials. Item 16 applies to the reservoir voidage rate these - //! surface rates correspond to. Limits on a phase that is not active - //! in the run are ignored. + //! \brief Check the minimum production rate limits (WECON items 2, 3, 14 and + //! 16) against \p rates_or_potentials, the well's surface rates or, + //! when WECON item 10 is POTN, its potentials. Item 16 applies to the + //! reservoir voidage rate those rates correspond to. Limits on a phase + //! the run does not have are ignored. //! - //! \param report describes the first violated limit found; left untouched - //! when no limit is violated. + //! \param report the first violated limit; untouched when none is violated. bool checkRateEconLimits(const WellEconProductionLimits& econ_production_limits, const std::vector& rates_or_potentials, RateLimitCheckReport& report) const;