Repository: OPM/opm-simulators
Pull request #6090: PRT: Use {fmt} to format injection log
Author: login: akva2
Created: 2025-03-18T15:25:32Z | Merged: 2025-03-19T11:49:52Z

I'm working on adding support for RPTSCHED WELLS=2.

This appends connection level data to the log. In the process I realized two issues with current code;
1) the table format is dependent on group/well names being left aligned. This to properly fit the connection data and giving a sense of 'this is under this entry'.
2) the ijk-indices were misaligned

As fixing things like this with non-programmed table layouts is a major pain in the behind, I decided to fix the problem properly. In the process I also found various inconsistencies in the existing code, wrong centerings and such.

This is the first part of this work, starting with the injection log.

Before
```
:    INJ:   1,    1    :
:  BLOCK:   1,  1,  3 <-- this is the new data
```
After
```
:INJ   :   1,  1     :
: BLOCK:   1,  1,  3 :
```


Diff:
diff --git a/opm/simulators/flow/LogOutputHelper.cpp b/opm/simulators/flow/LogOutputHelper.cpp
index 326dce56b2c..ee7cb403540 100644
--- a/opm/simulators/flow/LogOutputHelper.cpp
+++ b/opm/simulators/flow/LogOutputHelper.cpp
@@ -32,14 +32,15 @@
 #include <opm/simulators/utils/PressureAverage.hpp>
 #include <opm/input/eclipse/Units/Units.hpp>
 
-#include <fmt/format.h>
-
 #include <algorithm>
 #include <cmath>
 #include <iomanip>
 #include <sstream>
+#include <string_view>
 #include <vector>
 
+#include <fmt/format.h>
+
 namespace {
 
 template <typename IJKString>
@@ -105,6 +106,31 @@ std::string producerCModeToString(const Opm::WellProducerCMode cmode)
     return cmodeStr;
 }
 
+template<class Array>
+std::string formatBorder(const Array& widths)
+{
+    std::string ss;
+    std::for_each(widths.begin(), widths.end(),
+                  [&ss](const auto w)
+                  { ss += fmt::format(":{:->{}}", "", w); });
+    ss += ':';
+
+    return ss;
+}
+
+template<std::size_t size>
+std::string formatTextRow(const std::array<int, size>& widths,
+                          const std::array<std::string_view, size>& entries)
+{
+    std::string ss;
+    std::for_each(widths.begin(), widths.end(),
+                  [&entries, &ss, i = 0](const auto w) mutable
+                  { ss += fmt::format(":{:^{}}", entries[i++], w); });
+    ss += ":\n";
+
+    return ss;
+}
+
 } // Namespace anonymous
 
 namespace Opm {
@@ -580,22 +606,98 @@ outputCumulativeReportRecord_(const std::vector<Scalar>& wellCum,
 template <typename Scalar>
 void LogOutputHelper<Scalar>::beginInjectionReport_() const
 {
-    const auto unitType = this->eclState_.getUnits().getType();
+    const auto& units = this->eclState_.getUnits();
+    const auto widths = std::array{8, 11, 6, 6, 6, 11, 11, 11, 11, 8, 8};
+
+    using namespace std::string_view_literals;
+
+    auto oil_rate =
+        [utype = units.getType()]()
+        {
+            switch (utype) {
+            case UnitSystem::UnitType::UNIT_TYPE_METRIC: return "SCM/DAY"sv;
+            case UnitSystem::UnitType::UNIT_TYPE_FIELD: return "STB/DAY"sv;
+            case UnitSystem::UnitType::UNIT_TYPE_LAB: return "SCC/HR"sv;
+            default: return "unsupp"sv;
+            }
+        };
+    auto wat_rate =
+        [utype = units.getType()]()
+        {
+            switch (utype) {
+            case UnitSystem::UnitType::UNIT_TYPE_METRIC: return "SCM/DAY"sv;
+            case UnitSystem::UnitType::UNIT_TYPE_FIELD: return "STB/DAY"sv;
+            case UnitSystem::UnitType::UNIT_TYPE_LAB: return "SCC/HR"sv;
+            default: return "unsupp"sv;
+            }
+        };
+    auto gas_rate =
+        [utype = units.getType()]()
+        {
+            switch (utype) {
+            case UnitSystem::UnitType::UNIT_TYPE_METRIC: return "SCM/DAY"sv;
+            case UnitSystem::UnitType::UNIT_TYPE_FIELD: return "MSCF/DAY"sv;
+            case UnitSystem::UnitType::UNIT_TYPE_LAB: return "SCC/HR"sv;
+            default: return "unsupp"sv;
+            }
+        };
+    auto res_vol =
+        [utype = units.getType()]()
+        {
+            switch (utype) {
+            case UnitSystem::UnitType::UNIT_TYPE_METRIC: return "RCM/DAY"sv;
+            case UnitSystem::UnitType::UNIT_TYPE_FIELD: return "RB/DAY"sv;
+            case UnitSystem::UnitType::UNIT_TYPE_LAB: return "RCC/HR"sv;
+            default: return "unsupp"sv;
+            }
+        };
+
+    const auto unitStrings = std::array{
+                                ""sv,
+                                ""sv,
+                                "OIL"sv,
+                                "WAT"sv,
+                                "GAS"sv,
+                                oil_rate(),
+                                wat_rate(),
+                                gas_rate(),
+                                res_vol(),
+                                std::string_view{units.name(UnitSystem::measure::pressure)},
+                                std::string_view{units.name(UnitSystem::measure::pressure)},
+                             };
 
     std::ostringstream ss;
-    ss << "\n=================================================== INJECTION REPORT ========================================\n"//===================== \n"
-       << ":  WELL  :  LOCATION : CTRL : CTRL : CTRL :    OIL    :   WATER   :    GAS    :   FLUID   : BHP OR : THP OR :\n"// STEADY-ST II       :\n"
-       << ":  NAME  :  (I,J,K)  : MODE : MODE : MODE :    RATE   :   RATE    :    RATE   :  RES.VOL. : CON.PR.: BLK.PR.:\n";// OR POTENTIAL       :\n";
-
-    if (unitType == UnitSystem::UnitType::UNIT_TYPE_METRIC) {
-        ss << ":        :           : OIL  : WAT  : GAS  :  SCM/DAY  :  SCM/DAY  :  SCM/DAY  :  RCM/DAY  :  BARSA :  BARSA :\n";//                    :\n";
-    } else if (unitType == UnitSystem::UnitType::UNIT_TYPE_FIELD) {
-        ss << ":        :           : OIL  : WAT  : GAS  :  STB/DAY  :  STB/DAY  :  MSCF/DAY :  RB/DAY   :  PSIA  :  PSIA  :\n";//                    :\n";
-    } else if (unitType == UnitSystem::UnitType::UNIT_TYPE_LAB) {
-        ss << ":        :           : OIL  : WAT  : GAS  :   SCC/HR  :  SCC/HR   :  SCC/HR   :  RCC/HR   :  ATMA  :  ATMA  :\n";//                    :\n";
-    }
-
-    ss << "=============================================================================================================";//=====================";
+    ss << fmt::format("\n{:=^109}\n", " INJECTION REPORT ")
+       << formatTextRow(widths,
+                        std::array{
+                            "WELL"sv,
+                            "LOCATION"sv,
+                            "CTRL"sv,
+                            "CTRL"sv,
+                            "CTRL"sv,
+                            "OIL"sv,
+                            "WATER"sv,
+                            "GAS"sv,
+                            "FLUID"sv,
+                            "BHP OR"sv,
+                            "THP OR"sv,
+                        })
+       << formatTextRow(widths,
+                        std::array{
+                            "NAME"sv,
+                            "(I,J,K)"sv,
+                            "MODE"sv,
+                            "MODE"sv,
+                            "MODE"sv,
+                            "RATE"sv,
+                            "RATE"sv,
+                            "RATE"sv,
+                            "RES.VOL."sv,
+                            "CON.PR."sv,
+                            "BLK.PR."sv,
+                        })
+       << formatTextRow(widths, unitStrings)
+       << fmt::format("{:=>109}", "");
 
     OpmLog::note(ss.str());
 }
@@ -603,9 +705,8 @@ void LogOutputHelper<Scalar>::beginInjectionReport_() const
 template <typename Scalar>
 void LogOutputHelper<Scalar>::endInjectionReport_() const
 {
-    const auto ss = std::string { ":--------:-----------:------:------:------:-----------:-----------:-----------:-----------:--------:--------:" };
-
-    OpmLog::note(ss);
+    const auto widths = std::array{8, 11, 6, 6, 6, 11, 11, 11, 11, 8, 8};
+    OpmLog::note(formatBorder(widths));
 }
 
 template<class Scalar>
@@ -617,30 +718,32 @@ outputInjectionReportRecord_(const std::vector<Scalar>& wellInj,
         wellInj[WellProdDataType::WellLocationi] >= 1;
 
     std::ostringstream ss;
-
-    ss << std::right << std::fixed << std::setprecision(0) << ':'
-       << std::setw(8) << wellInjNames[WellInjDataType::WellName] << ':';
+    ss << fmt::format(":{:<8}:", wellInjNames[WellInjDataType::WellName]);
 
     if (! isWellRecord) {
-        ss << std::setw(11) << "" << ':';
+        ss << fmt::format("{:11}:", "");
     } else {
-        ss << std::setw( 5) << wellInj[WellInjDataType::WellLocationi] << ','
-           << std::setw( 5) << wellInj[WellInjDataType::WellLocationj] << ':';
+        ss << fmt::format("{:>3},{:>3} {:>3}:",
+                          wellInj[WellInjDataType::WellLocationi],
+                          wellInj[WellInjDataType::WellLocationj],
+                          "");
     }
 
-    ss << std::setw( 6) << wellInjNames[WellInjDataType::CTRLModeOil] << ':'
-       << std::setw( 6) << wellInjNames[WellInjDataType::CTRLModeWat] << ':'
-       << std::setw( 6) << wellInjNames[WellInjDataType::CTRLModeGas] << ':' << std::setprecision(1)
-       << std::setw(11) << wellInj[WellInjDataType::OilRate]          << ':'
-       << std::setw(11) << wellInj[WellInjDataType::WaterRate]        << ':'
-       << std::setw(11) << wellInj[WellInjDataType::GasRate]          << ':'
-       << std::setw(11) << wellInj[WellInjDataType::FluidResVol]      << ':';
+    ss << fmt::format("{:>6}:{:>6}:{:>6}:{:>11.1f}:{:>11.1f}:{:>11.1f}:{:>11.1f}:",
+                      wellInjNames[WellInjDataType::CTRLModeOil],
+                      wellInjNames[WellInjDataType::CTRLModeWat],
+                      wellInjNames[WellInjDataType::CTRLModeGas],
+                      wellInj[WellInjDataType::OilRate],
+                      wellInj[WellInjDataType::WaterRate],
+                      wellInj[WellInjDataType::GasRate],
+                      wellInj[WellInjDataType::FluidResVol]);
 
     if (! isWellRecord) {
-        ss << std::setw(8) << "" << ':' << std::setw(8) << "" << ':'; //wellInj[WellInjDataType::SteadyStateII] << std::setw(10) << "\n"
+        ss << fmt::format("{0:8}:{0:8}:", "");
     } else {
-        ss << std::setw(8) << wellInj[WellInjDataType::BHP] << ':'
-           << std::setw(8) << wellInj[WellInjDataType::THP] << ':'; //wellInj[WellInjDataType::SteadyStateII] << std::setw(10) << "\n"
+        ss << fmt::format("{:>8.1f}:{:>8.1f}:",
+                          wellInj[WellInjDataType::BHP],
+                          wellInj[WellInjDataType::THP]);
     }
 
     OpmLog::note(ss.str());
diff --git a/tests/test_LogOutputHelper.cpp b/tests/test_LogOutputHelper.cpp
index d5f8d31cebf..d63cbada5f7 100644
--- a/tests/test_LogOutputHelper.cpp
+++ b/tests/test_LogOutputHelper.cpp
@@ -329,14 +329,14 @@ BOOST_FIXTURE_TEST_CASE(FipResv, LogNoteFixture)
 BOOST_FIXTURE_TEST_CASE(Injection, LogNoteFixture)
 {
     const auto reference = std::string {
-        R"(=================================================== INJECTION REPORT ========================================
-:  WELL  :  LOCATION : CTRL : CTRL : CTRL :    OIL    :   WATER   :    GAS    :   FLUID   : BHP OR : THP OR :
-:  NAME  :  (I,J,K)  : MODE : MODE : MODE :    RATE   :   RATE    :    RATE   :  RES.VOL. : CON.PR.: BLK.PR.:
-:        :           : OIL  : WAT  : GAS  :  STB/DAY  :  STB/DAY  :  MSCF/DAY :  RB/DAY   :  PSIA  :  PSIA  :
+        R"(============================================= INJECTION REPORT ==============================================
+:  WELL  : LOCATION  : CTRL : CTRL : CTRL :    OIL    :   WATER   :    GAS    :   FLUID   : BHP OR : THP OR :
+:  NAME  :  (I,J,K)  : MODE : MODE : MODE :   RATE    :   RATE    :   RATE    : RES.VOL.  :CON.PR. :BLK.PR. :
+:        :           : OIL  : WAT  : GAS  :  STB/DAY  :  STB/DAY  : MSCF/DAY  :  RB/DAY   :  PSIA  :  PSIA  :
 =============================================================================================================
-:   FIELD:           :      :      :      :        1.0:        2.0:        3.0:        4.0:        :        :
-:      G1:           :      :      :      :        5.0:        6.0:        7.0:        8.0:        :        :
-:     INJ:    1,    1:      :      :  GRAT:        9.0:       10.0:       11.0:       12.0:    13.0:    14.0:
+:FIELD   :           :      :      :      :        1.0:        2.0:        3.0:        4.0:        :        :
+:G1      :           :      :      :      :        5.0:        6.0:        7.0:        8.0:        :        :
+:INJ     :  1,  1    :      :      :  GRAT:        9.0:       10.0:       11.0:       12.0:    13.0:    14.0:
 :--------:-----------:------:------:------:-----------:-----------:-----------:-----------:--------:--------:
 )"
     };


Reviews:
user: login: bska
body: Looks good to me.  Thanks a lot for the clean-up.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-19T11:33:41Z
user: login: akva2
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T11:35:13Z
user: login: akva2
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T11:39:13Z
user: login: bska
body: Thanks a lot for the update.  This looks good to me now and I'll merge into master.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-19T11:49:40Z

Review comments:
diff_hunk: @@ -617,30 +718,32 @@ outputInjectionReportRecord_(const std::vector<Scalar>& wellInj,
         wellInj[WellProdDataType::WellLocationi] >= 1;
 
     std::ostringstream ss;
-
-    ss << std::right << std::fixed << std::setprecision(0) << ':'
-       << std::setw(8) << wellInjNames[WellInjDataType::WellName] << ':';
+    ss << fmt::format(":{:<8}:", wellInjNames[WellInjDataType::WellName]);
 
     if (! isWellRecord) {
-        ss << std::setw(11) << "" << ':';
+        ss << fmt::format("{:11}:", "");
     } else {
-        ss << std::setw( 5) << wellInj[WellInjDataType::WellLocationi] << ','
-           << std::setw( 5) << wellInj[WellInjDataType::WellLocationj] << ':';
+        ss << fmt::format("{:>3},{:>3} {:>3}:",
+                          wellInj[WellInjDataType::WellLocationi],
+                          wellInj[WellInjDataType::WellLocationj],
+                          "");
path: opm/simulators/flow/LogOutputHelper.cpp
user: login: bska
body: So if we need three digits for both I and J, then we'll get output of the form
```
123,456
```
Is that intentional?
created_at: 2025-03-19T11:24:25Z
author_association: MEMBER
diff_hunk: @@ -580,32 +606,107 @@ outputCumulativeReportRecord_(const std::vector<Scalar>& wellCum,
 template <typename Scalar>
 void LogOutputHelper<Scalar>::beginInjectionReport_() const
 {
-    const auto unitType = this->eclState_.getUnits().getType();
+    const auto& units = this->eclState_.getUnits();
+    const auto widths = std::array{8, 11, 6, 6, 6, 11, 11, 11, 11, 8, 8};
+
+    using namespace std::string_view_literals;
+
+    auto oil_rate =
+        [&units]()
path: opm/simulators/flow/LogOutputHelper.cpp
user: login: bska
body: Might as well capture `units.getType()` here, e.g.,
```C++
auto oil_rate = [utype = units.getType()]()
{
    switch (utype) {
    // ...
    }
};
```
since you're not really using a lot of the `units` object internally.
created_at: 2025-03-19T11:28:55Z
author_association: MEMBER
diff_hunk: @@ -617,30 +718,32 @@ outputInjectionReportRecord_(const std::vector<Scalar>& wellInj,
         wellInj[WellProdDataType::WellLocationi] >= 1;
 
     std::ostringstream ss;
-
-    ss << std::right << std::fixed << std::setprecision(0) << ':'
-       << std::setw(8) << wellInjNames[WellInjDataType::WellName] << ':';
+    ss << fmt::format(":{:<8}:", wellInjNames[WellInjDataType::WellName]);
 
     if (! isWellRecord) {
-        ss << std::setw(11) << "" << ':';
+        ss << fmt::format("{:11}:", "");
     } else {
-        ss << std::setw( 5) << wellInj[WellInjDataType::WellLocationi] << ','
-           << std::setw( 5) << wellInj[WellInjDataType::WellLocationj] << ':';
+        ss << fmt::format("{:>3},{:>3} {:>3}:",
+                          wellInj[WellInjDataType::WellLocationi],
+                          wellInj[WellInjDataType::WellLocationj],
+                          "");
path: opm/simulators/flow/LogOutputHelper.cpp
user: login: akva2
body: yes, that is the intent. it is dictated by column width (assuming i'm gonna stay "compatible"). with connection data it would be 123,456,789 which is exactly column width.
created_at: 2025-03-19T11:35:13Z
author_association: MEMBER
diff_hunk: @@ -580,32 +606,107 @@ outputCumulativeReportRecord_(const std::vector<Scalar>& wellCum,
 template <typename Scalar>
 void LogOutputHelper<Scalar>::beginInjectionReport_() const
 {
-    const auto unitType = this->eclState_.getUnits().getType();
+    const auto& units = this->eclState_.getUnits();
+    const auto widths = std::array{8, 11, 6, 6, 6, 11, 11, 11, 11, 8, 8};
+
+    using namespace std::string_view_literals;
+
+    auto oil_rate =
+        [&units]()
path: opm/simulators/flow/LogOutputHelper.cpp
user: login: akva2
body: sure.
created_at: 2025-03-19T11:39:13Z
author_association: MEMBER
diff_hunk: @@ -617,30 +718,32 @@ outputInjectionReportRecord_(const std::vector<Scalar>& wellInj,
         wellInj[WellProdDataType::WellLocationi] >= 1;
 
     std::ostringstream ss;
-
-    ss << std::right << std::fixed << std::setprecision(0) << ':'
-       << std::setw(8) << wellInjNames[WellInjDataType::WellName] << ':';
+    ss << fmt::format(":{:<8}:", wellInjNames[WellInjDataType::WellName]);
 
     if (! isWellRecord) {
-        ss << std::setw(11) << "" << ':';
+        ss << fmt::format("{:11}:", "");
     } else {
-        ss << std::setw( 5) << wellInj[WellInjDataType::WellLocationi] << ','
-           << std::setw( 5) << wellInj[WellInjDataType::WellLocationj] << ':';
+        ss << fmt::format("{:>3},{:>3} {:>3}:",
+                          wellInj[WellInjDataType::WellLocationi],
+                          wellInj[WellInjDataType::WellLocationj],
+                          "");
path: opm/simulators/flow/LogOutputHelper.cpp
user: login: bska
body: > with connection data it would be 123,456,789 which is exactly column width.

Right.  Okay, that makes sense.
created_at: 2025-03-19T11:49:17Z
author_association: MEMBER

PR comments:
user: login: akva2
created_at: 2025-03-19T10:01:08Z
body: jenkins build this please
author_association: MEMBER
user: login: akva2
created_at: 2025-03-19T10:26:15Z
body: jenkins build this please
author_association: MEMBER
user: login: akva2
created_at: 2025-03-19T11:20:05Z
body: jenkins build this please
author_association: MEMBER
user: login: akva2
created_at: 2025-03-19T11:39:28Z
body: jenkins build this please
author_association: MEMBER