Repository: OPM/opm-common
Pull request #4334: Compositional well
Author: login: GitPaean
Created: 2024-11-11T12:01:01Z | Merged: 2025-03-06T17:35:13Z

This PR is providing some necessary elements for the compositional simulation with wells. It mostly including the following elements, 

1. parsing the schedule keywords WINJGAS and WELLSTRE
2. making PTFlash::solve() can handle arbitrary number of derivatives from the input AD variables. 
3. adding WAMF in the summary keywords

The PR can be reviewed and merged by itself without the downstream PR https://github.com/OPM/opm-simulators/pull/6042 . 


Diff:
diff --git a/opm/input/eclipse/EclipseState/SummaryConfig/SummaryConfig.cpp b/opm/input/eclipse/EclipseState/SummaryConfig/SummaryConfig.cpp
index a87a0983b07..ffb6bc534ae 100644
--- a/opm/input/eclipse/EclipseState/SummaryConfig/SummaryConfig.cpp
+++ b/opm/input/eclipse/EclipseState/SummaryConfig/SummaryConfig.cpp
@@ -394,7 +394,7 @@ struct SummaryConfigContext {
     bool is_well_comp(const std::string& keyword)
     {
         static const auto well_comp_kw = keyword_set {
-             "WXMF", "WYMF", "WZMF", "WCGMR", "WCOMR"
+             "WAMF", "WXMF", "WYMF", "WZMF", "WCGMR", "WCOMR",
         };
 
         return is_in_set(well_comp_kw, keyword);
diff --git a/opm/input/eclipse/Schedule/ScheduleState.cpp b/opm/input/eclipse/Schedule/ScheduleState.cpp
index b07edb7335c..56cd97139e8 100644
--- a/opm/input/eclipse/Schedule/ScheduleState.cpp
+++ b/opm/input/eclipse/Schedule/ScheduleState.cpp
@@ -311,6 +311,7 @@ bool ScheduleState::operator==(const ScheduleState& other) const {
         && this->bhp_defaults.get() == other.bhp_defaults.get()
         && this->source.get() == other.source.get()
         && this->wells == other.wells
+        && this->inj_streams == other.inj_streams
         && this->groups == other.groups
         && this->vfpprod == other.vfpprod
         && this->vfpinj == other.vfpinj
diff --git a/opm/input/eclipse/Schedule/ScheduleState.hpp b/opm/input/eclipse/Schedule/ScheduleState.hpp
index b79fcce1c42..1b6349177e8 100644
--- a/opm/input/eclipse/Schedule/ScheduleState.hpp
+++ b/opm/input/eclipse/Schedule/ScheduleState.hpp
@@ -201,6 +201,9 @@ namespace Opm {
                 return (ptr != nullptr);
             }
 
+            void update(const K& key, std::shared_ptr<T> value) {
+                this->m_data.insert_or_assign(key, std::move(value));
+            }
 
             void update(T object) {
                 auto key = object.name();
@@ -509,6 +512,9 @@ namespace Opm {
         // constant flux aquifers
         std::unordered_map<int, SingleAquiferFlux> aqufluxs;
         BCProp bcprop;
+        // injection streams for compostional STREAM injection using WINJGAS 
+        map_member<std::string, std::vector<double>> inj_streams;
+
         std::unordered_map<std::string, double> target_wellpi;
         std::optional<NextStep> next_tstep;
 
@@ -546,6 +552,7 @@ namespace Opm {
             serializer(wells);
             serializer(aqufluxs);
             serializer(bcprop);
+            serializer(inj_streams);
             serializer(target_wellpi);
             serializer(this->next_tstep);
             serializer(m_start_time);
diff --git a/opm/input/eclipse/Schedule/Well/Well.hpp b/opm/input/eclipse/Schedule/Well/Well.hpp
index ea8ce5c1c01..869b1f168dd 100644
--- a/opm/input/eclipse/Schedule/Well/Well.hpp
+++ b/opm/input/eclipse/Schedule/Well/Well.hpp
@@ -164,6 +164,9 @@ class Well {
 
         double rsRvInj;
 
+        // injection stream compostion for compositional simulation
+        std::optional<std::vector<double>> gas_inj_composition{};
+
         bool operator==(const WellInjectionProperties& other) const;
         bool operator!=(const WellInjectionProperties& other) const;
 
@@ -230,6 +233,9 @@ class Well {
         void update_uda(const UDQConfig& udq_config, UDQActive& udq_active, UDAControl control, const UDAValue& value);
         void handleWTMULT(Well::WELTARGCMode cmode, double factor);
 
+        void setGasInjComposition(const std::vector<double>& composition);
+        const std::vector<double>& gasInjComposition() const;
+
         template<class Serializer>
         void serializeOp(Serializer& serializer)
         {
@@ -248,6 +254,7 @@ class Well {
             serializer(injectorType);
             serializer(controlMode);
             serializer(rsRvInj);
+            serializer(gas_inj_composition);
         }
     };
 
diff --git a/opm/input/eclipse/Schedule/Well/WellInjectionProperties.cpp b/opm/input/eclipse/Schedule/Well/WellInjectionProperties.cpp
index 74b9c2b106e..479bce9050b 100644
--- a/opm/input/eclipse/Schedule/Well/WellInjectionProperties.cpp
+++ b/opm/input/eclipse/Schedule/Well/WellInjectionProperties.cpp
@@ -60,7 +60,8 @@ namespace Opm {
           injectionControls(0),
           injectorType(InjectorType::WATER),
           controlMode(InjectorCMode::CMODE_UNDEFINED),
-          rsRvInj(0.0)
+          rsRvInj(0.0),
+          gas_inj_composition(std::nullopt)
     {
     }
 
@@ -82,6 +83,7 @@ namespace Opm {
         result.injectorType = InjectorType::OIL;
         result.controlMode = InjectorCMode::BHP;
         result.rsRvInj = 11;
+        result.gas_inj_composition = std::vector<double>{1.0, 2.0, 3.0};
 
         return result;
     }
@@ -284,7 +286,8 @@ namespace Opm {
             (injectionControls == other.injectionControls) &&
             (injectorType == other.injectorType) &&
             (controlMode == other.controlMode) &&
-            (rsRvInj == other.rsRvInj))
+            (rsRvInj == other.rsRvInj) &&
+            (gas_inj_composition == other.gas_inj_composition))
             return true;
         else
             return false;
@@ -500,5 +503,15 @@ namespace Opm {
         }
     }
 
+    void Well::WellInjectionProperties::setGasInjComposition(const std::vector<double>& composition) {
+        gas_inj_composition = composition;
+    }
+
+    const std::vector<double>& Well::WellInjectionProperties::gasInjComposition() const {
+        if (!gas_inj_composition.has_value()) {
+            throw std::invalid_argument("Gas injection composition not set");
+        }
+        return gas_inj_composition.value();
+    }
 
 }
diff --git a/opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp b/opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
index 91e02f50786..11d4a8f3310 100644
--- a/opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
+++ b/opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
@@ -445,6 +445,29 @@ void handleWCYCLE(HandlerContext& handlerContext)
     handlerContext.state().wcycle.update(std::move(new_config));
 }
 
+void handleWELLSTRE(HandlerContext& handlerContext)
+{
+    auto& inj_streams = handlerContext.state().inj_streams;
+    for (const auto& record : handlerContext.keyword) {
+        const auto stream_name = record.getItem<ParserKeywords::WELLSTRE::STREAM>().getTrimmedString(0);
+        const auto& composition = record.getItem<ParserKeywords::WELLSTRE::COMPOSITIONS>().getSIDoubleData();
+        const std::size_t num_comps = handlerContext.static_schedule().m_runspec.numComps();
+        if (composition.size() != num_comps) {
+            const std::string msg = fmt::format("The number of the composition values for stream '{}' is not the same as the number of components.", stream_name);
+            throw OpmInputError(msg, handlerContext.keyword.location());
+        }
+
+        const double sum = std::accumulate(composition.begin(), composition.end(), 0.0);
+        if (std::abs(sum - 1.0) > std::numeric_limits<double>::epsilon()) {
+            const std::string msg = fmt::format("The sum of the composition values for stream '{}' is not 1.0, but {}.", stream_name, sum);
+            throw OpmInputError(msg, handlerContext.keyword.location());
+        }
+        auto composition_ptr = std::make_shared<std::vector<double>>(composition);
+        inj_streams.update(stream_name, std::move(composition_ptr));
+    }
+
+}
+
 void handleWELOPEN(HandlerContext& handlerContext)
 {
     const auto& keyword = handlerContext.keyword;
@@ -527,6 +550,44 @@ void handleWELOPEN(HandlerContext& handlerContext)
     }
 }
 
+void handleWINJGAS(HandlerContext& handlerContext)
+{
+    // \Note: we do not support the item 4 MAKEUPGAS and item 5 STAGE in WINJGAS keyword yet
+    for (const auto& record : handlerContext.keyword) {
+        const std::string fluid_nature = record.getItem<ParserKeywords::WINJGAS::FLUID>().getTrimmedString(0);
+
+        // \Note: technically, only the first two characters are significant
+        // with some testing, we can determine whether we want to enforce this.
+        // at the moment, we only support full string STREAM for fluid nature
+        if (fluid_nature != "STREAM") {
+            const std::string msg = fmt::format("The fluid nature '{}' is not supported in WINJGAS keyword.", fluid_nature);
+            throw OpmInputError(msg, handlerContext.keyword.location());
+        }
+
+        const std::string stream_name = record.getItem<ParserKeywords::WINJGAS::STREAM>().getTrimmedString(0);
+        // we make sure the stream is defined in WELLSTRE keyword
+        const auto& inj_streams = handlerContext.state().inj_streams;
+        if (!inj_streams.has(stream_name)) {
+            const std::string msg = fmt::format("The stream '{}' is not defined in WELLSTRE keyword.", stream_name);
+            throw OpmInputError(msg, handlerContext.keyword.location());
+        }
+
+        const std::string wellNamePattern = record.getItem<ParserKeywords::WINJGAS::WELL>().getTrimmedString(0);
+        const auto well_names = handlerContext.wellNames(wellNamePattern, false);
+        for (const auto& well_name : well_names) {
+            auto well2 = handlerContext.state().wells.get(well_name);
+            auto injection = std::make_shared<Well::WellInjectionProperties>(well2.getInjectionProperties());
+
+            const auto& inj_stream = inj_streams.get(stream_name);
+            injection->setGasInjComposition(inj_stream);
+
+            if (well2.updateInjection(injection)) {
+                handlerContext.state().wells.update(std::move(well2));
+            }
+        }
+    }
+}
+
 void handleWELSPECS(HandlerContext& handlerContext)
 {
     using Kw = ParserKeywords::WELSPECS;
@@ -945,11 +1006,13 @@ getWellHandlers()
         { "WCONPROD", &handleWCONPROD },
         { "WCYCLE",   &handleWCYCLE   },
         { "WELOPEN" , &handleWELOPEN  },
+        { "WELLSTRE", &handleWELLSTRE },
         { "WELSPECS", &handleWELSPECS },
         { "WELSPECL", &handleWELSPECL },
         { "WELTARG" , &handleWELTARG  },
         { "WELTRAJ" , &handleWELTRAJ  },
         { "WHISTCTL", &handleWHISTCTL },
+        { "WINJGAS",  &handleWINJGAS  },
         { "WLIST"   , &handleWLIST    },
         { "WPAVE"   , &handleWPAVE    },
         { "WPAVEDEP", &handleWPAVEDEP },
diff --git a/opm/input/eclipse/share/keywords/001_Eclipse300/W/WELLSTRE b/opm/input/eclipse/share/keywords/001_Eclipse300/W/WELLSTRE
index cae65e60782..2abd762acea 100644
--- a/opm/input/eclipse/share/keywords/001_Eclipse300/W/WELLSTRE
+++ b/opm/input/eclipse/share/keywords/001_Eclipse300/W/WELLSTRE
@@ -10,16 +10,11 @@
       "value_type": "STRING"
     },
     {
-        "item": 2,
-        "name": "XMOLE1",
-        "value_type": "DOUBLE",
-        "dimension": "1"
-      },
-      {
-        "item": 3,
-        "name": "XMOLE2",
-        "value_type": "DOUBLE",
-        "dimension": "1"
-      }
+      "item": 2,
+      "name": "COMPOSITIONS",
+      "value_type": "DOUBLE",
+      "dimension": "1",
+      "size_type": "ALL"
+    }
   ]
 }
diff --git a/opm/input/eclipse/share/keywords/001_Eclipse300/W/WELL_PROBE_COMP b/opm/input/eclipse/share/keywords/001_Eclipse300/W/WELL_PROBE_COMP
index 9692ab376ea..6a767cf3b22 100644
--- a/opm/input/eclipse/share/keywords/001_Eclipse300/W/WELL_PROBE_COMP
+++ b/opm/input/eclipse/share/keywords/001_Eclipse300/W/WELL_PROBE_COMP
@@ -4,6 +4,7 @@
     "SUMMARY"
   ],
   "deck_names": [
+    "WAMF",
     "WXMF",
     "WYMF",
     "WZMF",
diff --git a/opm/material/constraintsolvers/PTFlash.hpp b/opm/material/constraintsolvers/PTFlash.hpp
index 66cb99db311..3a66c76d237 100644
--- a/opm/material/constraintsolvers/PTFlash.hpp
+++ b/opm/material/constraintsolvers/PTFlash.hpp
@@ -80,7 +80,7 @@ class PTFlash
      *
      */
     template <class FluidState>
-    static void solve(FluidState& fluid_state,
+    static bool solve(FluidState& fluid_state,
                       const std::string& twoPhaseMethod,
                       Scalar flash_tolerance,
                       const EOSType& eos_type,
@@ -116,6 +116,8 @@ class PTFlash
 
         // we update the derivatives in fluid_state
         updateDerivatives_(fluid_state_scalar, fluid_state, eos_type, is_single_phase);
+
+        return is_single_phase;
     } //end solve
 
     /*!
@@ -992,7 +994,7 @@ class PTFlash
         // p_l and p_v are the same here, in the future, there might be slightly more complicated scenarios when capillary
         // pressure joins
 
-        constexpr size_t num_deri = numComponents;
+        constexpr size_t num_deri = InputEval::numVars;
         for (unsigned compIdx = 0; compIdx < numComponents; ++compIdx) {
             std::vector<double> deri(num_deri, 0.);
             // derivatives from P


Reviews:
user: login: bska
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-11T12:29:50Z
user: login: GitPaean
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-11T13:09:03Z
user: login: bska
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-11T14:33:53Z
user: login: GitPaean
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-12T09:11:54Z
user: login: GitPaean
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-12T09:14:06Z
user: login: GitPaean
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-12T09:56:25Z
user: login: akva2
body: Just a first pass cosmetics
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T12:47:14Z
user: login: bska
body: When writing new keyword handlers, we generally prefer to use compiled item names instead of string literals. Other than that this looks good to me, with a couple of opportunities to use `move()` instead of copy.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T13:18:23Z
user: login: GitPaean
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T14:59:24Z
user: login: bska
body: Thanks a lot for the updates.  This looks good to me now and I'll merge into master.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T17:35:05Z

Review comments:
diff_hunk: @@ -500,6 +500,9 @@ namespace Opm {
         // constant flux aquifers
         std::unordered_map<int, SingleAquiferFlux> aqufluxs;
         BCProp bcprop;
+        // injection streams for compostional STREAM injection using WINJGAS 
+        std::unordered_map<std::string, std::vector<double>> inj_streams;
path: opm/input/eclipse/Schedule/ScheduleState.hpp
user: login: bska
body: Please use `map_member<>`.  It really is the primary solution to this problem.
created_at: 2024-11-11T12:29:50Z
author_association: MEMBER
diff_hunk: @@ -500,6 +500,9 @@ namespace Opm {
         // constant flux aquifers
         std::unordered_map<int, SingleAquiferFlux> aqufluxs;
         BCProp bcprop;
+        // injection streams for compostional STREAM injection using WINJGAS 
+        std::unordered_map<std::string, std::vector<double>> inj_streams;
path: opm/input/eclipse/Schedule/ScheduleState.hpp
user: login: GitPaean
body: Sure.  I went back and forth on this  and thanks for the suggestion.  I will do that. 

not very familiar with the design here.  Will you explain a little bit what is the difference here for future record?
created_at: 2024-11-11T13:09:02Z
author_association: MEMBER
diff_hunk: @@ -500,6 +500,9 @@ namespace Opm {
         // constant flux aquifers
         std::unordered_map<int, SingleAquiferFlux> aqufluxs;
         BCProp bcprop;
+        // injection streams for compostional STREAM injection using WINJGAS 
+        std::unordered_map<std::string, std::vector<double>> inj_streams;
path: opm/input/eclipse/Schedule/ScheduleState.hpp
user: login: bska
body: > not very familiar with the design here. Will you explain a little bit what is the difference here for future record?

Sure.  The point is that `map_member<K,V>` is more or less a `map<K, shared_ptr<V>>` with some additional logic on top.  This enables sharing data between `ScheduleState` objects at different report steps if there is no intervening keyword to add/change the data.
created_at: 2024-11-11T14:33:52Z
author_association: MEMBER
diff_hunk: @@ -500,6 +500,9 @@ namespace Opm {
         // constant flux aquifers
         std::unordered_map<int, SingleAquiferFlux> aqufluxs;
         BCProp bcprop;
+        // injection streams for compostional STREAM injection using WINJGAS 
+        std::unordered_map<std::string, std::vector<double>> inj_streams;
path: opm/input/eclipse/Schedule/ScheduleState.hpp
user: login: GitPaean
body: The design looks like `V` needs to be a struct containing `name()` , which is the key . 
created_at: 2024-11-12T09:11:54Z
author_association: MEMBER
diff_hunk: @@ -500,6 +500,9 @@ namespace Opm {
         // constant flux aquifers
         std::unordered_map<int, SingleAquiferFlux> aqufluxs;
         BCProp bcprop;
+        // injection streams for compostional STREAM injection using WINJGAS 
+        std::unordered_map<std::string, std::vector<double>> inj_streams;
path: opm/input/eclipse/Schedule/ScheduleState.hpp
user: login: GitPaean
body: Is it okay that I add a member function 

```c++
void update(const K& key, std::shared_ptr<T> value);
```

or you think it will break the design. 
created_at: 2024-11-12T09:14:06Z
author_association: MEMBER
diff_hunk: @@ -500,6 +500,9 @@ namespace Opm {
         // constant flux aquifers
         std::unordered_map<int, SingleAquiferFlux> aqufluxs;
         BCProp bcprop;
+        // injection streams for compostional STREAM injection using WINJGAS 
+        std::unordered_map<std::string, std::vector<double>> inj_streams;
path: opm/input/eclipse/Schedule/ScheduleState.hpp
user: login: GitPaean
body: It looks it might work, but it will be on draft mode for some while until we figure out how things work in the opm-simulators.  I will check what Jenkins think about it. 

If it does not work later, I will make a StreamComposition struct to make it the same with Well or Group to following the design style. 
created_at: 2024-11-12T09:56:25Z
author_association: MEMBER
diff_hunk: @@ -445,6 +445,25 @@ void handleWCYCLE(HandlerContext& handlerContext)
     handlerContext.state().wcycle.update(std::move(new_config));
 }
 
+void handleWELLSTRE(HandlerContext& handlerContext)
+{
+    auto& inj_streams = handlerContext.state().inj_streams;
+    for (const auto& record : handlerContext.keyword) {
+        const std::string& stream_name = record.getItem("STREAM").getTrimmedString(0);
+        const auto& compostion = record.getItem("COMPOSITIONS").getData<double>();
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: akva2
body: composition
created_at: 2025-03-06T12:41:39Z
author_association: MEMBER
diff_hunk: @@ -445,6 +445,25 @@ void handleWCYCLE(HandlerContext& handlerContext)
     handlerContext.state().wcycle.update(std::move(new_config));
 }
 
+void handleWELLSTRE(HandlerContext& handlerContext)
+{
+    auto& inj_streams = handlerContext.state().inj_streams;
+    for (const auto& record : handlerContext.keyword) {
+        const std::string& stream_name = record.getItem("STREAM").getTrimmedString(0);
+        const auto& compostion = record.getItem("COMPOSITIONS").getData<double>();
+        // TODO: we need to check the number of the values in the composition is the same as the number of components
+
+        const double sum = std::accumulate(compostion.begin(), compostion.end(), 0.0);
+        if (std::abs(sum - 1.0) > std::numeric_limits<double>::epsilon()) {
+            std::string msg = fmt::format("The sum of the composition values for stream '{}' is not 1.0, but {}.", stream_name, sum);
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: akva2
body: const or eliminate temporary
created_at: 2025-03-06T12:42:19Z
author_association: MEMBER
diff_hunk: @@ -527,6 +546,41 @@ void handleWELOPEN(HandlerContext& handlerContext)
     }
 }
 
+void handleWINJGAS(HandlerContext& HandlerContext)
+{
+    for (const auto& record : HandlerContext.keyword) {
+        const std::string& wellNamePattern = record.getItem("WELL").getTrimmedString(0);
+        const auto well_names = HandlerContext.wellNames(wellNamePattern, false);
+        const std::string& fluid_nature = record.getItem("FLUID").getTrimmedString(0);
+        // TODO: technically, only the firt two characters are significant
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: akva2
body: first
created_at: 2025-03-06T12:42:37Z
author_association: MEMBER
diff_hunk: @@ -527,6 +546,41 @@ void handleWELOPEN(HandlerContext& handlerContext)
     }
 }
 
+void handleWINJGAS(HandlerContext& HandlerContext)
+{
+    for (const auto& record : HandlerContext.keyword) {
+        const std::string& wellNamePattern = record.getItem("WELL").getTrimmedString(0);
+        const auto well_names = HandlerContext.wellNames(wellNamePattern, false);
+        const std::string& fluid_nature = record.getItem("FLUID").getTrimmedString(0);
+        // TODO: technically, only the firt two characters are significant
+        // TODO: we need to test it out whether only the first two characters matter
+        if (fluid_nature != "STREAM") {
+            std::string msg = fmt::format("The fluid nature '{}' is not supported in WINJGAS keyword.", fluid_nature);
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: akva2
body: const or eliminate temporary
created_at: 2025-03-06T12:42:54Z
author_association: MEMBER
diff_hunk: @@ -527,6 +546,41 @@ void handleWELOPEN(HandlerContext& handlerContext)
     }
 }
 
+void handleWINJGAS(HandlerContext& HandlerContext)
+{
+    for (const auto& record : HandlerContext.keyword) {
+        const std::string& wellNamePattern = record.getItem("WELL").getTrimmedString(0);
+        const auto well_names = HandlerContext.wellNames(wellNamePattern, false);
+        const std::string& fluid_nature = record.getItem("FLUID").getTrimmedString(0);
+        // TODO: technically, only the firt two characters are significant
+        // TODO: we need to test it out whether only the first two characters matter
+        if (fluid_nature != "STREAM") {
+            std::string msg = fmt::format("The fluid nature '{}' is not supported in WINJGAS keyword.", fluid_nature);
+            throw OpmInputError(msg, HandlerContext.keyword.location());
+        }
+        const std::string& stream_name = record.getItem("STREAM").getTrimmedString(0);
+        // TODO: we do not handle other records for now
+
+        // we make sure the stream is defined in WELLSTRE keyword
+        const auto& inj_streams = HandlerContext.state().inj_streams;
+        if (!inj_streams.has(stream_name)) {
+            std::string msg = fmt::format("The stream '{}' is not defined in WELLSTRE keyword.", stream_name);
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: akva2
body: const or eliminate temporary
created_at: 2025-03-06T12:43:10Z
author_association: MEMBER
diff_hunk: @@ -527,6 +546,41 @@ void handleWELOPEN(HandlerContext& handlerContext)
     }
 }
 
+void handleWINJGAS(HandlerContext& HandlerContext)
+{
+    for (const auto& record : HandlerContext.keyword) {
+        const std::string& wellNamePattern = record.getItem("WELL").getTrimmedString(0);
+        const auto well_names = HandlerContext.wellNames(wellNamePattern, false);
+        const std::string& fluid_nature = record.getItem("FLUID").getTrimmedString(0);
+        // TODO: technically, only the firt two characters are significant
+        // TODO: we need to test it out whether only the first two characters matter
+        if (fluid_nature != "STREAM") {
+            std::string msg = fmt::format("The fluid nature '{}' is not supported in WINJGAS keyword.", fluid_nature);
+            throw OpmInputError(msg, HandlerContext.keyword.location());
+        }
+        const std::string& stream_name = record.getItem("STREAM").getTrimmedString(0);
+        // TODO: we do not handle other records for now
+
+        // we make sure the stream is defined in WELLSTRE keyword
+        const auto& inj_streams = HandlerContext.state().inj_streams;
+        if (!inj_streams.has(stream_name)) {
+            std::string msg = fmt::format("The stream '{}' is not defined in WELLSTRE keyword.", stream_name);
+            throw OpmInputError(msg, HandlerContext.keyword.location());
+        }
+
+        auto well2 = HandlerContext.state().wells.get(well_names[0]);
+        // if (well2.isProducer()) {
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: akva2
body: no new commented code please
created_at: 2025-03-06T12:43:30Z
author_association: MEMBER
diff_hunk: @@ -394,7 +394,7 @@ struct SummaryConfigContext {
     bool is_well_comp(const std::string& keyword)
     {
         static const auto well_comp_kw = keyword_set {
-             "WXMF", "WYMF", "WZMF", "WCGMR", "WCOMR"
+             "WAMF", "WXMF", "WYMF", "WZMF", "WCGMR", "WCOMR"
path: opm/input/eclipse/EclipseState/SummaryConfig/SummaryConfig.cpp
user: login: akva2
body: maybe add trailing comma while at it.
created_at: 2025-03-06T12:44:23Z
author_association: MEMBER
diff_hunk: @@ -992,7 +992,7 @@ class PTFlash
         // p_l and p_v are the same here, in the future, there might be slightly more complicated scenarios when capillary
         // pressure joins
 
-        constexpr size_t num_deri = numComponents;
+        constexpr size_t num_deri = InputEval::numVars; // numComponents;
path: opm/material/constraintsolvers/PTFlash.hpp
user: login: akva2
body: just remove the trailing comment
created_at: 2025-03-06T12:45:46Z
author_association: MEMBER
diff_hunk: @@ -451,7 +451,11 @@ void handleWELLSTRE(HandlerContext& handlerContext)
     for (const auto& record : handlerContext.keyword) {
         const std::string& stream_name = record.getItem("STREAM").getTrimmedString(0);
         const auto& compostion = record.getItem("COMPOSITIONS").getData<double>();
-        // TODO: we need to check the number of the values in the composition is the same as the number of components
+        const std::size_t num_comps = handlerContext.static_schedule().m_runspec.numComps();
+        if (compostion.size() != num_comps) {
+            std::string msg = fmt::format("The number of the composition values for stream '{}' is not the same as the number of components.", stream_name);
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: akva2
body: const or eliminate temporary
created_at: 2025-03-06T12:46:25Z
author_association: MEMBER
diff_hunk: @@ -568,19 +574,21 @@ void handleWINJGAS(HandlerContext& handlerContext)
             throw OpmInputError(msg, handlerContext.keyword.location());
         }
 
-        auto well2 = handlerContext.state().wells.get(well_names[0]);
-        // if (well2.isProducer()) {
-        //     std::string msg = fmt::format("The well '{}' is a producer, not an injector.", well_names[0]);
-        //     throw OpmInputError(msg, HandlerContext.keyword.location());
-        // }
-        auto injection = std::make_shared<Well::WellInjectionProperties>(well2.getInjectionProperties());
+        for (const auto& well_name : well_names) {
+            auto well2 = handlerContext.state().wells.get(well_name);
+            if (well2.isProducer()) {
+                std::string msg = fmt::format(
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: akva2
body: const or eliminate temporary
created_at: 2025-03-06T12:46:55Z
author_association: MEMBER
diff_hunk: @@ -201,6 +201,10 @@ namespace Opm {
                 return (ptr != nullptr);
             }
 
+            void update(const K& key, std::shared_ptr<T> value) {
+                this->m_data[key] = value;
+            }
path: opm/input/eclipse/Schedule/ScheduleState.hpp
user: login: bska
body: Might as well write this as
```C++
this->m_data.insert_or_assign(key, std::move(value));
```
to reduce the number of copies.
created_at: 2025-03-06T13:00:35Z
author_association: MEMBER
diff_hunk: @@ -445,6 +445,29 @@ void handleWCYCLE(HandlerContext& handlerContext)
     handlerContext.state().wcycle.update(std::move(new_config));
 }
 
+void handleWELLSTRE(HandlerContext& handlerContext)
+{
+    auto& inj_streams = handlerContext.state().inj_streams;
+    for (const auto& record : handlerContext.keyword) {
+        const std::string& stream_name = record.getItem("STREAM").getTrimmedString(0);
+        const auto& composition = record.getItem("COMPOSITIONS").getData<double>();
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: bska
body: Please prefer compiled item strings over string literals:
```C++
const auto stream_name = record.getItem<ParserKeywords::WELLSTRE::STREAM>().getTrimmedString(0);
auto composition = record.getItem<ParserKeywords::WELLSTRE::COMPOSITIONS>().getSIDoubleData();
```
The net effect is the same as when using string literals, but the compiled item names will give build failures if the item names change and are therefore safer for future maintenance.

By the way, we **generally** prefer `getSIDoubleData()` over direct calls to `getData<double>()`.  The latter should be limited only to those cases where performing unit conversion is wrong.  Also, since you're `move()`-ing `composition` in the `make_shared<>()` call below the object should typically be an independent copy rather than a reference.  Finally, `getTrimmedString()` returns a `std::string` value, not a reference, so there's no benefit to capturing the value as a reference in this particular case.
created_at: 2025-03-06T13:09:28Z
author_association: MEMBER
diff_hunk: @@ -445,6 +445,29 @@ void handleWCYCLE(HandlerContext& handlerContext)
     handlerContext.state().wcycle.update(std::move(new_config));
 }
 
+void handleWELLSTRE(HandlerContext& handlerContext)
+{
+    auto& inj_streams = handlerContext.state().inj_streams;
+    for (const auto& record : handlerContext.keyword) {
+        const std::string& stream_name = record.getItem("STREAM").getTrimmedString(0);
+        const auto& composition = record.getItem("COMPOSITIONS").getData<double>();
+        const std::size_t num_comps = handlerContext.static_schedule().m_runspec.numComps();
+        if (composition.size() != num_comps) {
+            const std::string msg = fmt::format("The number of the composition values for stream '{}' is not the same as the number of components.", stream_name);
+            throw OpmInputError(msg, handlerContext.keyword.location());
+        }
+
+        const double sum = std::accumulate(composition.begin(), composition.end(), 0.0);
+        if (std::abs(sum - 1.0) > std::numeric_limits<double>::epsilon()) {
+            const std::string msg = fmt::format("The sum of the composition values for stream '{}' is not 1.0, but {}.", stream_name, sum);
+            throw OpmInputError(msg, handlerContext.keyword.location());
+        }
+        auto composition_ptr = std::make_shared<std::vector<double>>(std::move(composition));
+        inj_streams.update(stream_name, composition_ptr);
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: bska
body: Might as well `move()` the `composition_ptr` into `inj_streams` here.
created_at: 2025-03-06T13:10:11Z
author_association: MEMBER
diff_hunk: @@ -527,6 +550,44 @@ void handleWELOPEN(HandlerContext& handlerContext)
     }
 }
 
+void handleWINJGAS(HandlerContext& handlerContext)
+{
+    // \Note: we do not support the item 4 MAKEUPGAS and item 5 STAGE in WINJGAS keyword yet
+    for (const auto& record : handlerContext.keyword) {
+        const std::string& wellNamePattern = record.getItem("WELL").getTrimmedString(0);
+        const auto well_names = handlerContext.wellNames(wellNamePattern, false);
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: bska
body: It's a minor point, but we might as well move the call to `wellNames()` below the error checking so that we don't perform work unless we know that we need to.
created_at: 2025-03-06T13:17:06Z
author_association: MEMBER
diff_hunk: @@ -445,6 +445,29 @@ void handleWCYCLE(HandlerContext& handlerContext)
     handlerContext.state().wcycle.update(std::move(new_config));
 }
 
+void handleWELLSTRE(HandlerContext& handlerContext)
+{
+    auto& inj_streams = handlerContext.state().inj_streams;
+    for (const auto& record : handlerContext.keyword) {
+        const std::string& stream_name = record.getItem("STREAM").getTrimmedString(0);
+        const auto& composition = record.getItem("COMPOSITIONS").getData<double>();
path: opm/input/eclipse/Schedule/Well/WellKeywordHandlers.cpp
user: login: GitPaean
body: I am using `const auto& composition` here and removing the std::move() later.  I feel more comfortable to keep the composition const. 
created_at: 2025-03-06T14:59:24Z
author_association: MEMBER

PR comments:
user: login: GitPaean
created_at: 2024-11-11T12:01:09Z
body: jenkins build this please
author_association: MEMBER
user: login: GitPaean
created_at: 2024-11-12T10:00:44Z
body: jenkins build this please
author_association: MEMBER
user: login: GitPaean
created_at: 2025-02-28T14:06:21Z
body: jenkins build this opm-simulators=6042 please
author_association: MEMBER
user: login: GitPaean
created_at: 2025-02-28T14:45:16Z
body: jenkins build this opm-simulators=6042 please
author_association: MEMBER
user: login: GitPaean
created_at: 2025-02-28T15:28:47Z
body: jenkins build this opm-simulators=6042 please
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-02T11:07:00Z
body: jenkins build this opm-simulators=6042 please
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-02T11:42:10Z
body: jenkins build this opm-simulators=6042 please
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-05T14:36:35Z
body: jenkins build this please
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T12:28:03Z
body: I think this PR is ready for review and potential merging.  It can go in without the downstream PR. 
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T12:28:26Z
body: jenkins build this please
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T12:56:22Z
body: The failure is due to the following message, 
```
Error: Unrecoverable errors while loading input:
Problem with keyword WINJGAS
In /var/lib/jenkins/workspace/opm-common-PR-builder/deps/opm-tests/co2store/CO2STORE_GASWAT.DATA line 186
The well 'INJ1' is a producer, not an injector, can not be specified with WINJGAS.
```
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T13:02:37Z
body: The comments are addressed. and remove some requirement for WINJGAS keyword so the jenkins can pass. 
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T13:02:50Z
body: jenkins build this please
author_association: MEMBER
user: login: akva2
created_at: 2025-03-06T13:05:48Z
body: uhm, are we sure it's not just a broken test case? it seems like a very good sanity check to me even if we have to reorder those files.
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T13:19:14Z
body: > uhm, are we sure it's not just a broken test case? it seems like a very good safety check to me even if we have to reorder those files.

Basically, the schedule parsing in OPM is in sequential order. We have multiple occasions that some DATA file is parsed by other simulator while rejected by OPM. 

The direct issue for this incident is that all the wells are created by default as producers when creating the wells based on WELSPECS, 
```c++
        Well well(wellName,
                  group,
                  timeStep,
                  0,
                  headI, headJ,
                  ref_depth,
                  WellType(preferredPhase),
```
```c++
WellType::WellType(const Phase phase)
    : WellType { true, phase }
{}
```

Then we later update whether it is a producer or injector based on WCONP* keywords or WCONJ* keywords. 

For the specific DATA file, 

the order of the keywords are 

WELSPECS
COMPDAT
WINJGAS
WCONINJE

Then it is a decision about whether we want to let this DATA pass. 
I am okay to let is pass. If it happens that the well is a producer, then WINJGAS setup is just not used. 

I am open with other suggestions also. 
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T13:22:02Z
body: > When writing new keyword handlers, we generally prefer to use compiled item names instead of string literals. Other than that this looks good to me, with a couple of opportunities to use `move()` instead of copy.

Thanks for the reviewing comments, will come back to address them later to allow other more urgent issue. 
author_association: MEMBER
user: login: akva2
created_at: 2025-03-06T13:23:07Z
body: yes i understood the problem/point. my question is if  we should force sane sequential order. what is the semantics of the input file format? as far as i know, the semantics are sequential where order matters. if this is not actually the case then we indeed have to forgo the sanity check.
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T14:49:30Z
body: I think for the problematic DATA files, the way they specify the things, I mean specifying WINJGAS before making it a injector through WCONINJE. 

```
WELSPECS
COMPDAT
WINJGAS
WCONINJE
```
we should make OPM can handle it. Otherwise, in the future we might face complaining from the user side and asking the users to modify the DATA file is not always easy. 

What we can do (not in this PR), we can have a consistence checking for each report steps when all the relevant keywords are parsed. 

For example if happens for one report step, the following situation specification happens, 
```
WELSPECS
COMPDAT
WINJGAS
WCONINJE (with or without this one)
WCONPROD
```

During the consistence checking, it shows indeed WINJGAS is specified for a producer, we should throw and gives helpful instructions for the users. 

author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T15:05:10Z
body: > > When writing new keyword handlers, we generally prefer to use compiled item names instead of string literals. Other than that this looks good to me, with a couple of opportunities to use `move()` instead of copy.
> 
> Thanks for the reviewing comments, will come back to address them later to allow other more urgent issue.

I believe I addressed the reviewing comments. 
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T15:05:43Z
body: jenkins build this please
author_association: MEMBER
user: login: bska
created_at: 2025-03-06T15:41:41Z
body: >> When writing new keyword handlers, we generally prefer to use compiled item names instead of string literals
>
> I believe I addressed the reviewing comments.

You did for `WELLSTRE`, but `handleWINJGAS()` still uses string literals to identify items.
author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T16:19:25Z
body: > > > When writing new keyword handlers, we generally prefer to use compiled item names instead of string literals
> > 
> > 
> > I believe I addressed the reviewing comments.
> 
> You did for `WELLSTRE`, but `handleWINJGAS()` still uses string literals to identify items.

Oh, sorry for missing it.  now it should be updated. 

author_association: MEMBER
user: login: GitPaean
created_at: 2025-03-06T16:19:35Z
body: jenkins build this please
author_association: MEMBER