mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-25 06:38:13 -05:00
Well Event: Emit RPTRST/RPTSCHED as proper mnemonic lists
Schedule-level keywords whose OPM ParserRecord is a single ALL-sized item
(RPTRST, RPTSCHED, ...) are not positional — they take a free-form list
of mnemonic tokens like `BASIC=2 DEN ROCKC NORST=1`. Previously each
`keyword_data` entry became its own DeckItem, and OPM emitted them
either as truncated positional values or as a quoted, space-collapsed
single string.
Detect that schema shape in `RifEventKeywordFormatter::formatKeyword`
and pack the user's entries into one RawString DeckItem of mnemonic
tokens: `KEY` for FLAG items, `KEY=VALUE` otherwise.
To distinguish flag mnemonics (`DEN`) from `KEY=1`-style entries, add a
new `FLAG` value to `RimWellEventKeywordItem::ItemType` plumbed end to
end (Python -> GRPC -> Rimc -> storage -> formatter). The Python API
now maps `bool True` to FLAG and silently drops `bool False`.
For positional keywords (WCONHIST etc.) FLAG items have no value to
emit and are rendered as default markers ("1*").
A `rawStringItem` helper is added to `RifOpmDeckTools` so the
mnemonic-list path can produce DeckItems whose OPM output is not
quoted.
This commit is contained in:
1 parent
9ada4eed09
commit
d2e7bed6b8
12 files changed
+243
-63
No files matched your search
@@ -33,6 +33,7 @@
|
||||
#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/ParserItem.hpp"
|
||||
#include "opm/input/eclipse/Parser/ParserKeyword.hpp"
|
||||
#include "opm/input/eclipse/Parser/ParserKeywords/W.hpp"
|
||||
#include "opm/input/eclipse/Parser/ParserRecord.hpp"
|
||||
@@ -56,80 +57,132 @@ QString RifEventKeywordFormatter::formatKeyword( const QString& keywordName, con
|
||||
const Opm::ParserKeyword& parserKw = parser.getKeyword( kwName );
|
||||
Opm::DeckKeyword kw( parserKw );
|
||||
|
||||
// Eclipse keyword records are positional: items must be emitted in the schema-defined order
|
||||
// regardless of how the caller supplied them (e.g. Python dict insertion order).
|
||||
std::unordered_map<std::string, const RimWellEventKeywordItem*> userItemsByName;
|
||||
userItemsByName.reserve( items.size() );
|
||||
for ( const auto* item : items )
|
||||
{
|
||||
userItemsByName.emplace( item->itemName().toStdString(), item );
|
||||
}
|
||||
|
||||
const Opm::ParserRecord& parserRecord = parserKw.getRecord( 0 );
|
||||
|
||||
// Determine the highest canonical index that the user actually provided; trailing
|
||||
// unspecified items are dropped, intermediate gaps become default markers ("1*").
|
||||
std::optional<size_t> lastProvidedIdx;
|
||||
std::unordered_set<std::string> canonicalNames;
|
||||
canonicalNames.reserve( parserRecord.size() );
|
||||
for ( size_t i = 0; i < parserRecord.size(); ++i )
|
||||
{
|
||||
const std::string& name = parserRecord.get( i ).name();
|
||||
canonicalNames.insert( name );
|
||||
if ( userItemsByName.contains( name ) )
|
||||
{
|
||||
lastProvidedIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
auto appendDeckItem = [&]( std::vector<Opm::DeckItem>& out, const std::string& name, const RimWellEventKeywordItem* item )
|
||||
auto stringValue = []( const RimWellEventKeywordItem* item ) -> std::string
|
||||
{
|
||||
switch ( item->itemType() )
|
||||
{
|
||||
case RimWellEventKeywordItem::ItemType::STRING:
|
||||
out.push_back( RifOpmDeckTools::item( name, item->stringValue().toStdString() ) );
|
||||
break;
|
||||
return item->stringValue().toStdString();
|
||||
case RimWellEventKeywordItem::ItemType::INTEGER:
|
||||
out.push_back( RifOpmDeckTools::item( name, item->intValue() ) );
|
||||
break;
|
||||
return std::to_string( item->intValue() );
|
||||
case RimWellEventKeywordItem::ItemType::DOUBLE:
|
||||
out.push_back( RifOpmDeckTools::item( name, item->doubleValue() ) );
|
||||
break;
|
||||
return std::to_string( item->doubleValue() );
|
||||
case RimWellEventKeywordItem::ItemType::FLAG:
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
std::vector<Opm::DeckItem> deckItems;
|
||||
if ( lastProvidedIdx.has_value() )
|
||||
// RPTRST / RPTSCHED-style keywords have a single ALL-sized item that holds a free-form
|
||||
// list of mnemonics ("BASIC=2 DEN ROCKC ..."). Emit each user-supplied key either as
|
||||
// "KEY=VALUE" (typed value) or bare "KEY" (FLAG), all packed into one DeckItem.
|
||||
const bool isMnemonicList = parserRecord.size() == 1 && parserRecord.get( 0 ).sizeType() == Opm::ParserItem::item_size::ALL;
|
||||
|
||||
if ( isMnemonicList )
|
||||
{
|
||||
deckItems.reserve( *lastProvidedIdx + 1 );
|
||||
for ( size_t i = 0; i <= *lastProvidedIdx; ++i )
|
||||
std::vector<std::string> tokens;
|
||||
tokens.reserve( items.size() );
|
||||
for ( const auto* item : items )
|
||||
{
|
||||
const std::string& name = parserRecord.get( i ).name();
|
||||
auto it = userItemsByName.find( name );
|
||||
if ( it == userItemsByName.end() )
|
||||
const std::string name = item->itemName().toStdString();
|
||||
if ( item->itemType() == RimWellEventKeywordItem::ItemType::FLAG )
|
||||
{
|
||||
deckItems.push_back( RifOpmDeckTools::defaultItem( name ) );
|
||||
tokens.push_back( name );
|
||||
}
|
||||
else
|
||||
{
|
||||
appendDeckItem( deckItems, name, it->second );
|
||||
tokens.push_back( name + "=" + stringValue( item ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Items not in the keyword's schema are still emitted (preserves behaviour for
|
||||
// mnemonic-list keywords such as RPTRST/RPTSCHED) but in caller-supplied order
|
||||
// after the canonical block.
|
||||
for ( const auto* item : items )
|
||||
{
|
||||
const std::string name = item->itemName().toStdString();
|
||||
if ( canonicalNames.contains( name ) ) continue;
|
||||
appendDeckItem( deckItems, name, item );
|
||||
if ( !tokens.empty() )
|
||||
{
|
||||
std::vector<Opm::DeckItem> deckItems;
|
||||
deckItems.push_back( RifOpmDeckTools::rawStringItem( parserRecord.get( 0 ).name(), std::move( tokens ) ) );
|
||||
kw.addRecord( Opm::DeckRecord{ std::move( deckItems ) } );
|
||||
}
|
||||
}
|
||||
|
||||
if ( !deckItems.empty() )
|
||||
else
|
||||
{
|
||||
kw.addRecord( Opm::DeckRecord{ std::move( deckItems ) } );
|
||||
// Positional keyword (WCONHIST, WELTARG, ...): emit items in the schema-defined order
|
||||
// regardless of caller-supplied order (e.g. Python dict insertion order).
|
||||
std::unordered_map<std::string, const RimWellEventKeywordItem*> userItemsByName;
|
||||
userItemsByName.reserve( items.size() );
|
||||
for ( const auto* item : items )
|
||||
{
|
||||
userItemsByName.emplace( item->itemName().toStdString(), item );
|
||||
}
|
||||
|
||||
// Highest canonical index actually provided by the user. Trailing unspecified items are
|
||||
// dropped, intermediate gaps become default markers ("1*").
|
||||
std::optional<size_t> lastProvidedIdx;
|
||||
std::unordered_set<std::string> canonicalNames;
|
||||
canonicalNames.reserve( parserRecord.size() );
|
||||
for ( size_t i = 0; i < parserRecord.size(); ++i )
|
||||
{
|
||||
const std::string& name = parserRecord.get( i ).name();
|
||||
canonicalNames.insert( name );
|
||||
if ( userItemsByName.contains( name ) )
|
||||
{
|
||||
lastProvidedIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
auto appendDeckItem = [&]( std::vector<Opm::DeckItem>& out, const std::string& name, const RimWellEventKeywordItem* item )
|
||||
{
|
||||
switch ( item->itemType() )
|
||||
{
|
||||
case RimWellEventKeywordItem::ItemType::STRING:
|
||||
out.push_back( RifOpmDeckTools::item( name, item->stringValue().toStdString() ) );
|
||||
break;
|
||||
case RimWellEventKeywordItem::ItemType::INTEGER:
|
||||
out.push_back( RifOpmDeckTools::item( name, item->intValue() ) );
|
||||
break;
|
||||
case RimWellEventKeywordItem::ItemType::DOUBLE:
|
||||
out.push_back( RifOpmDeckTools::item( name, item->doubleValue() ) );
|
||||
break;
|
||||
case RimWellEventKeywordItem::ItemType::FLAG:
|
||||
// FLAG only makes sense for mnemonic-list keywords; for positional schemas
|
||||
// there is no meaningful value to emit, so render as a default marker.
|
||||
out.push_back( RifOpmDeckTools::defaultItem( name ) );
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<Opm::DeckItem> deckItems;
|
||||
if ( lastProvidedIdx.has_value() )
|
||||
{
|
||||
deckItems.reserve( *lastProvidedIdx + 1 );
|
||||
for ( size_t i = 0; i <= *lastProvidedIdx; ++i )
|
||||
{
|
||||
const std::string& name = parserRecord.get( i ).name();
|
||||
auto it = userItemsByName.find( name );
|
||||
if ( it == userItemsByName.end() )
|
||||
{
|
||||
deckItems.push_back( RifOpmDeckTools::defaultItem( name ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
appendDeckItem( deckItems, name, it->second );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Items not in the keyword's schema are still emitted in caller-supplied order
|
||||
// after the canonical block.
|
||||
for ( const auto* item : items )
|
||||
{
|
||||
const std::string name = item->itemName().toStdString();
|
||||
if ( canonicalNames.contains( name ) ) continue;
|
||||
appendDeckItem( deckItems, name, item );
|
||||
}
|
||||
|
||||
if ( !deckItems.empty() )
|
||||
{
|
||||
kw.addRecord( Opm::DeckRecord{ std::move( deckItems ) } );
|
||||
}
|
||||
}
|
||||
|
||||
// Return empty string if keyword creation failed
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "opm/input/eclipse/Deck/DeckItem.hpp"
|
||||
#include "opm/input/eclipse/Units/Dimension.hpp"
|
||||
#include "opm/input/eclipse/Utility/Typetools.hpp"
|
||||
|
||||
namespace RifOpmDeckTools
|
||||
{
|
||||
@@ -157,4 +158,16 @@ Opm::DeckItem item( std::string name, std::vector<std::string> values )
|
||||
return item1;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Raw-string DeckItem: emitted by OPM DeckOutput without surrounding quotes.
|
||||
/// Used for free-form mnemonic lists (e.g. RPTRST tokens like "BASIC=2", "DEN").
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
Opm::DeckItem rawStringItem( std::string name, std::vector<std::string> values )
|
||||
{
|
||||
Opm::DeckItem item1( name, Opm::RawString{} );
|
||||
for ( const auto& value : values )
|
||||
item1.push_back( Opm::RawString{ value } );
|
||||
return item1;
|
||||
}
|
||||
|
||||
} // namespace RifOpmDeckTools
|
||||
@@ -42,5 +42,6 @@ Opm::DeckItem optionalItem( std::string name, std::optional<double> value );
|
||||
Opm::DeckItem defaultItem( std::string name );
|
||||
Opm::DeckItem item( std::string name, std::set<std::string> values );
|
||||
Opm::DeckItem item( std::string name, std::vector<std::string> values );
|
||||
Opm::DeckItem rawStringItem( std::string name, std::vector<std::string> values );
|
||||
|
||||
} // namespace RifOpmDeckTools
|
||||
@@ -98,6 +98,17 @@ void RimKeywordEvent::addDoubleItem( const QString& name, double value )
|
||||
m_items.push_back( item );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimKeywordEvent::addFlagItem( const QString& name )
|
||||
{
|
||||
auto* item = new RimWellEventKeywordItem();
|
||||
item->setItemName( name );
|
||||
item->setFlag();
|
||||
m_items.push_back( item );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -46,6 +46,7 @@ public:
|
||||
void addStringItem( const QString& name, const QString& value );
|
||||
void addIntItem( const QString& name, int value );
|
||||
void addDoubleItem( const QString& name, double value );
|
||||
void addFlagItem( const QString& name );
|
||||
|
||||
std::vector<class RimWellEventKeywordItem*> items() const;
|
||||
|
||||
|
||||
@@ -98,6 +98,17 @@ void RimWellEventKeyword::addDoubleItem( const QString& name, double value )
|
||||
m_items.push_back( item );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellEventKeyword::addFlagItem( const QString& name )
|
||||
{
|
||||
auto* item = new RimWellEventKeywordItem();
|
||||
item->setItemName( name );
|
||||
item->setFlag();
|
||||
m_items.push_back( item );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -45,6 +45,7 @@ public:
|
||||
void addStringItem( const QString& name, const QString& value );
|
||||
void addIntItem( const QString& name, int value );
|
||||
void addDoubleItem( const QString& name, double value );
|
||||
void addFlagItem( const QString& name );
|
||||
|
||||
std::vector<class RimWellEventKeywordItem*> items() const;
|
||||
|
||||
|
||||
@@ -116,6 +116,14 @@ void RimWellEventKeywordItem::setDoubleValue( double value )
|
||||
m_doubleValue = value;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellEventKeywordItem::setFlag()
|
||||
{
|
||||
m_itemType = ItemType::FLAG;
|
||||
}
|
||||
|
||||
namespace caf
|
||||
{
|
||||
template <>
|
||||
@@ -124,6 +132,7 @@ void AppEnum<RimWellEventKeywordItem::ItemType>::setUp()
|
||||
addItem( RimWellEventKeywordItem::ItemType::STRING, "STRING", "String" );
|
||||
addItem( RimWellEventKeywordItem::ItemType::INTEGER, "INTEGER", "Integer" );
|
||||
addItem( RimWellEventKeywordItem::ItemType::DOUBLE, "DOUBLE", "Double" );
|
||||
addItem( RimWellEventKeywordItem::ItemType::FLAG, "FLAG", "Flag" );
|
||||
setDefault( RimWellEventKeywordItem::ItemType::STRING );
|
||||
}
|
||||
} // namespace caf
|
||||
@@ -38,7 +38,8 @@ public:
|
||||
{
|
||||
STRING,
|
||||
INTEGER,
|
||||
DOUBLE
|
||||
DOUBLE,
|
||||
FLAG
|
||||
};
|
||||
|
||||
RimWellEventKeywordItem();
|
||||
@@ -56,6 +57,7 @@ public:
|
||||
void setStringValue( const QString& value );
|
||||
void setIntValue( int value );
|
||||
void setDoubleValue( double value );
|
||||
void setFlag();
|
||||
|
||||
private:
|
||||
caf::PdmField<QString> m_itemName;
|
||||
|
||||
@@ -406,9 +406,13 @@ std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_addWellKeywo
|
||||
}
|
||||
event->addDoubleItem( name, doubleValue );
|
||||
}
|
||||
else if ( type == "FLAG" )
|
||||
{
|
||||
event->addFlagItem( name );
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::unexpected( QString( "Unknown item type: %1. Must be STRING, INT, or DOUBLE" ).arg( type ) );
|
||||
return std::unexpected( QString( "Unknown item type: %1. Must be STRING, INT, DOUBLE, or FLAG" ).arg( type ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,9 +502,13 @@ std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_addKeywordEv
|
||||
}
|
||||
event->addDoubleItem( name, doubleValue );
|
||||
}
|
||||
else if ( type == "FLAG" )
|
||||
{
|
||||
event->addFlagItem( name );
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::unexpected( QString( "Unknown item type: %1. Must be STRING, INT, or DOUBLE" ).arg( type ) );
|
||||
return std::unexpected( QString( "Unknown item type: %1. Must be STRING, INT, DOUBLE, or FLAG" ).arg( type ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1895,3 +1895,55 @@ class TestScheduleKeywordEvents:
|
||||
)
|
||||
|
||||
assert event is not None, "Event with mixed types should be created"
|
||||
|
||||
def test_rptrst_mnemonic_output(self, project_with_case_and_well):
|
||||
"""RPTRST/RPTSCHED are mnemonic-list keywords. bool True must emit a
|
||||
bare KEY, int/float/str must emit KEY=VALUE, bool False must be omitted.
|
||||
"""
|
||||
project, case, timeline = project_with_case_and_well
|
||||
well_path = project.well_paths()[0]
|
||||
|
||||
# Schedule generation requires at least one well event so a well path is
|
||||
# selected for output; the assertions below target only the RPTRST block.
|
||||
timeline.add_control_event(
|
||||
event_date="2024-01-01",
|
||||
well_path=well_path,
|
||||
control_mode="ORAT",
|
||||
control_value=1000.0,
|
||||
oil_rate=1000.0,
|
||||
is_producer=True,
|
||||
)
|
||||
|
||||
timeline.add_keyword_event(
|
||||
event_date="2024-01-01",
|
||||
keyword_name="RPTRST",
|
||||
keyword_data={
|
||||
"BASIC": 2,
|
||||
"DEN": True,
|
||||
"ROCKC": True,
|
||||
"RPORV": True,
|
||||
"RFIP": True,
|
||||
"FLOWS": True,
|
||||
"NORST": 1,
|
||||
"FLORES": True,
|
||||
"OBSOLETE": False,
|
||||
},
|
||||
)
|
||||
|
||||
schedule_text = timeline.generate_schedule_text(eclipse_case=case)
|
||||
print(f"\nRPTRST mnemonic output:\n{schedule_text}")
|
||||
|
||||
assert "RPTRST" in schedule_text
|
||||
rptrst_block = schedule_text.split("RPTRST", 1)[1].split("/", 1)[0]
|
||||
|
||||
# Keyed mnemonics rendered as KEY=VALUE.
|
||||
assert "BASIC=2" in rptrst_block
|
||||
assert "NORST=1" in rptrst_block
|
||||
# Flag mnemonics rendered as bare tokens. Whitespace-bounded so we don't accept
|
||||
# accidental substring matches like 'DEN' inside another token.
|
||||
for flag in ("DEN", "ROCKC", "RPORV", "RFIP", "FLOWS", "FLORES"):
|
||||
assert f" {flag} " in rptrst_block or rptrst_block.rstrip().endswith(
|
||||
f" {flag}"
|
||||
), f"flag {flag!r} missing from RPTRST output: {rptrst_block!r}"
|
||||
# False-valued flag must be omitted entirely.
|
||||
assert "OBSOLETE" not in rptrst_block
|
||||
@@ -121,15 +121,24 @@ def add_well_keyword_event(
|
||||
item_values = []
|
||||
|
||||
for name, value in keyword_data.items():
|
||||
# Handle bool before int (bool is subclass of int in Python).
|
||||
# bool semantics: True -> flag (emitted as bare KEY in mnemonic-list keywords,
|
||||
# ignored elsewhere); False -> the entry is dropped entirely.
|
||||
if isinstance(value, bool):
|
||||
if not value:
|
||||
continue
|
||||
item_names.append(name)
|
||||
item_types.append("FLAG")
|
||||
# The value is ignored for FLAG items but must be a non-empty string;
|
||||
# empty strings get dropped by the GRPC vector<string> serialization.
|
||||
item_values.append("1")
|
||||
continue
|
||||
|
||||
item_names.append(name)
|
||||
|
||||
if isinstance(value, str):
|
||||
item_types.append("STRING")
|
||||
item_values.append(value)
|
||||
elif isinstance(value, bool):
|
||||
# Handle bool before int (bool is subclass of int in Python)
|
||||
item_types.append("INT")
|
||||
item_values.append("1" if value else "0")
|
||||
elif isinstance(value, int):
|
||||
item_types.append("INT")
|
||||
item_values.append(str(value))
|
||||
@@ -228,15 +237,24 @@ def add_keyword_event(
|
||||
item_values = []
|
||||
|
||||
for name, value in keyword_data.items():
|
||||
# Handle bool before int (bool is subclass of int in Python).
|
||||
# bool semantics: True -> flag (emitted as bare KEY in mnemonic-list keywords,
|
||||
# ignored elsewhere); False -> the entry is dropped entirely.
|
||||
if isinstance(value, bool):
|
||||
if not value:
|
||||
continue
|
||||
item_names.append(name)
|
||||
item_types.append("FLAG")
|
||||
# The value is ignored for FLAG items but must be a non-empty string;
|
||||
# empty strings get dropped by the GRPC vector<string> serialization.
|
||||
item_values.append("1")
|
||||
continue
|
||||
|
||||
item_names.append(name)
|
||||
|
||||
if isinstance(value, str):
|
||||
item_types.append("STRING")
|
||||
item_values.append(value)
|
||||
elif isinstance(value, bool):
|
||||
# Handle bool before int (bool is subclass of int in Python)
|
||||
item_types.append("INT")
|
||||
item_values.append("1" if value else "0")
|
||||
elif isinstance(value, int):
|
||||
item_types.append("INT")
|
||||
item_values.append(str(value))
|
||||
|
||||
Reference in new issue
Block a user