mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-24 22:27:55 -05:00
#14878: Add FMU well-picks data model (RigWellPathFormations, RimWellFormationsFile/Collection)
Parse depth-based FMU well-pick formations.csv files in RifWellPathFormationReader and expose them as a project-level RimWellFormationsCollection of RimWellFormationsFile objects. RigWellPathFormations becomes a value type with top/base MD and TVD depths per formation, independent from the existing K-layer-index .lyr formation names.
This commit is contained in:
1 parent
fe887270e5
commit
37cacc9a41
13 files changed
+740
-23
No files matched your search
@@ -22,19 +22,53 @@
|
||||
#include <QStringList>
|
||||
#include <QTextStream>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The FMU formations.csv format is comma-separated, the "well pick" formats are semicolon-separated
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QChar detectDelimiter( const QString& line )
|
||||
{
|
||||
return line.contains( ',' ) ? ',' : ';';
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QStringList parseHeader( const QString& line )
|
||||
QStringList parseHeader( const QString& line, QChar delimiter )
|
||||
{
|
||||
QString header = line.toLower();
|
||||
header.removeIf( []( QChar c ) { return c.isSpace(); } );
|
||||
header.removeIf( []( QChar c ) { return c.isSpace() || c == '_'; } );
|
||||
|
||||
return header.split( ';' );
|
||||
return header.split( delimiter );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Returns the index of the first of the candidate column names present in the header, or -1
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
int findColumn( const QStringList& header, std::initializer_list<const char*> candidates )
|
||||
{
|
||||
for ( const char* candidate : candidates )
|
||||
{
|
||||
const int index = header.indexOf( QString( candidate ) );
|
||||
if ( index != -1 ) return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::optional<double> toOptionalDouble( const QStringList& columns, int index )
|
||||
{
|
||||
if ( index == -1 || index >= columns.size() ) return std::nullopt;
|
||||
|
||||
bool ok = false;
|
||||
const double value = columns[index].toDouble( &ok );
|
||||
return ok ? std::optional( value ) : std::nullopt;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -64,22 +98,38 @@ std::expected<RifWellPathFormationReader::WellFormations, QString> RifWellPathFo
|
||||
QTextStream stream( &text );
|
||||
|
||||
QStringList header;
|
||||
QChar delimiter = ';';
|
||||
while ( header.size() < 3 )
|
||||
{
|
||||
if ( stream.atEnd() ) return std::unexpected( parseFailure );
|
||||
|
||||
header = parseHeader( stream.readLine() );
|
||||
const QString rawHeaderLine = stream.readLine();
|
||||
delimiter = detectDelimiter( rawHeaderLine );
|
||||
header = parseHeader( rawHeaderLine, delimiter );
|
||||
}
|
||||
|
||||
const int wellNameIndex = header.indexOf( "wellname" );
|
||||
const int unitNameIndex = header.indexOf( "unitname" );
|
||||
const int mdTopIndex = header.indexOf( "topmd" );
|
||||
const int mdBaseIndex = header.indexOf( "basemd" );
|
||||
const int tvdTopIndex = header.indexOf( "toptvdss" );
|
||||
const int tvdBaseIndex = header.indexOf( "basetvdss" );
|
||||
// The "well pick" formats use wellname/unitname, the FMU formations.csv format uses well/zone (and
|
||||
// ignores zone_code). Both support md and/or tvd, and the FMU format can also include an x/y
|
||||
// position for the zone top.
|
||||
//
|
||||
// "tvdss" columns hold a positive magnitude in the opposite (RMS, Z-up) sign convention and are
|
||||
// negated to match ResInsight's positive-down TVD. Plain "tvd" columns (as used in the FMU
|
||||
// formations.csv) are already positive-down and are used as-is.
|
||||
const int wellNameIndex = findColumn( header, { "wellname", "well" } );
|
||||
const int unitNameIndex = findColumn( header, { "unitname", "zone" } );
|
||||
const int mdTopIndex = header.indexOf( "topmd" );
|
||||
const int mdBaseIndex = header.indexOf( "basemd" );
|
||||
const int tvdSSTopIndex = header.indexOf( "toptvdss" );
|
||||
const int tvdSSBaseIndex = header.indexOf( "basetvdss" );
|
||||
const int tvdTopIndex = tvdSSTopIndex != -1 ? tvdSSTopIndex : header.indexOf( "toptvd" );
|
||||
const int tvdBaseIndex = tvdSSBaseIndex != -1 ? tvdSSBaseIndex : header.indexOf( "basetvd" );
|
||||
const int xIndex = header.indexOf( "xutme" );
|
||||
const int yIndex = header.indexOf( "yutmn" );
|
||||
|
||||
const bool hasMd = mdTopIndex != -1 && mdBaseIndex != -1;
|
||||
const bool hasTvd = tvdTopIndex != -1 && tvdBaseIndex != -1;
|
||||
const bool hasMd = mdTopIndex != -1 && mdBaseIndex != -1;
|
||||
const bool hasTvd = tvdTopIndex != -1 && tvdBaseIndex != -1;
|
||||
const bool isTvdSS = tvdSSTopIndex != -1 && tvdSSBaseIndex != -1;
|
||||
const bool hasXY = xIndex != -1 && yIndex != -1;
|
||||
|
||||
if ( wellNameIndex == -1 || unitNameIndex == -1 )
|
||||
{
|
||||
@@ -95,7 +145,7 @@ std::expected<RifWellPathFormationReader::WellFormations, QString> RifWellPathFo
|
||||
|
||||
while ( !stream.atEnd() )
|
||||
{
|
||||
const QStringList columns = stream.readLine().split( ';' );
|
||||
const QStringList columns = stream.readLine().split( delimiter );
|
||||
if ( columns.size() != header.size() ) continue;
|
||||
|
||||
const QString wellName = columns[wellNameIndex];
|
||||
@@ -113,8 +163,16 @@ std::expected<RifWellPathFormationReader::WellFormations, QString> RifWellPathFo
|
||||
|
||||
if ( hasTvd )
|
||||
{
|
||||
formation.tvdTop = -columns[tvdTopIndex].toDouble();
|
||||
formation.tvdBase = -columns[tvdBaseIndex].toDouble();
|
||||
const double sign = isTvdSS ? -1.0 : 1.0;
|
||||
formation.tvdTop = sign * columns[tvdTopIndex].toDouble();
|
||||
formation.tvdBase = sign * columns[tvdBaseIndex].toDouble();
|
||||
}
|
||||
|
||||
if ( hasXY )
|
||||
{
|
||||
auto x = toOptionalDouble( columns, xIndex );
|
||||
auto y = toOptionalDouble( columns, yIndex );
|
||||
if ( x && y ) formation.topXY = cvf::Vec2d( *x, *y );
|
||||
}
|
||||
|
||||
formationsPerWell[wellName].push_back( formation );
|
||||
|
||||
@@ -2,6 +2,8 @@ set(SOURCE_GROUP_SOURCE_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimFormationNames.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimFormationNamesCollection.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimFormationTools.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWellFormationsFile.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWellFormationsCollection.cpp
|
||||
)
|
||||
|
||||
list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES})
|
||||
@@ -0,0 +1,120 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWellFormationsCollection.h"
|
||||
|
||||
#include "RiaLogging.h"
|
||||
#include "RimWellFormationsFile.h"
|
||||
|
||||
#include "RiuMessageDialog.h"
|
||||
|
||||
#include "cafCmdFeatureMenuBuilder.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWellFormationsCollection, "WellFormationsCollection" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWellFormationsCollection::RimWellFormationsCollection()
|
||||
{
|
||||
CAF_PDM_InitObject( "Well Picks (Formations)", ":/FormationCollection16x16.png" );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_wellFormationsFiles, "WellFormationsFiles", "Well Formations" );
|
||||
|
||||
setDeletable( true );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Returns the existing entry for filePath if present, otherwise imports and returns a new one
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWellFormationsFile* RimWellFormationsCollection::findOrCreate( const QString& filePath )
|
||||
{
|
||||
const QString absoluteFilePath = QFileInfo( filePath ).absoluteFilePath();
|
||||
|
||||
for ( RimWellFormationsFile* file : m_wellFormationsFiles )
|
||||
{
|
||||
if ( QFileInfo( file->filePath() ).absoluteFilePath() == absoluteFilePath )
|
||||
{
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
auto newFile = new RimWellFormationsFile();
|
||||
newFile->setFilePath( filePath );
|
||||
if ( auto result = newFile->reload(); !result )
|
||||
{
|
||||
RiaLogging::error( result.error().toStdString() );
|
||||
}
|
||||
|
||||
m_wellFormationsFiles.push_back( newFile );
|
||||
|
||||
return newFile;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Returns the first file containing zone picks for wellName (matched via RimWellFormationsFile's
|
||||
/// normalized well name comparison), or nullptr if no file has a matching well.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWellFormationsFile* RimWellFormationsCollection::findFileForWell( const QString& wellName ) const
|
||||
{
|
||||
for ( RimWellFormationsFile* file : m_wellFormationsFiles )
|
||||
{
|
||||
if ( file->formationsForWell( wellName ).has_value() ) return file;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<RimWellFormationsFile*> RimWellFormationsCollection::importFiles( const QStringList& filePaths )
|
||||
{
|
||||
std::vector<RimWellFormationsFile*> importedFiles;
|
||||
QString totalErrorMessage;
|
||||
|
||||
for ( const QString& filePath : filePaths )
|
||||
{
|
||||
auto newFile = new RimWellFormationsFile();
|
||||
newFile->setFilePath( filePath );
|
||||
if ( auto result = newFile->reload(); !result )
|
||||
{
|
||||
totalErrorMessage += "\nError in: " + filePath + "\n\t" + result.error();
|
||||
}
|
||||
|
||||
m_wellFormationsFiles.push_back( newFile );
|
||||
importedFiles.push_back( newFile );
|
||||
}
|
||||
|
||||
if ( !totalErrorMessage.isEmpty() )
|
||||
{
|
||||
RiuMessageDialog::showError( nullptr, "Import Well Formations", totalErrorMessage );
|
||||
}
|
||||
|
||||
return importedFiles;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellFormationsCollection::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const
|
||||
{
|
||||
menuBuilder << "RicImportWellFormationsFeature";
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cafPdmChildArrayField.h"
|
||||
#include "cafPdmObject.h"
|
||||
|
||||
namespace caf
|
||||
{
|
||||
class CmdFeatureMenuBuilder;
|
||||
}
|
||||
|
||||
class RimWellFormationsFile;
|
||||
|
||||
//==================================================================================================
|
||||
/// Project-level collection of well formations files (FMU formations.csv, well picks, ...), owned by
|
||||
/// RimOilField. RimEnsembleFileSet and RimWellPath point into this collection, so a given file is
|
||||
/// only parsed once even when it is used by several of them.
|
||||
//==================================================================================================
|
||||
class RimWellFormationsCollection : public caf::PdmObject
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
|
||||
public:
|
||||
RimWellFormationsCollection();
|
||||
|
||||
const caf::PdmChildArrayField<RimWellFormationsFile*>& wellFormationsFiles() const { return m_wellFormationsFiles; }
|
||||
|
||||
RimWellFormationsFile* findOrCreate( const QString& filePath );
|
||||
|
||||
RimWellFormationsFile* findFileForWell( const QString& wellName ) const;
|
||||
|
||||
std::vector<RimWellFormationsFile*> importFiles( const QStringList& filePaths );
|
||||
|
||||
protected:
|
||||
void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override;
|
||||
|
||||
private:
|
||||
caf::PdmChildArrayField<RimWellFormationsFile*> m_wellFormationsFiles;
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWellFormationsFile.h"
|
||||
|
||||
#include "RiaLogging.h"
|
||||
#include "RifCsvHtmlTableTools.h"
|
||||
|
||||
#include "cafPdmUiFilePathEditor.h"
|
||||
#include "cafPdmUiTextEditor.h"
|
||||
#include "cafPdmUiTreeOrdering.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWellFormationsFile, "WellFormationsFile" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWellFormationsFile::RimWellFormationsFile()
|
||||
{
|
||||
CAF_PDM_InitObject( "Well Formations", ":/Formations16x16.png" );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_filePath, "FilePath", "File Path" );
|
||||
m_filePath.uiCapability()->setUiEditorTypeName( caf::PdmUiFilePathEditor::uiEditorTypeName() );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_contentTable, "ContentTable", "Content" );
|
||||
m_contentTable.uiCapability()->setUiEditorTypeName( caf::PdmUiTextEditor::uiEditorTypeName() );
|
||||
m_contentTable.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::LabelPosition::HIDDEN );
|
||||
m_contentTable.uiCapability()->setUiReadOnly( true );
|
||||
m_contentTable.xmlCapability()->disableIO();
|
||||
|
||||
setDeletable( true );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellFormationsFile::setFilePath( const QString& filePath )
|
||||
{
|
||||
m_filePath = filePath;
|
||||
updateUiTreeName();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWellFormationsFile::filePath() const
|
||||
{
|
||||
return m_filePath().path();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWellFormationsFile::shortName() const
|
||||
{
|
||||
return QFileInfo( m_filePath().path() ).fileName();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<void, QString> RimWellFormationsFile::reload()
|
||||
{
|
||||
auto result = RifWellPathFormationReader::readWellFormations( filePath() );
|
||||
if ( !result )
|
||||
{
|
||||
m_wellFormations.clear();
|
||||
updateContentTable();
|
||||
return std::unexpected( result.error() );
|
||||
}
|
||||
|
||||
m_wellFormations = std::move( *result );
|
||||
updateContentTable();
|
||||
return {};
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QStringList RimWellFormationsFile::wellNames() const
|
||||
{
|
||||
QStringList names;
|
||||
for ( const auto& [wellName, formations] : m_wellFormations )
|
||||
{
|
||||
names.push_back( wellName );
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QStringList RimWellFormationsFile::zoneNames( const QString& wellName ) const
|
||||
{
|
||||
QStringList names;
|
||||
|
||||
auto formations = formationsForWell( wellName );
|
||||
if ( !formations ) return names;
|
||||
|
||||
for ( size_t i = 0; i < formations->formationCount(); i++ )
|
||||
{
|
||||
const QString name = formations->formationAt( i ).formationName;
|
||||
if ( !names.contains( name ) ) names.push_back( name );
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::optional<RigWellPathFormations> RimWellFormationsFile::formationsForWell( const QString& wellName ) const
|
||||
{
|
||||
if ( auto it = m_wellFormations.find( wellName ); it != m_wellFormations.end() )
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Fall back to a normalized name match, to handle differences between e.g. RFT and FMU well names
|
||||
const QString normalizedTarget = normalizedWellName( wellName );
|
||||
for ( const auto& [candidateName, formations] : m_wellFormations )
|
||||
{
|
||||
if ( normalizedWellName( candidateName ) == normalizedTarget )
|
||||
{
|
||||
return formations;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Well names are matched case-insensitively, with '-' and '_' treated as equal
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWellFormationsFile::normalizedWellName( const QString& wellName )
|
||||
{
|
||||
QString normalized = wellName.trimmed().toLower();
|
||||
normalized.replace( '_', '-' );
|
||||
return normalized;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellFormationsFile::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue )
|
||||
{
|
||||
if ( changedField == &m_filePath )
|
||||
{
|
||||
updateUiTreeName();
|
||||
if ( auto result = reload(); !result )
|
||||
{
|
||||
RiaLogging::error( result.error().toStdString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellFormationsFile::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName )
|
||||
{
|
||||
updateUiTreeName();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellFormationsFile::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
|
||||
{
|
||||
uiOrdering.add( &m_filePath );
|
||||
uiOrdering.add( &m_contentTable );
|
||||
uiOrdering.skipRemainingFields();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellFormationsFile::defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute )
|
||||
{
|
||||
if ( field == &m_contentTable )
|
||||
{
|
||||
auto myAttr = dynamic_cast<caf::PdmUiTextEditorAttribute*>( attribute );
|
||||
if ( myAttr )
|
||||
{
|
||||
myAttr->wrapMode = caf::PdmUiTextEditorAttribute::NoWrap;
|
||||
myAttr->textMode = caf::PdmUiTextEditorAttribute::HTML;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Parses the file right after the project is loaded, so the data is available without requiring a
|
||||
/// manual reload.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellFormationsFile::initAfterRead()
|
||||
{
|
||||
if ( m_filePath().path().isEmpty() ) return;
|
||||
|
||||
if ( auto result = reload(); !result )
|
||||
{
|
||||
RiaLogging::error( result.error().toStdString() );
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellFormationsFile::updateUiTreeName()
|
||||
{
|
||||
uiCapability()->setUiName( shortName() );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellFormationsFile::updateContentTable()
|
||||
{
|
||||
auto result = RifCsvHtmlTableTools::generateHtmlTableFromFile( filePath() );
|
||||
if ( result )
|
||||
{
|
||||
m_contentTable = *result;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_contentTable = result.error();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RifWellPathFormationReader.h"
|
||||
|
||||
#include "cafPdmField.h"
|
||||
#include "cafPdmObject.h"
|
||||
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
|
||||
//==================================================================================================
|
||||
/// A single well formations/well picks file (e.g. an FMU formations.csv or a well pick export),
|
||||
/// owned by RimWellFormationsCollection. Both RimEnsembleFileSet and RimWellPath can point to an
|
||||
/// entry, so the file is only parsed once.
|
||||
//==================================================================================================
|
||||
class RimWellFormationsFile : public caf::PdmObject
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
|
||||
public:
|
||||
RimWellFormationsFile();
|
||||
|
||||
void setFilePath( const QString& filePath );
|
||||
QString filePath() const;
|
||||
QString shortName() const;
|
||||
|
||||
std::expected<void, QString> reload();
|
||||
|
||||
QStringList wellNames() const;
|
||||
QStringList zoneNames( const QString& wellName ) const;
|
||||
std::optional<RigWellPathFormations> formationsForWell( const QString& wellName ) const;
|
||||
|
||||
static QString normalizedWellName( const QString& wellName );
|
||||
|
||||
protected:
|
||||
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
|
||||
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
|
||||
void defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override;
|
||||
void defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName = "" ) override;
|
||||
void initAfterRead() override;
|
||||
|
||||
private:
|
||||
void updateUiTreeName();
|
||||
void updateContentTable();
|
||||
|
||||
private:
|
||||
caf::PdmField<caf::FilePath> m_filePath;
|
||||
caf::PdmField<QString> m_contentTable;
|
||||
|
||||
RifWellPathFormationReader::WellFormations m_wellFormations;
|
||||
};
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include "ContourMap/RimEclipseContourMapViewCollection.h"
|
||||
#include "Formations/RimFormationNamesCollection.h"
|
||||
#include "Formations/RimWellFormationsCollection.h"
|
||||
#include "RimAnnotationCollection.h"
|
||||
#include "RimCompletionTemplateCollection.h"
|
||||
#include "RimEclipseCaseCollection.h"
|
||||
@@ -59,6 +60,7 @@ RimOilField::RimOilField()
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &summaryCaseMainCollection, "SummaryCaseCollection", "Summary Cases", ":/GridModels.png" );
|
||||
CAF_PDM_InitFieldNoDefault( &formationNamesCollection, "FormationNamesCollection", "Formations" );
|
||||
CAF_PDM_InitFieldNoDefault( &wellFormationsCollection, "WellFormationsCollection", "Well Formations" );
|
||||
CAF_PDM_InitFieldNoDefault( &observedDataCollection, "ObservedDataCollection", "Observed Data", ":/Cases16x16.png" );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &annotationCollection, "AnnotationCollection", "Annotations" );
|
||||
@@ -96,6 +98,7 @@ RimOilField::RimOilField()
|
||||
summaryCaseMainCollection = new RimSummaryCaseMainCollection();
|
||||
observedDataCollection = new RimObservedDataCollection();
|
||||
formationNamesCollection = new RimFormationNamesCollection();
|
||||
wellFormationsCollection = new RimWellFormationsCollection();
|
||||
annotationCollection = new RimAnnotationCollection();
|
||||
ensembleWellLogsCollection = new RimEnsembleWellLogsCollection();
|
||||
polygonCollection = RimPolygonCollection::createTopmost();
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
class RimEclipseCaseCollection;
|
||||
class RimFormationNamesCollection;
|
||||
class RimWellFormationsCollection;
|
||||
class RimFractureTemplateCollection;
|
||||
class RimCompletionTemplateCollection;
|
||||
class RimValveTemplateCollection;
|
||||
@@ -73,6 +74,7 @@ public:
|
||||
caf::PdmChildField<RimSummaryCaseMainCollection*> summaryCaseMainCollection;
|
||||
caf::PdmChildField<RimObservedDataCollection*> observedDataCollection;
|
||||
caf::PdmChildField<RimFormationNamesCollection*> formationNamesCollection;
|
||||
caf::PdmChildField<RimWellFormationsCollection*> wellFormationsCollection;
|
||||
caf::PdmChildField<RimAnnotationCollection*> annotationCollection;
|
||||
caf::PdmChildField<RimMeasurement*> measurement;
|
||||
caf::PdmChildField<RimSurfaceCollection*> surfaceCollection;
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include "ContourMap/RimEclipseContourMapViewCollection.h"
|
||||
#include "EnsembleFileSet/RimEnsembleFileSetCollection.h"
|
||||
#include "Formations/RimFormationNamesCollection.h"
|
||||
#include "Formations/RimWellFormationsCollection.h"
|
||||
#include "Jobs/RimJobCollection.h"
|
||||
#include "PlotTemplates/RimPlotTemplateFolderItem.h"
|
||||
#include "Polygons/RimPolygonCollection.h"
|
||||
@@ -1489,6 +1490,10 @@ void RimProject::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, Q
|
||||
{
|
||||
uiTreeOrdering.add( oilField->vfpDataCollection() );
|
||||
}
|
||||
if ( oilField->wellFormationsCollection() )
|
||||
{
|
||||
uiTreeOrdering.add( oilField->wellFormationsCollection() );
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ( uiConfigName == "PlotWindow.Scripts" || uiConfigName == "MainWindow.Scripts" )
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
#include "RigWellPathFormations.h"
|
||||
|
||||
#include "RiaWellLogTrackDefines.h"
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -275,6 +277,34 @@ std::pair<std::vector<QString>, std::vector<double>>
|
||||
return result;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Returns one (name, top, base) range per formation whose level does not exceed the given level,
|
||||
/// for use as shaded zone regions. Unlike depthAndFormationNamesUpToLevel(), this does not merge
|
||||
/// hierarchical levels at coincident depths, so a parent/child formation pair can overlap in the
|
||||
/// result when both satisfy the level filter (e.g. requesting LEVEL1 also includes any GROUP-level
|
||||
/// parent zone covering the same depth range).
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<std::tuple<QString, double, double>> RigWellPathFormations::depthRangesUpToLevel( FormationLevel level,
|
||||
RiaDefines::DepthType depthType ) const
|
||||
{
|
||||
std::vector<std::tuple<QString, double, double>> result;
|
||||
if ( level == FormationLevel::NONE ) return result;
|
||||
|
||||
for ( const auto& [formation, formationLevel] : m_formations )
|
||||
{
|
||||
if ( level != FormationLevel::ALL && formationLevel > level ) continue;
|
||||
|
||||
auto top = pickDepth( formation, PickPosition::TOP, depthType );
|
||||
auto base = pickDepth( formation, PickPosition::BASE, depthType );
|
||||
if ( !top || !base ) continue;
|
||||
|
||||
result.emplace_back( formation.formationName, *top, *base );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -306,3 +336,19 @@ size_t RigWellPathFormations::formationNamesCount() const
|
||||
{
|
||||
return m_formations.size() + m_fluids.size();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Returns the non-fluid formation at index, in the order the input data was given
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
const RigWellPathFormation& RigWellPathFormations::formationAt( size_t index ) const
|
||||
{
|
||||
return m_formations[index].first;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
size_t RigWellPathFormations::formationCount() const
|
||||
{
|
||||
return m_formations.size();
|
||||
}
|
||||
@@ -19,21 +19,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "RiaDefines.h"
|
||||
#include "RiaWellLogTrackDefines.h"
|
||||
|
||||
#include "cvfVector2.h"
|
||||
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace RiaDefines
|
||||
{
|
||||
enum class WellLogTrackFormationLevel;
|
||||
}
|
||||
|
||||
struct RigWellPathFormation
|
||||
{
|
||||
double mdTop{ 0.0 };
|
||||
double mdBase{ 0.0 };
|
||||
double tvdTop{ 0.0 };
|
||||
double tvdBase{ 0.0 };
|
||||
QString formationName;
|
||||
double mdTop{ 0.0 };
|
||||
double mdBase{ 0.0 };
|
||||
double tvdTop{ 0.0 };
|
||||
double tvdBase{ 0.0 };
|
||||
QString formationName;
|
||||
std::optional<cvf::Vec2d> topXY;
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -51,12 +60,17 @@ public:
|
||||
std::pair<std::vector<QString>, std::vector<double>>
|
||||
depthAndFormationNamesUpToLevel( FormationLevel level, bool includeFluids, RiaDefines::DepthType depthType ) const;
|
||||
|
||||
// Returns formation name and (top, base) depth range per zone, for shaded region plotting.
|
||||
std::vector<std::tuple<QString, double, double>> depthRangesUpToLevel( FormationLevel level, RiaDefines::DepthType depthType ) const;
|
||||
|
||||
std::vector<FormationLevel> formationsLevelsPresent() const;
|
||||
|
||||
QString filePath() const;
|
||||
QString keyInFile() const;
|
||||
|
||||
size_t formationNamesCount() const;
|
||||
size_t formationNamesCount() const;
|
||||
size_t formationCount() const;
|
||||
const RigWellPathFormation& formationAt( size_t index ) const;
|
||||
|
||||
private:
|
||||
QString m_filePath;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "RifWellPathFormationReader.h"
|
||||
|
||||
#include "RiaWellLogTrackDefines.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <vector>
|
||||
@@ -105,7 +107,7 @@ TEST( RifWellPathFormationReader, ErrorWhenNoDepthColumns )
|
||||
|
||||
TEST( RifWellPathFormationReader, ErrorWhenRequiredColumnsMissing )
|
||||
{
|
||||
const QString content = "wellname;zone;topmd;basemd\n"
|
||||
const QString content = "wellname;formation;topmd;basemd\n"
|
||||
"A-1;Zone;100.0;110.0\n";
|
||||
|
||||
EXPECT_FALSE( RifWellPathFormationReader::parseWellFormations( content, "picks.txt" ).has_value() );
|
||||
@@ -121,3 +123,57 @@ TEST( RifWellPathFormationReader, ErrorWhenFileMissing )
|
||||
{
|
||||
EXPECT_FALSE( RifWellPathFormationReader::readWellFormations( "this/file/does/not/exist.txt" ).has_value() );
|
||||
}
|
||||
|
||||
TEST( RifWellPathFormationReader, ParseFmuFormationsCsv )
|
||||
{
|
||||
const QString content = "X_UTME,Y_UTMN,TOP_TVD,TOP_MD,ZONE_CODE,WELL,BASE_TVD,BASE_MD,ZONE\n"
|
||||
"460994.9,5933813.29,1644.0,1693.0,1,R_A2,1662.0,1711.0,Valysar\n"
|
||||
"460994.9,5933813.29,1662.0,1711.0,2,R_A2,1678.0,1727.0,Therys\n";
|
||||
|
||||
auto result = RifWellPathFormationReader::parseWellFormations( content, "formations.csv" );
|
||||
ASSERT_TRUE( result.has_value() );
|
||||
ASSERT_EQ( 1u, result->size() );
|
||||
|
||||
const auto& well = result->at( "R_A2" );
|
||||
EXPECT_EQ( 2u, well.formationNamesCount() );
|
||||
|
||||
auto [mdNames, mdDepths] = allFormations( well, RiaDefines::DepthType::MEASURED_DEPTH );
|
||||
// The base of Valysar coincides with the top of Therys, so it is deduplicated like adjacent "unitname" zones
|
||||
EXPECT_EQ( ( std::vector<QString>{ "Valysar Top", "Therys Top", "Therys Base" } ), mdNames );
|
||||
EXPECT_EQ( ( std::vector<double>{ 1693.0, 1711.0, 1727.0 } ), mdDepths );
|
||||
|
||||
// TOP_TVD/BASE_TVD (without the "SS" suffix) are already positive-down, and are used as-is
|
||||
auto [tvdNames, tvdDepths] = allFormations( well, RiaDefines::DepthType::TRUE_VERTICAL_DEPTH );
|
||||
EXPECT_EQ( ( std::vector<double>{ 1644.0, 1662.0, 1678.0 } ), tvdDepths );
|
||||
}
|
||||
|
||||
TEST( RifWellPathFormationReader, ParseFmuFormationsCsvIncludesTopXY )
|
||||
{
|
||||
const QString content = "X_UTME,Y_UTMN,TOP_TVD,TOP_MD,ZONE_CODE,WELL,BASE_TVD,BASE_MD,ZONE\n"
|
||||
"460994.9,5933813.29,1644.0,1693.0,1,R_A2,1662.0,1711.0,Valysar\n"
|
||||
"461200.1,5933900.5,1662.0,1711.0,2,R_A2,1678.0,1727.0,Therys\n";
|
||||
|
||||
auto result = RifWellPathFormationReader::parseWellFormations( content, "formations.csv" );
|
||||
ASSERT_TRUE( result.has_value() );
|
||||
|
||||
const auto& well = result->at( "R_A2" );
|
||||
ASSERT_TRUE( well.formationAt( 0 ).topXY.has_value() );
|
||||
EXPECT_DOUBLE_EQ( 460994.9, well.formationAt( 0 ).topXY->x() );
|
||||
EXPECT_DOUBLE_EQ( 5933813.29, well.formationAt( 0 ).topXY->y() );
|
||||
|
||||
ASSERT_TRUE( well.formationAt( 1 ).topXY.has_value() );
|
||||
EXPECT_DOUBLE_EQ( 461200.1, well.formationAt( 1 ).topXY->x() );
|
||||
EXPECT_DOUBLE_EQ( 5933900.5, well.formationAt( 1 ).topXY->y() );
|
||||
}
|
||||
|
||||
TEST( RifWellPathFormationReader, ParseFmuFormationsCsvWithoutXY )
|
||||
{
|
||||
const QString content = "TOP_MD,WELL,BASE_MD,ZONE\n"
|
||||
"1693.0,R_A2,1711.0,Valysar\n";
|
||||
|
||||
auto result = RifWellPathFormationReader::parseWellFormations( content, "formations.csv" );
|
||||
ASSERT_TRUE( result.has_value() );
|
||||
|
||||
const auto& well = result->at( "R_A2" );
|
||||
EXPECT_FALSE( well.formationAt( 0 ).topXY.has_value() );
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "Well/RigWellPathFormations.h"
|
||||
|
||||
#include "RiaWellLogTrackDefines.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <vector>
|
||||
@@ -131,3 +133,44 @@ TEST( RigWellPathFormations, UnsupportedDepthTypeGivesNoPicks )
|
||||
EXPECT_TRUE( depths.empty() );
|
||||
}
|
||||
}
|
||||
|
||||
TEST( RigWellPathFormations, DepthRangesUpToLevelReturnsZoneIntervals )
|
||||
{
|
||||
auto formations = createHierarchy();
|
||||
|
||||
{
|
||||
auto ranges = formations.depthRangesUpToLevel( FormationLevel::GROUP, DepthType::MEASURED_DEPTH );
|
||||
ASSERT_EQ( 1u, ranges.size() );
|
||||
EXPECT_EQ( QString( "GARN" ), std::get<0>( ranges[0] ) );
|
||||
EXPECT_DOUBLE_EQ( 100.0, std::get<1>( ranges[0] ) );
|
||||
EXPECT_DOUBLE_EQ( 200.0, std::get<2>( ranges[0] ) );
|
||||
}
|
||||
{
|
||||
auto ranges = formations.depthRangesUpToLevel( FormationLevel::LEVEL1, DepthType::MEASURED_DEPTH );
|
||||
ASSERT_EQ( 3u, ranges.size() );
|
||||
EXPECT_EQ( QString( "GARN" ), std::get<0>( ranges[0] ) );
|
||||
EXPECT_EQ( QString( "Garn 1" ), std::get<0>( ranges[1] ) );
|
||||
EXPECT_EQ( QString( "Garn 2" ), std::get<0>( ranges[2] ) );
|
||||
}
|
||||
{
|
||||
auto ranges = formations.depthRangesUpToLevel( FormationLevel::NONE, DepthType::MEASURED_DEPTH );
|
||||
EXPECT_TRUE( ranges.empty() );
|
||||
}
|
||||
}
|
||||
|
||||
TEST( RigWellPathFormations, DepthRangesUpToLevelExcludesFluids )
|
||||
{
|
||||
RigWellPathFormations formations( { formation( "Garn", 100.0, 200.0 ), formation( "OIL", 120.0, 140.0 ) }, "file", "well" );
|
||||
|
||||
auto ranges = formations.depthRangesUpToLevel( FormationLevel::ALL, DepthType::MEASURED_DEPTH );
|
||||
ASSERT_EQ( 1u, ranges.size() );
|
||||
EXPECT_EQ( QString( "Garn" ), std::get<0>( ranges[0] ) );
|
||||
}
|
||||
|
||||
TEST( RigWellPathFormations, DepthRangesUpToLevelUnsupportedDepthTypeGivesNoRanges )
|
||||
{
|
||||
RigWellPathFormations formations( { formation( "Garn", 100.0, 200.0 ) }, "file", "well" );
|
||||
|
||||
auto ranges = formations.depthRangesUpToLevel( FormationLevel::ALL, DepthType::TRUE_VERTICAL_DEPTH_RKB );
|
||||
EXPECT_TRUE( ranges.empty() );
|
||||
}
|
||||
Reference in new issue
Block a user