Repository: OPM/opm-simulators
Pull request #6092: Support WCONHIST/WCONINJ in ACTIONX
Author: login: blattms
Created: 2025-03-18T21:39:36Z | Merged: 2025-03-26T15:04:10Z

Downstream of OPM/opm-common#4527

Comparison with other simulator looks good for the test cases actionx/ACTIONX_WCONINJH.DATA actionx/ACTIONX_WCONHIST.DATA

I needed to make the container set up for producers/injector to work for the other types.  This here seems to work if the ACTIONX is executed at the end of a report step. I am bit skeptical whether this also works during the time stepping. Maybe there is  a better way?


Diff:
diff --git a/opm/simulators/wells/ALQState.cpp b/opm/simulators/wells/ALQState.cpp
index 50921fe4390..7037c67116b 100644
--- a/opm/simulators/wells/ALQState.cpp
+++ b/opm/simulators/wells/ALQState.cpp
@@ -41,6 +41,12 @@ ALQState<Scalar> ALQState<Scalar>::serializationTestObject()
     return result;
 }
 
+template<class Scalar>
+void ALQState<Scalar>::insert(const std::string& wname)
+{
+    this->current_alq_.emplace(wname, 0.0);
+}
+
 template<class Scalar>
 Scalar ALQState<Scalar>::get(const std::string& wname) const
 {
diff --git a/opm/simulators/wells/ALQState.hpp b/opm/simulators/wells/ALQState.hpp
index 111e4397ed0..404d5118deb 100644
--- a/opm/simulators/wells/ALQState.hpp
+++ b/opm/simulators/wells/ALQState.hpp
@@ -37,6 +37,7 @@ class ALQState
 
     Scalar get(const std::string& wname) const;
     void update_default(const std::string& wname, Scalar value);
+    void insert(const std::string& wname);
     void set(const std::string& wname, Scalar value);
     bool oscillation(const std::string& wname) const;
     void update_count(const std::string& wname, bool increase);
diff --git a/opm/simulators/wells/BlackoilWellModelGeneric.cpp b/opm/simulators/wells/BlackoilWellModelGeneric.cpp
index ac2d84b6c3c..b3c0c8107b9 100644
--- a/opm/simulators/wells/BlackoilWellModelGeneric.cpp
+++ b/opm/simulators/wells/BlackoilWellModelGeneric.cpp
@@ -867,7 +867,10 @@ updateEclWellsConstraints(const int              timeStepIdx,
         auto& ws = this->wellState().well(wellIdx);
 
         ws.updateStatus(well.getStatus());
-        ws.update_targets(well, st);
+        auto switchToProducer = ws.update_type_and_targets(well, st);
+        if (switchToProducer) {
+            this->wellState().switchToProducer(well.name());
+        }
     });
 }
 
diff --git a/opm/simulators/wells/PerfData.cpp b/opm/simulators/wells/PerfData.cpp
index d8d776c82b3..97096a93ff4 100644
--- a/opm/simulators/wells/PerfData.cpp
+++ b/opm/simulators/wells/PerfData.cpp
@@ -51,13 +51,20 @@ PerfData<Scalar>::PerfData(const std::size_t num_perf,
     , ecl_index(num_perf)
 {
     if (injector) {
-        this->water_throughput.resize(num_perf);
-        this->skin_pressure.resize(num_perf);
-        this->water_velocity.resize(num_perf);
-        this->filtrate_data.resize(num_perf);
+        prepareInjectorContainers();
     }
 }
 
+template<class Scalar>
+void PerfData<Scalar>::prepareInjectorContainers()
+{
+    auto num_perf = pressure.size();
+    this->water_throughput.resize(num_perf);
+    this->skin_pressure.resize(num_perf);
+    this->water_velocity.resize(num_perf);
+    this->filtrate_data.resize(num_perf);
+}
+
 template<class Scalar>
 PerfData<Scalar> PerfData<Scalar>::serializationTestObject()
 {
diff --git a/opm/simulators/wells/PerfData.hpp b/opm/simulators/wells/PerfData.hpp
index 1c00251077d..c3b99d3938e 100644
--- a/opm/simulators/wells/PerfData.hpp
+++ b/opm/simulators/wells/PerfData.hpp
@@ -48,6 +48,11 @@ class PerfData
     bool empty() const;
     bool try_assign(const PerfData& other);
 
+    /// \brief Make containers valid for injectors
+    ///
+    /// Needed if a producer is switched to an injector,
+    void prepareInjectorContainers();
+
     template<class Serializer>
     void serializeOp(Serializer& serializer)
     {
diff --git a/opm/simulators/wells/SingleWellState.cpp b/opm/simulators/wells/SingleWellState.cpp
index 298c02de35d..5696f489e68 100644
--- a/opm/simulators/wells/SingleWellState.cpp
+++ b/opm/simulators/wells/SingleWellState.cpp
@@ -323,13 +323,36 @@ update_injector_targets(const Well& ecl_well, const SummaryState& st)
 }
 
 template<class Scalar>
-void SingleWellState<Scalar>::
-update_targets(const Well& ecl_well, const SummaryState& st)
+bool SingleWellState<Scalar>::
+update_type_and_targets(const Well& ecl_well, const SummaryState& st)
 {
+    bool switchedToProducer = false;
+    if (this->producer != ecl_well.isProducer()) {
+        // type has changed due to ACTIONX
+        // Make sure that we are consistent with the ecl_well
+        switchedToProducer = this->producer = ecl_well.isProducer();
+        if (switchedToProducer) {
+            this->production_cmode = ecl_well.productionControls(st).cmode;
+            // clear injection rates (those are positive)
+            std::transform(this->surface_rates.begin(), this->surface_rates.end(),
+                           this->surface_rates.begin(),
+                           [](const Scalar& val){ return std::min(Scalar(), val);});
+        } else {
+            perf_data.prepareInjectorContainers();
+            this->injection_cmode = ecl_well.injectionControls(st).cmode;
+            // clear production rates (those are negative)
+            std::transform(this->surface_rates.begin(), this->surface_rates.end(),
+                           this->surface_rates.begin(),
+                           [](const Scalar& val){ return std::max(Scalar(), val);});
+        }
+    }
+
     if (this->producer)
         this->update_producer_targets(ecl_well, st);
     else
         this->update_injector_targets(ecl_well, st);
+
+    return switchedToProducer;
 }
 
 template<class Scalar>
diff --git a/opm/simulators/wells/SingleWellState.hpp b/opm/simulators/wells/SingleWellState.hpp
index 36991053f0f..f96e3522a6a 100644
--- a/opm/simulators/wells/SingleWellState.hpp
+++ b/opm/simulators/wells/SingleWellState.hpp
@@ -126,7 +126,12 @@ class SingleWellState {
     void reset_connection_factors(const std::vector<PerforationData<Scalar>>& new_perf_data);
     void update_producer_targets(const Well& ecl_well, const SummaryState& st);
     void update_injector_targets(const Well& ecl_well, const SummaryState& st);
-    void update_targets(const Well& ecl_well, const SummaryState& st);
+    /// \brief update the type of the well and the targets.
+    ///
+    /// This called after ACTIONX is executed to update well rates. The new status is
+    /// in ecl_well and st.
+    /// \return whether well was switched to a producer
+    bool update_type_and_targets(const Well& ecl_well, const SummaryState& st);
     void updateStatus(WellStatus status);
     void init_timestep(const SingleWellState& other);
     void shut();
diff --git a/opm/simulators/wells/WellState.hpp b/opm/simulators/wells/WellState.hpp
index 4310995d49e..bba0872c8e0 100644
--- a/opm/simulators/wells/WellState.hpp
+++ b/opm/simulators/wells/WellState.hpp
@@ -277,6 +277,10 @@ class WellState
     void shutWell(int well_index);
     void stopWell(int well_index);
 
+    void switchToProducer(const std::string& name) {
+        alq_state.insert(name);
+    }
+
     /// The number of phases present.
     int numPhases() const
     {


Reviews:
user: login: blattms
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T07:43:36Z
user: login: bska
body: I like that we're now able to handle the case of a well switching from a producer to an injector, or the other way around, in the context of an action block&ndash;that's a great improvement.  Other than that, I only have minor comments here.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T09:08:17Z
user: login: blattms
body: Comments addresses.

The remaining question is still, what the correct way of switching and reinitialization of wells is. But maybe there is just no conclusion, which is fine.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T11:15:01Z
user: login: bska
body: Thanks a lot for the updates.  I'll merge into master.  I might still have liked a comment in `ALQState<>::insert()` explaining the reasoning for using `emplace()`, but we can add that later.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-26T15:04:00Z

Review comments:
diff_hunk: @@ -323,13 +323,40 @@ update_injector_targets(const Well& ecl_well, const SummaryState& st)
 }
 
 template<class Scalar>
-void SingleWellState<Scalar>::
-update_targets(const Well& ecl_well, const SummaryState& st)
+bool SingleWellState<Scalar>::
+update_type_and_targets(const Well& ecl_well, const SummaryState& st)
 {
+    bool switchedToProducer = false;
+    if (this->producer != ecl_well.isProducer()) {
+        // type has changed due to ACTIONX
+        // Make sure that we consistent with the ecl_well
+        switchedToProducer = this->producer = ecl_well.isProducer();
+        if (switchedToProducer) {
+            this->production_cmode = ecl_well.productionControls(st).cmode;
+            // clear injection rates
+            for(auto&& val: this->surface_rates) {
+                if (val > 0.0) {
+                    val = 0.0;
+                }
path: opm/simulators/wells/SingleWellState.cpp
user: login: blattms
body: Without this I get an exception because wfac is larger than 1.
created_at: 2025-03-19T07:43:36Z
author_association: MEMBER
diff_hunk: @@ -41,6 +41,12 @@ ALQState<Scalar> ALQState<Scalar>::serializationTestObject()
     return result;
 }
 
+template<class Scalar>
+void ALQState<Scalar>::insert(const std::string& wname)
+{
+    this->current_alq_.emplace(wname, 0.0);
path: opm/simulators/wells/ALQState.cpp
user: login: bska
body: If `wname` already exists in `current_alq_`, e.g., because the well was a producer before, then switched to an injector, and then later back to a producer, then its value will be unchanged.  Is that what you want?  If so, please add a comment as to why.  If not, then I'd recommend using `insert_or_assign()` instead of `emplace()`.
created_at: 2025-03-21T08:51:59Z
author_association: MEMBER
diff_hunk: @@ -323,13 +323,40 @@ update_injector_targets(const Well& ecl_well, const SummaryState& st)
 }
 
 template<class Scalar>
-void SingleWellState<Scalar>::
-update_targets(const Well& ecl_well, const SummaryState& st)
+bool SingleWellState<Scalar>::
+update_type_and_targets(const Well& ecl_well, const SummaryState& st)
 {
+    bool switchedToProducer = false;
+    if (this->producer != ecl_well.isProducer()) {
+        // type has changed due to ACTIONX
+        // Make sure that we consistent with the ecl_well
+        switchedToProducer = this->producer = ecl_well.isProducer();
+        if (switchedToProducer) {
+            this->production_cmode = ecl_well.productionControls(st).cmode;
+            // clear injection rates
+            for(auto&& val: this->surface_rates) {
+                if (val > 0.0) {
+                    val = 0.0;
+                }
path: opm/simulators/wells/SingleWellState.cpp
user: login: bska
body: You don't need forwarding references here, and in fact they're a little barrier to readability.  I think I'd prefer using `std::transform()` instead,
```C++
std::transform(this->surface_rates.begin(), this->surface_rates_.end(),
               this->surface_rates.begin(),
               [](const auto& rate) { return std::min(rate, Scalar{]); });
```
Better yet, make a member function which does this.
created_at: 2025-03-21T08:56:21Z
author_association: MEMBER
diff_hunk: @@ -323,13 +323,40 @@ update_injector_targets(const Well& ecl_well, const SummaryState& st)
 }
 
 template<class Scalar>
-void SingleWellState<Scalar>::
-update_targets(const Well& ecl_well, const SummaryState& st)
+bool SingleWellState<Scalar>::
+update_type_and_targets(const Well& ecl_well, const SummaryState& st)
 {
+    bool switchedToProducer = false;
+    if (this->producer != ecl_well.isProducer()) {
+        // type has changed due to ACTIONX
+        // Make sure that we consistent with the ecl_well
+        switchedToProducer = this->producer = ecl_well.isProducer();
+        if (switchedToProducer) {
+            this->production_cmode = ecl_well.productionControls(st).cmode;
+            // clear injection rates
+            for(auto&& val: this->surface_rates) {
+                if (val > 0.0) {
+                    val = 0.0;
+                }
+            }
+        } else {
+            perf_data.prepareInjectorContainers();
+            this->injection_cmode = ecl_well.injectionControls(st).cmode;
+            // clear production rates
+            for(auto&& val: this->surface_rates) {
+                if (val < 0.0) {
+                    val = 0.0;
+                }
+            }
path: opm/simulators/wells/SingleWellState.cpp
user: login: bska
body: Here, too, I'd prefer using `std::transform()`:
```C++
std::transform(this->surface_rates.begin(), this->surface_rates_.end(),
               this->surface_rates.begin(),
               [](const auto& rate) { return std::max(rate, Scalar{]); });
```
Better yet, make a member function which does this.
created_at: 2025-03-21T08:58:57Z
author_association: MEMBER
diff_hunk: @@ -51,13 +51,19 @@ PerfData<Scalar>::PerfData(const std::size_t num_perf,
     , ecl_index(num_perf)
 {
     if (injector) {
-        this->water_throughput.resize(num_perf);
-        this->skin_pressure.resize(num_perf);
-        this->water_velocity.resize(num_perf);
-        this->filtrate_data.resize(num_perf);
+        prepareInjectorContainers();
     }
 }
 
+template<class Scalar>
+void PerfData<Scalar>::prepareInjectorContainers()
+{
+    auto num_perf = pressure.size();
+    this->water_throughput.resize(num_perf);
+    this->skin_pressure.resize(num_perf);
+    this->water_velocity.resize(num_perf);
+    this->filtrate_data.resize(num_perf);
+}
path: opm/simulators/wells/PerfData.cpp
user: login: bska
body: Please leave at least one blank line between functions.
created_at: 2025-03-21T08:59:52Z
author_association: MEMBER
diff_hunk: @@ -323,13 +323,40 @@ update_injector_targets(const Well& ecl_well, const SummaryState& st)
 }
 
 template<class Scalar>
-void SingleWellState<Scalar>::
-update_targets(const Well& ecl_well, const SummaryState& st)
+bool SingleWellState<Scalar>::
+update_type_and_targets(const Well& ecl_well, const SummaryState& st)
 {
+    bool switchedToProducer = false;
+    if (this->producer != ecl_well.isProducer()) {
+        // type has changed due to ACTIONX
+        // Make sure that we consistent with the ecl_well
+        switchedToProducer = this->producer = ecl_well.isProducer();
+        if (switchedToProducer) {
+            this->production_cmode = ecl_well.productionControls(st).cmode;
+            // clear injection rates
+            for(auto&& val: this->surface_rates) {
+                if (val > 0.0) {
+                    val = 0.0;
+                }
path: opm/simulators/wells/SingleWellState.cpp
user: login: bska
body: > Without this I get an exception because wfac is larger than 1.

Would you mind elaborating a little on that, please?  What is `wfac`?
created_at: 2025-03-21T09:03:23Z
author_association: MEMBER
diff_hunk: @@ -41,6 +41,12 @@ ALQState<Scalar> ALQState<Scalar>::serializationTestObject()
     return result;
 }
 
+template<class Scalar>
+void ALQState<Scalar>::insert(const std::string& wname)
+{
+    this->current_alq_.emplace(wname, 0.0);
path: opm/simulators/wells/ALQState.cpp
user: login: blattms
body: I am only resolving symptoms here. For output we need an entry for the well in this case or there is a segmentation fault. Hence : yes this is what I want.

But what I want might not be what we should do. That is why I asked people more familiar with the well code for opinions. Basically the real question is what we reset and how should we reset this when we switch an injector to a producer.
created_at: 2025-03-24T08:52:06Z
author_association: MEMBER
diff_hunk: @@ -323,13 +323,40 @@ update_injector_targets(const Well& ecl_well, const SummaryState& st)
 }
 
 template<class Scalar>
-void SingleWellState<Scalar>::
-update_targets(const Well& ecl_well, const SummaryState& st)
+bool SingleWellState<Scalar>::
+update_type_and_targets(const Well& ecl_well, const SummaryState& st)
 {
+    bool switchedToProducer = false;
+    if (this->producer != ecl_well.isProducer()) {
+        // type has changed due to ACTIONX
+        // Make sure that we consistent with the ecl_well
+        switchedToProducer = this->producer = ecl_well.isProducer();
+        if (switchedToProducer) {
+            this->production_cmode = ecl_well.productionControls(st).cmode;
+            // clear injection rates
+            for(auto&& val: this->surface_rates) {
+                if (val > 0.0) {
+                    val = 0.0;
+                }
path: opm/simulators/wells/SingleWellState.cpp
user: login: blattms
body: I meant the WFRAC entry in StandardWellPrimaryVariables, sorry.
created_at: 2025-03-24T09:01:15Z
author_association: MEMBER
diff_hunk: @@ -323,13 +323,40 @@ update_injector_targets(const Well& ecl_well, const SummaryState& st)
 }
 
 template<class Scalar>
-void SingleWellState<Scalar>::
-update_targets(const Well& ecl_well, const SummaryState& st)
+bool SingleWellState<Scalar>::
+update_type_and_targets(const Well& ecl_well, const SummaryState& st)
 {
+    bool switchedToProducer = false;
+    if (this->producer != ecl_well.isProducer()) {
+        // type has changed due to ACTIONX
+        // Make sure that we consistent with the ecl_well
+        switchedToProducer = this->producer = ecl_well.isProducer();
+        if (switchedToProducer) {
+            this->production_cmode = ecl_well.productionControls(st).cmode;
+            // clear injection rates
+            for(auto&& val: this->surface_rates) {
+                if (val > 0.0) {
+                    val = 0.0;
+                }
path: opm/simulators/wells/SingleWellState.cpp
user: login: blattms
body: I think we used to need them due to compiler bugs.
Anyway, I changed to your preference (under slight protest...).

created_at: 2025-03-24T11:12:32Z
author_association: MEMBER

PR comments:
user: login: blattms
created_at: 2025-03-19T11:12:16Z
body: This is now tested by the client and works perfectly for the test cases. Ready for review.
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-20T09:42:49Z
body: Thanks for the request, but if it is okay I think I will suggest @bska to review this for better efficiency. I can review it while I realized it will kill some time for me to get familiar with the implementation of the actions first. 
author_association: MEMBER
user: login: bska
created_at: 2025-03-26T13:33:16Z
body: jenkins build this please
author_association: MEMBER