Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline&
const std::vector<RimWellPath*>& wellPaths,
const std::vector<QDateTime>& dates,
const std::set<const RimWellPath*>& mswWells,
bool firstDateAsComment )
bool firstDateAsComment,
bool alignColumns )
{
QString result;

Expand All @@ -76,7 +77,8 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline&
bool isFirstDate = true;
for ( const auto& date : dates )
{
QString dateSection = generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells, isFirstDate && firstDateAsComment );
QString dateSection =
generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells, isFirstDate && firstDateAsComment, alignColumns );
if ( !dateSection.isEmpty() )
{
result += dateSection;
Expand Down Expand Up @@ -138,7 +140,8 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
const std::vector<RimWellPath*>& wellPaths,
const QDateTime& date,
const std::set<const RimWellPath*>& mswWells,
bool dateAsComment )
bool dateAsComment,
bool alignColumns )
{
// Keyword priority order for output
static const std::vector<QString> keywordOrder = { "WELSPECS",
Expand All @@ -161,6 +164,9 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin

QString result;

auto serializeKeyword = [&]( const Opm::DeckKeyword& kw )
{ return alignColumns ? RimKeywordFactory::deckKeywordToAlignedString( kw ) : RimKeywordFactory::deckKeywordToString( kw ); };

// Generate DATES keyword, or a date comment when requested (e.g. for the first date, which
// equals the simulation start date and is rejected as a DATES entry by some simulators).
if ( dateAsComment )
Expand All @@ -171,7 +177,7 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
}
else
{
result += RimKeywordFactory::deckKeywordToString( RimKeywordFactory::datesKeyword( date ) ) + "\n";
result += serializeKeyword( RimKeywordFactory::datesKeyword( date ) ) + "\n";
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,15 @@ class RicScheduleDataGenerator
// When firstDateAsComment is true, the first (earliest) date is written as a comment line
// instead of a DATES keyword, since some commercial simulators reject a DATES entry that
// equals the simulation start date. Later dates are always emitted as DATES keywords.
// When alignColumns is true, keywords are serialised with a "--"-prefixed column-header comment
// and right-aligned, fixed-width columns instead of the compact default form.
static QString generateSchedule( const RimWellEventTimeline& timeline,
RimEclipseCase& eclipseCase,
const std::vector<RimWellPath*>& wellPaths,
const std::vector<QDateTime>& dates,
const std::set<const RimWellPath*>& mswWells,
bool firstDateAsComment = true );
bool firstDateAsComment = true,
bool alignColumns = false );

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

static std::optional<Opm::DeckKeyword>
generateWelspecsForWell( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, RimWellPath& well, const QDateTime& date );
Expand Down
169 changes: 169 additions & 0 deletions ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
#include "opm/input/eclipse/Deck/DeckKeyword.hpp"
#include "opm/input/eclipse/Deck/DeckOutput.hpp"
#include "opm/input/eclipse/Deck/DeckRecord.hpp"
#include "opm/input/eclipse/Parser/Parser.hpp"
#include "opm/input/eclipse/Parser/ParserKeyword.hpp"
#include "opm/input/eclipse/Parser/ParserKeywords/B.hpp"
#include "opm/input/eclipse/Parser/ParserKeywords/C.hpp"
#include "opm/input/eclipse/Parser/ParserKeywords/D.hpp"
Expand Down Expand Up @@ -768,4 +770,171 @@ QString deckKeywordToString( const Opm::DeckKeyword& keyword )
return QString::fromStdString( oss.str() );
}

namespace
{
//--------------------------------------------------------------------------------------------------
/// Render a single DeckItem to text the same way OPM's DeckOutput does (quoted strings, raw strings
/// unquoted, integers, doubles at precision 10), except defaults always render as "1*" per value
/// rather than being accumulated into "N*". Multi-value items are space-joined into one column.
//--------------------------------------------------------------------------------------------------
std::string renderDeckItemValue( const Opm::DeckItem& item )
{
const size_t numValues = item.data_size();
if ( numValues == 0 ) return "1*";

auto renderOne = [&]( size_t i ) -> std::string
{
if ( item.defaultApplied( i ) ) return "1*";

switch ( item.getType() )
{
case Opm::type_tag::integer:
return std::to_string( item.get<int>( i ) );
case Opm::type_tag::fdouble:
{
std::ostringstream oss;
oss.precision( 10 );
oss << item.get<double>( i );
return oss.str();
}
case Opm::type_tag::string:
return "'" + item.get<std::string>( i ) + "'";
case Opm::type_tag::raw_string:
return std::string( item.get<Opm::RawString>( i ) );
case Opm::type_tag::uda:
{
const Opm::UDAValue& uda = item.getData<Opm::UDAValue>()[i];
if ( uda.is<double>() )
{
std::ostringstream oss;
oss.precision( 10 );
oss << uda.get<double>();
return oss.str();
}
return "'" + uda.get<std::string>() + "'";
}
default:
return "1*";
}
};

std::string result;
for ( size_t i = 0; i < numValues; ++i )
{
if ( i > 0 ) result += " ";
result += renderOne( i );
}
return result;
}
} // namespace

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString deckKeywordToAlignedString( const Opm::DeckKeyword& keyword )
{
if ( keyword.name().empty() ) return {};

std::ostringstream oss;
oss << keyword.name() << "\n";

auto rightAlign = []( const std::string& s, size_t width ) { return std::string( width - s.size(), ' ' ) + s; };

// Emit the keyword as one or more groups of consecutive records that share the same ordered list
// of item names. Each group gets its own aligned column header. Tabular keywords (WCONHIST,
// COMPDAT, ...) form a single group; heterogeneous keywords (WELSEGS = header + segment records,
// TUNING = three distinct records) form one group per record shape.
const size_t numRecords = keyword.size();
size_t recordIndex = 0;
while ( recordIndex < numRecords )
{
// Group signature = item names of the first record in the group.
std::vector<std::string> header;
for ( const auto& item : keyword.getRecord( recordIndex ) )
{
header.push_back( item.name() );
}

// Collect every consecutive record matching this signature, rendering its values.
std::vector<std::vector<std::string>> rows;
size_t groupEnd = recordIndex;
for ( ; groupEnd < numRecords; ++groupEnd )
{
const Opm::DeckRecord& record = keyword.getRecord( groupEnd );
if ( record.size() != header.size() ) break;

bool sameNames = true;
size_t col = 0;
for ( const auto& item : record )
{
if ( item.name() != header[col] )
{
sameNames = false;
break;
}
++col;
}
if ( !sameNames ) break;

std::vector<std::string> row;
row.reserve( record.size() );
for ( const auto& item : record )
{
row.push_back( renderDeckItemValue( item ) );
}
rows.push_back( std::move( row ) );
}

const size_t numCols = header.size();
std::vector<size_t> width( numCols, 0 );
for ( size_t c = 0; c < numCols; ++c )
width[c] = header[c].size();
for ( const auto& row : rows )
for ( size_t c = 0; c < numCols; ++c )
width[c] = std::max( width[c], row[c].size() );

// Header comment line: "--" occupies the same two columns as the data-row indent so the
// header names line up with the values below them.
if ( numCols > 0 )
{
oss << "--";
for ( size_t c = 0; c < numCols; ++c )
{
if ( c > 0 ) oss << " ";
oss << rightAlign( header[c], width[c] );
}
oss << "\n";
}

for ( const auto& row : rows )
{
oss << " ";
for ( size_t c = 0; c < numCols; ++c )
{
if ( c > 0 ) oss << " ";
oss << rightAlign( row[c], width[c] );
}
oss << " /\n";
}

recordIndex = groupEnd;
}

// Emit the keyword-terminating slash for variable-size keywords, matching OPM
// (DeckKeyword::m_slashTerminated == !ParserKeyword::hasFixedSize()).
bool slashTerminated = true;
try
{
Opm::Parser parser;
slashTerminated = !parser.getKeyword( keyword.name() ).hasFixedSize();
}
catch ( ... )
{
slashTerminated = true;
}
if ( slashTerminated ) oss << "/\n";

return QString::fromStdString( oss.str() );
}

} // namespace RimKeywordFactory
6 changes: 6 additions & 0 deletions ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,10 @@ Opm::DeckKeyword editnncKeyword( const std::vector<RigSimulationInputTool::Trans
Opm::DeckKeyword datesKeyword( const QDateTime& date );
QString deckKeywordToString( const Opm::DeckKeyword& keyword );

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

} // namespace RimKeywordFactory
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,13 @@ RimcWellEventTimeline_generateSchedule::RimcWellEventTimeline_generateSchedule(
"",
"",
"Emit the first (simulation-start) date as a comment instead of a DATES keyword" );
CAF_PDM_InitScriptableField( &m_alignColumns,
"AlignColumns",
false,
"",
"",
"",
"Emit a column-header comment and right-aligned, fixed-width columns instead of the compact form" );
}

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

QString scheduleText =
RicScheduleDataGenerator::generateSchedule( *timeline, *eclipseCase, wellPathsWithEvents, dates, mswWells, m_firstDateAsComment() );
RicScheduleDataGenerator::generateSchedule( *timeline,
*eclipseCase,
wellPathsWithEvents,
dates,
mswWells,
m_firstDateAsComment(),
m_alignColumns() );

// Return the schedule text in a data container
auto* dataObject = new RimcDataContainerString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,5 @@ class RimcWellEventTimeline_generateSchedule : public caf::PdmObjectMethod
caf::PdmPtrField<RimEclipseCase*> m_eclipseCase;
caf::PdmPtrArrayField<RimWellPath*> m_exportMswForWells;
caf::PdmField<bool> m_firstDateAsComment;
caf::PdmField<bool> m_alignColumns;
};
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,13 @@ def main():
eclipse_case=case, export_msw_for_wells=[well_path]
)

# Generate the same schedule with align_columns=True, which adds a "--"-prefixed
# column-header comment per keyword and right-aligns the data into fixed-width
# columns. Only the formatting differs from the unaligned text above.
schedule_text_aligned = timeline.generate_schedule_text(
eclipse_case=case, export_msw_for_wells=[well_path], align_columns=True
)

if schedule_text:
print(f"\n Generated schedule text ({len(schedule_text)} characters)")
print(" " + "=" * 60)
Expand Down Expand Up @@ -308,15 +315,20 @@ def main():
print(f" - GRUPTREE entries: {schedule_text.count('GRUPTREE')}")
print(f" - TUNING entries: {schedule_text.count('TUNING')}")

# Save to file
output_file = "generated_schedule.sch"
with open(output_file, "w") as f:
# Save both formats to file
unaligned_file = "generate_schedule_unaligned.sch"
with open(unaligned_file, "w") as f:
f.write(schedule_text)
print(f"\n Schedule text saved to: {output_file}")
print(f"\n Unaligned schedule text saved to: {unaligned_file}")

aligned_file = "generate_schedule_aligned.sch"
with open(aligned_file, "w") as f:
f.write(schedule_text_aligned)
print(f" Aligned schedule text saved to: {aligned_file}")

# Show example of generated keywords
print("\n8. Example of generated Eclipse keywords:")
print(" (See generated_schedule.sch for complete output)")
print(f" (See {unaligned_file} / {aligned_file} for complete output)")
if "WELSEGS" in schedule_text:
print("\n Sample WELSEGS segment:")
for line in lines:
Expand Down
Loading
Loading