Skip to content

Commit 19c547c

Browse files
committed
Well Event: Add opt-in column-aligned schedule output
Add an align_columns option to schedule generation. When enabled, each keyword is serialized with a '--'-prefixed column-header comment and data rows right-aligned into fixed-width columns, instead of the compact default form. The default output is unchanged. A new RimKeywordFactory::deckKeywordToAlignedString introspects the OPM deck model and renders each item in its own column (consecutive defaults are not collapsed into 'N*'). The flag is threaded through RicScheduleDataGenerator and exposed on the GenerateSchedule scriptable method as align_columns in the Python generate_schedule_text wrapper.
1 parent bce2935 commit 19c547c

9 files changed

Lines changed: 317 additions & 13 deletions

File tree

ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline&
4949
const std::vector<RimWellPath*>& wellPaths,
5050
const std::vector<QDateTime>& dates,
5151
const std::set<const RimWellPath*>& mswWells,
52-
bool firstDateAsComment )
52+
bool firstDateAsComment,
53+
bool alignColumns )
5354
{
5455
QString result;
5556

@@ -76,7 +77,8 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline&
7677
bool isFirstDate = true;
7778
for ( const auto& date : dates )
7879
{
79-
QString dateSection = generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells, isFirstDate && firstDateAsComment );
80+
QString dateSection =
81+
generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells, isFirstDate && firstDateAsComment, alignColumns );
8082
if ( !dateSection.isEmpty() )
8183
{
8284
result += dateSection;
@@ -138,7 +140,8 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
138140
const std::vector<RimWellPath*>& wellPaths,
139141
const QDateTime& date,
140142
const std::set<const RimWellPath*>& mswWells,
141-
bool dateAsComment )
143+
bool dateAsComment,
144+
bool alignColumns )
142145
{
143146
// Keyword priority order for output
144147
static const std::vector<QString> keywordOrder = { "WELSPECS",
@@ -161,6 +164,9 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
161164

162165
QString result;
163166

167+
auto serializeKeyword = [&]( const Opm::DeckKeyword& kw )
168+
{ return alignColumns ? RimKeywordFactory::deckKeywordToAlignedString( kw ) : RimKeywordFactory::deckKeywordToString( kw ); };
169+
164170
// Generate DATES keyword, or a date comment when requested (e.g. for the first date, which
165171
// equals the simulation start date and is rejected as a DATES entry by some simulators).
166172
if ( dateAsComment )
@@ -171,7 +177,7 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
171177
}
172178
else
173179
{
174-
result += RimKeywordFactory::deckKeywordToString( RimKeywordFactory::datesKeyword( date ) ) + "\n";
180+
result += serializeKeyword( RimKeywordFactory::datesKeyword( date ) ) + "\n";
175181
}
176182

177183
// Records for each keyword name are accumulated across wells, then serialised once below.
@@ -213,7 +219,7 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
213219
auto appendKeywordText = [&]( const Opm::DeckKeyword& kw )
214220
{
215221
if ( kw.size() == 0 && !kw.isDataKeyword() ) return;
216-
result += RimKeywordFactory::deckKeywordToString( kw );
222+
result += serializeKeyword( kw );
217223
result += "\n";
218224
};
219225

ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,15 @@ class RicScheduleDataGenerator
5151
// When firstDateAsComment is true, the first (earliest) date is written as a comment line
5252
// instead of a DATES keyword, since some commercial simulators reject a DATES entry that
5353
// equals the simulation start date. Later dates are always emitted as DATES keywords.
54+
// When alignColumns is true, keywords are serialised with a "--"-prefixed column-header comment
55+
// and right-aligned, fixed-width columns instead of the compact default form.
5456
static QString generateSchedule( const RimWellEventTimeline& timeline,
5557
RimEclipseCase& eclipseCase,
5658
const std::vector<RimWellPath*>& wellPaths,
5759
const std::vector<QDateTime>& dates,
5860
const std::set<const RimWellPath*>& mswWells,
59-
bool firstDateAsComment = true );
61+
bool firstDateAsComment = true,
62+
bool alignColumns = false );
6063

6164
// Collect all unique dates from all wells' timelines
6265
static std::vector<QDateTime> collectAllDates( const RimWellEventTimeline& timeline, const std::vector<RimWellPath*>& wellPaths );
@@ -69,7 +72,8 @@ class RicScheduleDataGenerator
6972
const std::vector<RimWellPath*>& wellPaths,
7073
const QDateTime& date,
7174
const std::set<const RimWellPath*>& mswWells,
72-
bool dateAsComment = false );
75+
bool dateAsComment = false,
76+
bool alignColumns = false );
7377

7478
static std::optional<Opm::DeckKeyword>
7579
generateWelspecsForWell( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, RimWellPath& well, const QDateTime& date );

ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.cpp

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
#include "opm/input/eclipse/Deck/DeckKeyword.hpp"
4343
#include "opm/input/eclipse/Deck/DeckOutput.hpp"
4444
#include "opm/input/eclipse/Deck/DeckRecord.hpp"
45+
#include "opm/input/eclipse/Parser/Parser.hpp"
46+
#include "opm/input/eclipse/Parser/ParserKeyword.hpp"
4547
#include "opm/input/eclipse/Parser/ParserKeywords/B.hpp"
4648
#include "opm/input/eclipse/Parser/ParserKeywords/C.hpp"
4749
#include "opm/input/eclipse/Parser/ParserKeywords/D.hpp"
@@ -768,4 +770,171 @@ QString deckKeywordToString( const Opm::DeckKeyword& keyword )
768770
return QString::fromStdString( oss.str() );
769771
}
770772

773+
namespace
774+
{
775+
//--------------------------------------------------------------------------------------------------
776+
/// Render a single DeckItem to text the same way OPM's DeckOutput does (quoted strings, raw strings
777+
/// unquoted, integers, doubles at precision 10), except defaults always render as "1*" per value
778+
/// rather than being accumulated into "N*". Multi-value items are space-joined into one column.
779+
//--------------------------------------------------------------------------------------------------
780+
std::string renderDeckItemValue( const Opm::DeckItem& item )
781+
{
782+
const size_t numValues = item.data_size();
783+
if ( numValues == 0 ) return "1*";
784+
785+
auto renderOne = [&]( size_t i ) -> std::string
786+
{
787+
if ( item.defaultApplied( i ) ) return "1*";
788+
789+
switch ( item.getType() )
790+
{
791+
case Opm::type_tag::integer:
792+
return std::to_string( item.get<int>( i ) );
793+
case Opm::type_tag::fdouble:
794+
{
795+
std::ostringstream oss;
796+
oss.precision( 10 );
797+
oss << item.get<double>( i );
798+
return oss.str();
799+
}
800+
case Opm::type_tag::string:
801+
return "'" + item.get<std::string>( i ) + "'";
802+
case Opm::type_tag::raw_string:
803+
return std::string( item.get<Opm::RawString>( i ) );
804+
case Opm::type_tag::uda:
805+
{
806+
const Opm::UDAValue& uda = item.getData<Opm::UDAValue>()[i];
807+
if ( uda.is<double>() )
808+
{
809+
std::ostringstream oss;
810+
oss.precision( 10 );
811+
oss << uda.get<double>();
812+
return oss.str();
813+
}
814+
return "'" + uda.get<std::string>() + "'";
815+
}
816+
default:
817+
return "1*";
818+
}
819+
};
820+
821+
std::string result;
822+
for ( size_t i = 0; i < numValues; ++i )
823+
{
824+
if ( i > 0 ) result += " ";
825+
result += renderOne( i );
826+
}
827+
return result;
828+
}
829+
} // namespace
830+
831+
//--------------------------------------------------------------------------------------------------
832+
///
833+
//--------------------------------------------------------------------------------------------------
834+
QString deckKeywordToAlignedString( const Opm::DeckKeyword& keyword )
835+
{
836+
if ( keyword.name().empty() ) return {};
837+
838+
std::ostringstream oss;
839+
oss << keyword.name() << "\n";
840+
841+
auto rightAlign = []( const std::string& s, size_t width ) { return std::string( width - s.size(), ' ' ) + s; };
842+
843+
// Emit the keyword as one or more groups of consecutive records that share the same ordered list
844+
// of item names. Each group gets its own aligned column header. Tabular keywords (WCONHIST,
845+
// COMPDAT, ...) form a single group; heterogeneous keywords (WELSEGS = header + segment records,
846+
// TUNING = three distinct records) form one group per record shape.
847+
const size_t numRecords = keyword.size();
848+
size_t recordIndex = 0;
849+
while ( recordIndex < numRecords )
850+
{
851+
// Group signature = item names of the first record in the group.
852+
std::vector<std::string> header;
853+
for ( const auto& item : keyword.getRecord( recordIndex ) )
854+
{
855+
header.push_back( item.name() );
856+
}
857+
858+
// Collect every consecutive record matching this signature, rendering its values.
859+
std::vector<std::vector<std::string>> rows;
860+
size_t groupEnd = recordIndex;
861+
for ( ; groupEnd < numRecords; ++groupEnd )
862+
{
863+
const Opm::DeckRecord& record = keyword.getRecord( groupEnd );
864+
if ( record.size() != header.size() ) break;
865+
866+
bool sameNames = true;
867+
size_t col = 0;
868+
for ( const auto& item : record )
869+
{
870+
if ( item.name() != header[col] )
871+
{
872+
sameNames = false;
873+
break;
874+
}
875+
++col;
876+
}
877+
if ( !sameNames ) break;
878+
879+
std::vector<std::string> row;
880+
row.reserve( record.size() );
881+
for ( const auto& item : record )
882+
{
883+
row.push_back( renderDeckItemValue( item ) );
884+
}
885+
rows.push_back( std::move( row ) );
886+
}
887+
888+
const size_t numCols = header.size();
889+
std::vector<size_t> width( numCols, 0 );
890+
for ( size_t c = 0; c < numCols; ++c )
891+
width[c] = header[c].size();
892+
for ( const auto& row : rows )
893+
for ( size_t c = 0; c < numCols; ++c )
894+
width[c] = std::max( width[c], row[c].size() );
895+
896+
// Header comment line: "--" occupies the same two columns as the data-row indent so the
897+
// header names line up with the values below them.
898+
if ( numCols > 0 )
899+
{
900+
oss << "--";
901+
for ( size_t c = 0; c < numCols; ++c )
902+
{
903+
if ( c > 0 ) oss << " ";
904+
oss << rightAlign( header[c], width[c] );
905+
}
906+
oss << "\n";
907+
}
908+
909+
for ( const auto& row : rows )
910+
{
911+
oss << " ";
912+
for ( size_t c = 0; c < numCols; ++c )
913+
{
914+
if ( c > 0 ) oss << " ";
915+
oss << rightAlign( row[c], width[c] );
916+
}
917+
oss << " /\n";
918+
}
919+
920+
recordIndex = groupEnd;
921+
}
922+
923+
// Emit the keyword-terminating slash for variable-size keywords, matching OPM
924+
// (DeckKeyword::m_slashTerminated == !ParserKeyword::hasFixedSize()).
925+
bool slashTerminated = true;
926+
try
927+
{
928+
Opm::Parser parser;
929+
slashTerminated = !parser.getKeyword( keyword.name() ).hasFixedSize();
930+
}
931+
catch ( ... )
932+
{
933+
slashTerminated = true;
934+
}
935+
if ( slashTerminated ) oss << "/\n";
936+
937+
return QString::fromStdString( oss.str() );
938+
}
939+
771940
} // namespace RimKeywordFactory

ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,10 @@ Opm::DeckKeyword editnncKeyword( const std::vector<RigSimulationInputTool::Trans
8181
Opm::DeckKeyword datesKeyword( const QDateTime& date );
8282
QString deckKeywordToString( const Opm::DeckKeyword& keyword );
8383

84+
// Serialize a keyword as human-readable text: a "--"-prefixed column-header comment line listing
85+
// the item names, followed by data rows right-aligned into fixed-width columns with a two-space
86+
// indent. Unlike deckKeywordToString(), each item renders in its own column (consecutive defaults
87+
// are not collapsed into "N*"), so columns stay aligned.
88+
QString deckKeywordToAlignedString( const Opm::DeckKeyword& keyword );
89+
8490
} // namespace RimKeywordFactory

ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,13 @@ RimcWellEventTimeline_generateSchedule::RimcWellEventTimeline_generateSchedule(
585585
"",
586586
"",
587587
"Emit the first (simulation-start) date as a comment instead of a DATES keyword" );
588+
CAF_PDM_InitScriptableField( &m_alignColumns,
589+
"AlignColumns",
590+
false,
591+
"",
592+
"",
593+
"",
594+
"Emit a column-header comment and right-aligned, fixed-width columns instead of the compact form" );
588595
}
589596

590597
//--------------------------------------------------------------------------------------------------
@@ -638,7 +645,13 @@ std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_generateSche
638645
std::set<const RimWellPath*> mswWells( mswWellPaths.begin(), mswWellPaths.end() );
639646

640647
QString scheduleText =
641-
RicScheduleDataGenerator::generateSchedule( *timeline, *eclipseCase, wellPathsWithEvents, dates, mswWells, m_firstDateAsComment() );
648+
RicScheduleDataGenerator::generateSchedule( *timeline,
649+
*eclipseCase,
650+
wellPathsWithEvents,
651+
dates,
652+
mswWells,
653+
m_firstDateAsComment(),
654+
m_alignColumns() );
642655

643656
// Return the schedule text in a data container
644657
auto* dataObject = new RimcDataContainerString();

ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,5 @@ class RimcWellEventTimeline_generateSchedule : public caf::PdmObjectMethod
230230
caf::PdmPtrField<RimEclipseCase*> m_eclipseCase;
231231
caf::PdmPtrArrayField<RimWellPath*> m_exportMswForWells;
232232
caf::PdmField<bool> m_firstDateAsComment;
233+
caf::PdmField<bool> m_alignColumns;
233234
};

GrpcInterface/Python/rips/PythonExamples/wells_and_fractures/well_event_schedule.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ def main():
237237
eclipse_case=case, export_msw_for_wells=[well_path]
238238
)
239239

240+
# Generate the same schedule with align_columns=True, which adds a "--"-prefixed
241+
# column-header comment per keyword and right-aligns the data into fixed-width
242+
# columns. Only the formatting differs from the unaligned text above.
243+
schedule_text_aligned = timeline.generate_schedule_text(
244+
eclipse_case=case, export_msw_for_wells=[well_path], align_columns=True
245+
)
246+
240247
if schedule_text:
241248
print(f"\n Generated schedule text ({len(schedule_text)} characters)")
242249
print(" " + "=" * 60)
@@ -308,15 +315,20 @@ def main():
308315
print(f" - GRUPTREE entries: {schedule_text.count('GRUPTREE')}")
309316
print(f" - TUNING entries: {schedule_text.count('TUNING')}")
310317

311-
# Save to file
312-
output_file = "generated_schedule.sch"
313-
with open(output_file, "w") as f:
318+
# Save both formats to file
319+
unaligned_file = "generate_schedule_unaligned.sch"
320+
with open(unaligned_file, "w") as f:
314321
f.write(schedule_text)
315-
print(f"\n Schedule text saved to: {output_file}")
322+
print(f"\n Unaligned schedule text saved to: {unaligned_file}")
323+
324+
aligned_file = "generate_schedule_aligned.sch"
325+
with open(aligned_file, "w") as f:
326+
f.write(schedule_text_aligned)
327+
print(f" Aligned schedule text saved to: {aligned_file}")
316328

317329
# Show example of generated keywords
318330
print("\n8. Example of generated Eclipse keywords:")
319-
print(" (See generated_schedule.sch for complete output)")
331+
print(f" (See {unaligned_file} / {aligned_file} for complete output)")
320332
if "WELSEGS" in schedule_text:
321333
print("\n Sample WELSEGS segment:")
322334
for line in lines:

0 commit comments

Comments
 (0)