Skip to content

Commit 57b6d2c

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 326ae0d commit 57b6d2c

9 files changed

Lines changed: 307 additions & 13 deletions

File tree

ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline&
4646
RimEclipseCase& eclipseCase,
4747
const std::vector<RimWellPath*>& wellPaths,
4848
const std::vector<QDateTime>& dates,
49-
const std::set<const RimWellPath*>& mswWells )
49+
const std::set<const RimWellPath*>& mswWells,
50+
bool alignColumns )
5051
{
5152
QString result;
5253

@@ -71,7 +72,7 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline&
7172
// Generate section for each date
7273
for ( const auto& date : dates )
7374
{
74-
QString dateSection = generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells );
75+
QString dateSection = generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells, alignColumns );
7576
if ( !dateSection.isEmpty() )
7677
{
7778
result += dateSection;
@@ -131,7 +132,8 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
131132
RimEclipseCase& eclipseCase,
132133
const std::vector<RimWellPath*>& wellPaths,
133134
const QDateTime& date,
134-
const std::set<const RimWellPath*>& mswWells )
135+
const std::set<const RimWellPath*>& mswWells,
136+
bool alignColumns )
135137
{
136138
// Keyword priority order for output
137139
static const std::vector<QString> keywordOrder = { "WELSPECS",
@@ -154,8 +156,11 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
154156

155157
QString result;
156158

159+
auto serializeKeyword = [&]( const Opm::DeckKeyword& kw )
160+
{ return alignColumns ? RimKeywordFactory::deckKeywordToAlignedString( kw ) : RimKeywordFactory::deckKeywordToString( kw ); };
161+
157162
// Generate DATES keyword
158-
result += RimKeywordFactory::deckKeywordToString( RimKeywordFactory::datesKeyword( date ) ) + "\n";
163+
result += serializeKeyword( RimKeywordFactory::datesKeyword( date ) ) + "\n";
159164

160165
// Records for each keyword name are accumulated across wells, then serialised once below.
161166
std::map<QString, Opm::DeckKeyword> keywordBlocks;
@@ -196,7 +201,7 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
196201
auto appendKeywordText = [&]( const Opm::DeckKeyword& kw )
197202
{
198203
if ( kw.size() == 0 && !kw.isDataKeyword() ) return;
199-
result += RimKeywordFactory::deckKeywordToString( kw );
204+
result += serializeKeyword( kw );
200205
result += "\n";
201206
};
202207

ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,14 @@ class RicScheduleDataGenerator
4848
// mswWells lists the wells for which the multi-segment-well keywords (WELSEGS, COMPSEGS,
4949
// WSEGVALV, WSEGAICD) are exported. Wells not in the set get no MSW keywords; an empty set
5050
// suppresses MSW export for all wells.
51+
// When alignColumns is true, keywords are serialised with a "--"-prefixed column-header comment
52+
// and right-aligned, fixed-width columns instead of the compact default form.
5153
static QString generateSchedule( const RimWellEventTimeline& timeline,
5254
RimEclipseCase& eclipseCase,
5355
const std::vector<RimWellPath*>& wellPaths,
5456
const std::vector<QDateTime>& dates,
55-
const std::set<const RimWellPath*>& mswWells );
57+
const std::set<const RimWellPath*>& mswWells,
58+
bool alignColumns = false );
5659

5760
// Collect all unique dates from all wells' timelines
5861
static std::vector<QDateTime> collectAllDates( const RimWellEventTimeline& timeline, const std::vector<RimWellPath*>& wellPaths );
@@ -63,7 +66,8 @@ class RicScheduleDataGenerator
6366
RimEclipseCase& eclipseCase,
6467
const std::vector<RimWellPath*>& wellPaths,
6568
const QDateTime& date,
66-
const std::set<const RimWellPath*>& mswWells );
69+
const std::set<const RimWellPath*>& mswWells,
70+
bool alignColumns );
6771

6872
static std::optional<Opm::DeckKeyword>
6973
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: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,13 @@ RimcWellEventTimeline_generateSchedule::RimcWellEventTimeline_generateSchedule(
578578
"",
579579
"Wells for which multi-segment-well keywords (WELSEGS, COMPSEGS, WSEGVALV, WSEGAICD) are "
580580
"exported" );
581+
CAF_PDM_InitScriptableField( &m_alignColumns,
582+
"AlignColumns",
583+
false,
584+
"",
585+
"",
586+
"",
587+
"Emit a column-header comment and right-aligned, fixed-width columns instead of the compact form" );
581588
}
582589

583590
//--------------------------------------------------------------------------------------------------
@@ -630,7 +637,8 @@ std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_generateSche
630637
std::vector<RimWellPath*> mswWellPaths = m_exportMswForWells.ptrReferencedObjectsByType();
631638
std::set<const RimWellPath*> mswWells( mswWellPaths.begin(), mswWellPaths.end() );
632639

633-
QString scheduleText = RicScheduleDataGenerator::generateSchedule( *timeline, *eclipseCase, wellPathsWithEvents, dates, mswWells );
640+
QString scheduleText =
641+
RicScheduleDataGenerator::generateSchedule( *timeline, *eclipseCase, wellPathsWithEvents, dates, mswWells, m_alignColumns() );
634642

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

ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h

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

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)