#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:
Magne Sjaastad committed 2026-10-08 12:44:14 +02:00
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() );
}