diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/WellLog/CMakeLists_files.cmake index c356ad763d..b4cae01418 100644 --- a/ApplicationLibCode/ProjectDataModel/WellLog/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModel/WellLog/CMakeLists_files.cmake @@ -34,6 +34,8 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RimWellLogFileDataLoader.h ${CMAKE_CURRENT_LIST_DIR}/RimOsduWellLogDataLoader.h ${CMAKE_CURRENT_LIST_DIR}/RimImportedWellLog.h + ${CMAKE_CURRENT_LIST_DIR}/RimImportedWellLogData.h + ${CMAKE_CURRENT_LIST_DIR}/RimWellLogChannelData.h ) set(SOURCE_GROUP_SOURCE_FILES @@ -74,6 +76,8 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimWellLogFileDataLoader.cpp ${CMAKE_CURRENT_LIST_DIR}/RimOsduWellLogDataLoader.cpp ${CMAKE_CURRENT_LIST_DIR}/RimImportedWellLog.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimImportedWellLogData.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimWellLogChannelData.cpp ) list(APPEND CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES}) diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLog.cpp b/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLog.cpp index 257ed7d9ce..b7ff19e72e 100644 --- a/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLog.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLog.cpp @@ -18,10 +18,12 @@ #include "RimImportedWellLog.h" -#include "Well/RigImportedWellLogData.h" - +#include "RimImportedWellLogData.h" +#include "RimWellLogChannel.h" #include "RimWellPath.h" +#include "Well/RigImportedWellLogData.h" + #include "RiaFieldHandleTools.h" #include "cafPdmFieldScriptingCapability.h" @@ -41,6 +43,9 @@ RimImportedWellLog::RimImportedWellLog() CAF_PDM_InitScriptableField( &m_name, "Name", QString(), "Name" ); m_name.uiCapability()->setUiReadOnly( true ); + CAF_PDM_InitFieldNoDefault( &m_wellLogData, "WellLogData", "Well Log Data" ); + m_wellLogData.uiCapability()->setUiTreeChildrenHidden( true ); + setDeletable( true ); } @@ -65,8 +70,8 @@ QString RimImportedWellLog::name() const //-------------------------------------------------------------------------------------------------- QString RimImportedWellLog::wellName() const { - RimWellPath* wellPath = firstAncestorOrThisOfType(); - if ( wellPath ) + if ( RimWellPath* wellPath = firstAncestorOrThisOfType() ) + { return wellPath->name(); } @@ -78,15 +83,46 @@ QString RimImportedWellLog::wellName() const //-------------------------------------------------------------------------------------------------- RigWellLogData* RimImportedWellLog::wellLogData() { - return m_wellLogData.p(); + return m_cachedRigData.p(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -void RimImportedWellLog::setWellLogData( RigImportedWellLogData* wellLogData ) +void RimImportedWellLog::setWellLogData( RimImportedWellLogData* wellLogData ) { m_wellLogData = wellLogData; + // Clear cached Rig data when setting new PDM data + m_cachedRigData = nullptr; + + if ( wellLogData ) + { + // Create temporary Rig data to update channels + m_cachedRigData = wellLogData->createRigData(); + updateChannelsFromWellLogData( m_cachedRigData.p() ); + } + else + { + m_wellLogChannels.deleteChildren(); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimImportedWellLog::updateChannelsFromWellLogData( RigImportedWellLogData* wellLogData ) +{ + m_wellLogChannels.deleteChildren(); + + if ( !wellLogData ) return; + + QStringList wellLogNames = wellLogData->wellLogChannelNames(); + for ( int logIdx = 0; logIdx < wellLogNames.size(); logIdx++ ) + { + RimWellLogChannel* wellLog = new RimWellLogChannel(); + wellLog->setName( wellLogNames[logIdx] ); + m_wellLogChannels.push_back( wellLog ); + } } //-------------------------------------------------------------------------------------------------- @@ -97,10 +133,11 @@ std::vector> { if ( unitString ) *unitString = QString(); - if ( !m_wellLogData.notNull() ) return {}; + RigWellLogData* wld = wellLogData(); + if ( !wld ) return {}; - std::vector depths = m_wellLogData->depthValues(); - std::vector values = m_wellLogData->values( channelName ); + std::vector depths = wld->depthValues(); + std::vector values = wld->values( channelName ); std::vector> depthValuePairs; if ( depths.size() == values.size() ) @@ -114,10 +151,23 @@ std::vector> return depthValuePairs; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimImportedWellLog::initAfterRead() +{ + // Create cached Rig data from PDM data when loading from project file + if ( m_wellLogData() && !m_cachedRigData.notNull() ) + { + m_cachedRigData = m_wellLogData()->createRigData(); + updateChannelsFromWellLogData( m_cachedRigData.p() ); + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- caf::PdmFieldHandle* RimImportedWellLog::userDescriptionField() { return &m_name; -} \ No newline at end of file +} diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLog.h b/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLog.h index ba516ed333..882e1d1f35 100644 --- a/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLog.h +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLog.h @@ -20,12 +20,14 @@ #include "RimWellLog.h" +#include "cafPdmChildField.h" #include "cafPdmField.h" #include "cvfObject.h" #include class RigImportedWellLogData; +class RimImportedWellLogData; class RimWellPath; //================================================================================================== @@ -44,15 +46,20 @@ public: QString wellName() const override; RigWellLogData* wellLogData() override; - void setWellLogData( RigImportedWellLogData* wellLogData ); + void setWellLogData( RimImportedWellLogData* wellLogData ); std::vector> findMdAndChannelValuesForWellPath( const RimWellPath& wellPath, const QString& channelName, QString* unitString = nullptr ) override; private: + void initAfterRead() override; caf::PdmFieldHandle* userDescriptionField() override; + void updateChannelsFromWellLogData( RigImportedWellLogData* wellLogData ); private: - cvf::ref m_wellLogData; - caf::PdmField m_name; -}; \ No newline at end of file + // PDM-based data storage for project file persistence + caf::PdmChildField m_wellLogData; + caf::PdmField m_name; + + cvf::ref m_cachedRigData; +}; diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLogData.cpp b/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLogData.cpp new file mode 100644 index 0000000000..f0b7a3d3ac --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLogData.cpp @@ -0,0 +1,181 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimImportedWellLogData.h" + +#include "RimWellLogChannelData.h" + +#include "Well/RigImportedWellLogData.h" + +CAF_PDM_SOURCE_INIT( RimImportedWellLogData, "ImportedWellLogData" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimImportedWellLogData::RimImportedWellLogData() +{ + CAF_PDM_InitObject( "Imported Well Log Data", "", "", "Imported Well Log Data" ); + + CAF_PDM_InitField( &m_depthValues, "DepthValues", {}, "Depth Values" ); + CAF_PDM_InitField( &m_tvdMslValues, "TvdMslValues", {}, "TVD MSL Values" ); + CAF_PDM_InitField( &m_tvdRkbValues, "TvdRkbValues", {}, "TVD RKB Values" ); + CAF_PDM_InitFieldNoDefault( &m_channelDataObjects, "ChannelDataObjects", "Channel Data Objects" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimImportedWellLogData::setDepthValues( const std::vector& depths ) +{ + m_depthValues = depths; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimImportedWellLogData::setTvdMslValues( const std::vector& tvdMsl ) +{ + m_tvdMslValues = tvdMsl; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimImportedWellLogData::setTvdRkbValues( const std::vector& tvdRkb ) +{ + m_tvdRkbValues = tvdRkb; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimImportedWellLogData::setChannelData( const QString& name, const std::vector& values ) +{ + // Check if channel already exists + for ( RimWellLogChannelData* channelData : m_channelDataObjects ) + { + if ( channelData->name() == name ) + { + channelData->setValues( values ); + return; + } + } + + // Create new channel data object + RimWellLogChannelData* channelData = new RimWellLogChannelData(); + channelData->setName( name ); + channelData->setValues( values ); + m_channelDataObjects.push_back( channelData ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimImportedWellLogData::depthValues() const +{ + return m_depthValues(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimImportedWellLogData::tvdMslValues() const +{ + return m_tvdMslValues(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimImportedWellLogData::tvdRkbValues() const +{ + return m_tvdRkbValues(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimImportedWellLogData::channelNames() const +{ + std::vector names; + for ( const RimWellLogChannelData* channelData : m_channelDataObjects ) + { + names.push_back( channelData->name() ); + } + return names; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimImportedWellLogData::channelValues( const QString& name ) const +{ + for ( const RimWellLogChannelData* channelData : m_channelDataObjects ) + { + if ( channelData->name() == name ) + { + return channelData->values(); + } + } + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimImportedWellLogData::hasTvdMslValues() const +{ + return !m_tvdMslValues().empty(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimImportedWellLogData::hasTvdRkbValues() const +{ + return !m_tvdRkbValues().empty(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +cvf::ref RimImportedWellLogData::createRigData() const +{ + cvf::ref rigData = new RigImportedWellLogData(); + + // Set depth values + rigData->setDepthValues( m_depthValues() ); + + // Set TVD values if present + if ( hasTvdMslValues() ) + { + rigData->setTvdMslValues( m_tvdMslValues() ); + } + if ( hasTvdRkbValues() ) + { + rigData->setTvdRkbValues( m_tvdRkbValues() ); + } + + // Set channel data + for ( const RimWellLogChannelData* channelData : m_channelDataObjects ) + { + rigData->setChannelData( channelData->name(), channelData->values() ); + } + + return rigData; +} diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLogData.h b/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLogData.h new file mode 100644 index 0000000000..efcf2ca03c --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimImportedWellLogData.h @@ -0,0 +1,66 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "cafPdmChildArrayField.h" +#include "cafPdmField.h" +#include "cafPdmObject.h" +#include "cvfObject.h" + +#include +#include + +class RigImportedWellLogData; +class RimWellLogChannelData; + +//================================================================================================== +/// PDM class for storing imported well log data that can be persisted to project files +//================================================================================================== +class RimImportedWellLogData : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + RimImportedWellLogData(); + + // Setters for data + void setDepthValues( const std::vector& depths ); + void setTvdMslValues( const std::vector& tvdMsl ); + void setTvdRkbValues( const std::vector& tvdRkb ); + void setChannelData( const QString& name, const std::vector& values ); + + // Getters for data + std::vector depthValues() const; + std::vector tvdMslValues() const; + std::vector tvdRkbValues() const; + std::vector channelNames() const; + std::vector channelValues( const QString& name ) const; + + bool hasTvdMslValues() const; + bool hasTvdRkbValues() const; + + // Method to produce RigImportedWellLogData from PDM data + cvf::ref createRigData() const; + +private: + caf::PdmField> m_depthValues; + caf::PdmField> m_tvdMslValues; + caf::PdmField> m_tvdRkbValues; + caf::PdmChildArrayField m_channelDataObjects; +}; \ No newline at end of file diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogChannelData.cpp b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogChannelData.cpp new file mode 100644 index 0000000000..29cef4c886 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogChannelData.cpp @@ -0,0 +1,64 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimWellLogChannelData.h" + +CAF_PDM_SOURCE_INIT( RimWellLogChannelData, "WellLogChannelData" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimWellLogChannelData::RimWellLogChannelData() +{ + CAF_PDM_InitObject( "Well Log Channel Data", "", "", "Well Log Channel Data" ); + + CAF_PDM_InitField( &m_name, "Name", QString(), "Name" ); + CAF_PDM_InitField( &m_values, "Values", {}, "Values" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellLogChannelData::setName( const QString& name ) +{ + m_name = name; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimWellLogChannelData::name() const +{ + return m_name(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellLogChannelData::setValues( const std::vector& values ) +{ + m_values = values; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimWellLogChannelData::values() const +{ + return m_values(); +} \ No newline at end of file diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogChannelData.h b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogChannelData.h new file mode 100644 index 0000000000..72651c55ec --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogChannelData.h @@ -0,0 +1,46 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "cafPdmField.h" +#include "cafPdmObject.h" + +#include +#include + +//================================================================================================== +/// PDM class for storing individual channel data (name and values) for well logs +//================================================================================================== +class RimWellLogChannelData : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + RimWellLogChannelData(); + + void setName( const QString& name ); + QString name() const; + + void setValues( const std::vector& values ); + std::vector values() const; + +private: + caf::PdmField m_name; + caf::PdmField> m_values; +}; \ No newline at end of file diff --git a/ApplicationLibCode/ProjectDataModel/WellPath/RimWellPath.cpp b/ApplicationLibCode/ProjectDataModel/WellPath/RimWellPath.cpp index 41bc43efc0..1a453f1734 100644 --- a/ApplicationLibCode/ProjectDataModel/WellPath/RimWellPath.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellPath/RimWellPath.cpp @@ -37,6 +37,7 @@ #include "Rim3dWellLogCurveCollection.h" #include "RimFishbones.h" #include "RimFishbonesCollection.h" +#include "RimImportedWellLog.h" #include "RimMainPlotCollection.h" #include "RimMswCompletionParameters.h" #include "RimOsduWellLog.h" @@ -933,6 +934,10 @@ void RimWellPath::addWellLog( RimWellLog* wellLog ) } } } + else if ( RimImportedWellLog* importedWellLog = dynamic_cast( wellLog ) ) + { + m_wellLogs.push_back( importedWellLog ); + } else if ( RimOsduWellLog* osduWellLog = dynamic_cast( wellLog ) ) { m_wellLogs.push_back( osduWellLog ); diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellPath.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcWellPath.cpp index f659e13de1..2b970e29e6 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellPath.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellPath.cpp @@ -26,6 +26,7 @@ #include "RiaLogging.h" #include "RimImportedWellLog.h" +#include "RimImportedWellLogData.h" #include "Well/RigImportedWellLogData.h" #include "RimEclipseCase.h" @@ -406,7 +407,10 @@ RimcWellPath_addWellLogInternal::RimcWellPath_addWellLogInternal( caf::PdmObject CAF_PDM_InitObject( "Add Well Log", "", "", "Add Well Log" ); CAF_PDM_InitScriptableFieldNoDefault( &m_name, "Name", "Name" ); - CAF_PDM_InitScriptableFieldNoDefault( &m_valuesKey, "ValuesKey", "Values Key" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_measuredDepthKey, "MeasuredDepthKey", "Measured Depth Key" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_channelKeysCsv, "ChannelKeysCsv", "Channel Keys CSV" ); + CAF_PDM_InitScriptableField( &m_tvdMslKey, "TvdMslKey", QString(), "TVD MSL Key" ); + CAF_PDM_InitScriptableField( &m_tvdRkbKey, "TvdRkbKey", QString(), "TVD RKB Key" ); } //-------------------------------------------------------------------------------------------------- @@ -421,52 +425,184 @@ std::expected RimcWellPath_addWellLogInternal::e return std::unexpected( "Well log name cannot be empty" ); } - if ( m_valuesKey().isEmpty() ) + if ( m_measuredDepthKey().isEmpty() ) { - return std::unexpected( "Values key cannot be empty" ); + return std::unexpected( "Measured depth key cannot be empty" ); + } + + if ( m_channelKeysCsv().isEmpty() ) + { + return std::unexpected( "Channel keys CSV cannot be empty" ); } - // Retrieve values from key-value store auto keyValueStore = RiaApplication::instance()->keyValueStore(); - auto valuesData = keyValueStore->get( m_valuesKey().toStdString() ); - if ( !valuesData.has_value() ) + // Retrieve measured depth values from key-value store + auto measuredDepthData = keyValueStore->get( m_measuredDepthKey().toStdString() ); + if ( !measuredDepthData.has_value() ) { - return std::unexpected( "Failed to retrieve values from key-value store" ); + return std::unexpected( "Failed to retrieve measured depth from key-value store" ); } - // Convert from char vector to double vector - std::vector values = RiaKeyValueStoreUtil::convertToFloatVector( valuesData ); - if ( values.empty() ) + std::vector measuredDepthFloat = RiaKeyValueStoreUtil::convertToFloatVector( measuredDepthData ); + if ( measuredDepthFloat.empty() ) { - return std::unexpected( "Values array cannot be empty" ); + return std::unexpected( "Measured depth array cannot be empty" ); } - // Create well log data - auto wellLogData = new RigImportedWellLogData(); - - // Generate measured depth values from 0 to size-1 (assuming unit spacing) - std::vector depthValues; - std::vector doubleValues; - for ( size_t i = 0; i < values.size(); ++i ) + // Convert to double + std::vector measuredDepth; + for ( float value : measuredDepthFloat ) { - depthValues.push_back( static_cast( i ) ); - doubleValues.push_back( values[i] ); + measuredDepth.push_back( static_cast( value ) ); } - wellLogData->setDepthValues( depthValues ); - wellLogData->setChannelData( m_name(), doubleValues ); + // Handle optional TVD MSL + std::vector tvdMslValues; + if ( !m_tvdMslKey().isEmpty() ) + { + auto tvdMslData = keyValueStore->get( m_tvdMslKey().toStdString() ); + if ( !tvdMslData.has_value() ) + { + return std::unexpected( "Failed to retrieve TVD MSL from key-value store" ); + } + + std::vector tvdMslFloat = RiaKeyValueStoreUtil::convertToFloatVector( tvdMslData ); + if ( tvdMslFloat.size() != measuredDepth.size() ) + { + return std::unexpected( + QString( "TVD MSL has %1 values but measured depth has %2 values" ).arg( tvdMslFloat.size() ).arg( measuredDepth.size() ) ); + } + + for ( float value : tvdMslFloat ) + { + tvdMslValues.push_back( static_cast( value ) ); + } + } + + // Handle optional TVD RKB + std::vector tvdRkbValues; + if ( !m_tvdRkbKey().isEmpty() ) + { + auto tvdRkbData = keyValueStore->get( m_tvdRkbKey().toStdString() ); + if ( !tvdRkbData.has_value() ) + { + return std::unexpected( "Failed to retrieve TVD RKB from key-value store" ); + } + + std::vector tvdRkbFloat = RiaKeyValueStoreUtil::convertToFloatVector( tvdRkbData ); + if ( tvdRkbFloat.size() != measuredDepth.size() ) + { + return std::unexpected( + QString( "TVD RKB has %1 values but measured depth has %2 values" ).arg( tvdRkbFloat.size() ).arg( measuredDepth.size() ) ); + } + + for ( float value : tvdRkbFloat ) + { + tvdRkbValues.push_back( static_cast( value ) ); + } + } + + // Parse channel keys CSV (format: "channel1:key1,channel2:key2,...") + QString channelKeysStr = m_channelKeysCsv(); + if ( channelKeysStr.isEmpty() ) + { + return std::unexpected( "Channel keys cannot be empty" ); + } + + QStringList channelMappings = channelKeysStr.split( ",", Qt::SkipEmptyParts ); + if ( channelMappings.isEmpty() ) + { + return std::unexpected( "Channel data cannot be empty" ); + } + + QMap channelKeysMap; + for ( const QString& mapping : channelMappings ) + { + QStringList parts = mapping.split( ":", Qt::SkipEmptyParts ); + if ( parts.size() != 2 ) + { + return std::unexpected( QString( "Invalid channel mapping format: %1" ).arg( mapping ) ); + } + QString channelName = parts[0]; + QString channelKey = parts[1]; + + if ( channelName.isEmpty() || channelKey.isEmpty() ) + { + return std::unexpected( QString( "Empty channel name or key in mapping: %1" ).arg( mapping ) ); + } + + channelKeysMap[channelName] = channelKey; + } + + // Create PDM well log data for project file persistence + auto pdmWellLogData = new RimImportedWellLogData(); + pdmWellLogData->setDepthValues( measuredDepth ); + + // Set optional TVD data + if ( !tvdMslValues.empty() ) + { + pdmWellLogData->setTvdMslValues( tvdMslValues ); + } + if ( !tvdRkbValues.empty() ) + { + pdmWellLogData->setTvdRkbValues( tvdRkbValues ); + } + + // Retrieve and set channel data + QStringList keysToCleanup; + keysToCleanup.append( m_measuredDepthKey() ); + if ( !m_tvdMslKey().isEmpty() ) keysToCleanup.append( m_tvdMslKey() ); + if ( !m_tvdRkbKey().isEmpty() ) keysToCleanup.append( m_tvdRkbKey() ); + + for ( auto it = channelKeysMap.begin(); it != channelKeysMap.end(); ++it ) + { + QString channelName = it.key(); + QString channelKey = it.value(); + + keysToCleanup.append( channelKey ); + + // Retrieve channel values from key-value store + auto channelData = keyValueStore->get( channelKey.toStdString() ); + if ( !channelData.has_value() ) + { + return std::unexpected( QString( "Failed to retrieve channel '%1' from key-value store" ).arg( channelName ) ); + } + + std::vector channelValuesFloat = RiaKeyValueStoreUtil::convertToFloatVector( channelData ); + if ( channelValuesFloat.size() != measuredDepth.size() ) + { + return std::unexpected( QString( "Channel '%1' has %2 values but measured depth has %3 values" ) + .arg( channelName ) + .arg( channelValuesFloat.size() ) + .arg( measuredDepth.size() ) ); + } + + // Convert to double + std::vector channelValues; + for ( float value : channelValuesFloat ) + { + channelValues.push_back( static_cast( value ) ); + } + + pdmWellLogData->setChannelData( channelName, channelValues ); + } // Create well log object auto importedWellLog = new RimImportedWellLog(); importedWellLog->setName( m_name() ); - importedWellLog->setWellLogData( wellLogData ); + importedWellLog->setWellLogData( pdmWellLogData ); // Add to well path wellPath->addWellLog( importedWellLog ); // Clean up key-value store - keyValueStore->remove( m_valuesKey().toStdString() ); + for ( const QString& keyToCleanup : keysToCleanup ) + { + keyValueStore->remove( keyToCleanup.toStdString() ); + } + + wellPath->updateConnectedEditors(); return importedWellLog; } diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellPath.h b/ApplicationLibCode/ProjectDataModelCommands/RimcWellPath.h index 5c6b95afe5..10b54bae54 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellPath.h +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellPath.h @@ -160,5 +160,8 @@ public: private: caf::PdmField m_name; - caf::PdmField m_valuesKey; + caf::PdmField m_measuredDepthKey; + caf::PdmField m_channelKeysCsv; + caf::PdmField m_tvdMslKey; + caf::PdmField m_tvdRkbKey; }; diff --git a/ApplicationLibCode/ReservoirDataModel/Well/RigImportedWellLogData.cpp b/ApplicationLibCode/ReservoirDataModel/Well/RigImportedWellLogData.cpp index 69c11b515d..2ed4c0662e 100644 --- a/ApplicationLibCode/ReservoirDataModel/Well/RigImportedWellLogData.cpp +++ b/ApplicationLibCode/ReservoirDataModel/Well/RigImportedWellLogData.cpp @@ -65,8 +65,7 @@ std::vector RigImportedWellLogData::depthValues() const //-------------------------------------------------------------------------------------------------- std::vector RigImportedWellLogData::tvdMslValues() const { - // Not supported for imported data - return std::vector(); + return m_tvdMslValues; } //-------------------------------------------------------------------------------------------------- @@ -74,8 +73,7 @@ std::vector RigImportedWellLogData::tvdMslValues() const //-------------------------------------------------------------------------------------------------- std::vector RigImportedWellLogData::tvdRkbValues() const { - // Not supported for imported data - return std::vector(); + return m_tvdRkbValues; } //-------------------------------------------------------------------------------------------------- @@ -114,7 +112,7 @@ QString RigImportedWellLogData::depthUnitString() const //-------------------------------------------------------------------------------------------------- bool RigImportedWellLogData::hasTvdMslChannel() const { - return false; + return !m_tvdMslValues.empty(); } //-------------------------------------------------------------------------------------------------- @@ -122,7 +120,7 @@ bool RigImportedWellLogData::hasTvdMslChannel() const //-------------------------------------------------------------------------------------------------- bool RigImportedWellLogData::hasTvdRkbChannel() const { - return false; + return !m_tvdRkbValues.empty(); } //-------------------------------------------------------------------------------------------------- @@ -147,4 +145,20 @@ void RigImportedWellLogData::setChannelData( const QString& channelName, const s void RigImportedWellLogData::setDepthValues( const std::vector& depthValues ) { m_depthValues = depthValues; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RigImportedWellLogData::setTvdMslValues( const std::vector& tvdMslValues ) +{ + m_tvdMslValues = tvdMslValues; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RigImportedWellLogData::setTvdRkbValues( const std::vector& tvdRkbValues ) +{ + m_tvdRkbValues = tvdRkbValues; } \ No newline at end of file diff --git a/ApplicationLibCode/ReservoirDataModel/Well/RigImportedWellLogData.h b/ApplicationLibCode/ReservoirDataModel/Well/RigImportedWellLogData.h index a1dc52f495..af49021985 100644 --- a/ApplicationLibCode/ReservoirDataModel/Well/RigImportedWellLogData.h +++ b/ApplicationLibCode/ReservoirDataModel/Well/RigImportedWellLogData.h @@ -53,9 +53,13 @@ public: void setChannelData( const QString& channelName, const std::vector& values ); void setDepthValues( const std::vector& depthValues ); + void setTvdMslValues( const std::vector& tvdMslValues ); + void setTvdRkbValues( const std::vector& tvdRkbValues ); private: std::vector m_depthValues; + std::vector m_tvdMslValues; + std::vector m_tvdRkbValues; std::map> m_channelData; static const double MISSING_VALUE; diff --git a/GrpcInterface/Python/rips/PythonExamples/wells_and_fractures/well_log_import_example.py b/GrpcInterface/Python/rips/PythonExamples/wells_and_fractures/well_log_import_example.py index a6aada07e2..d0fc0a2046 100644 --- a/GrpcInterface/Python/rips/PythonExamples/wells_and_fractures/well_log_import_example.py +++ b/GrpcInterface/Python/rips/PythonExamples/wells_and_fractures/well_log_import_example.py @@ -1,62 +1,86 @@ """ Example: Import Well Log Data to Well Paths -This example demonstrates how to import well log data from Python arrays -into ResInsight well paths using the new well log import API. +This example demonstrates how to import well log data with measured depth +and multiple channels from Python arrays into ResInsight well paths using +the new well log import API. """ import rips def create_example_well_log_data(): - """Create example well log data""" - # Simulated gamma ray log data - gamma_ray = [75.2, 82.1, 78.5, 80.3, 85.7, 88.2, 90.1, 87.3, 84.6, 82.8] + """Create example well log data with measured depth and multiple channels""" + # Measured depth values in meters + measured_depth = [1000, 1200, 1400, 1600, 1800, 2000, 2200, 2400, 2600, 2800] - # Simulated porosity log data - porosity = [0.12, 0.15, 0.18, 0.22, 0.25, 0.28, 0.24, 0.20, 0.16, 0.14] + # Multiple channel data corresponding to each measured depth + channel_data = { + "gamma_ray": [75.2, 82.1, 78.5, 80.3, 85.7, 88.2, 90.1, 87.3, 84.6, 82.8], + "porosity": [0.12, 0.15, 0.18, 0.22, 0.25, 0.28, 0.24, 0.20, 0.16, 0.14], + "permeability": [15.5, 22.3, 35.8, 48.2, 65.1, 78.4, 52.9, 38.7, 25.6, 18.9], + "resistivity": [2.1, 1.8, 3.2, 2.5, 1.9, 1.4, 2.8, 3.1, 2.7, 2.3], + } - # Simulated permeability log data - permeability = [15.5, 22.3, 35.8, 48.2, 65.1, 78.4, 52.9, 38.7, 25.6, 18.9] - - return {"Gamma_Ray": gamma_ray, "Porosity": porosity, "Permeability": permeability} + return measured_depth, channel_data def main(): # Connect to ResInsight - resinsight = rips.Instance() + resinsight = rips.Instance.find() project = resinsight.project # Create a well path from coordinates coordinates = [ - [458000, 5934000, 1000], # X, Y, Z coordinates - [458100, 5934100, 1500], - [458200, 5934200, 2000], - [458300, 5934300, 2500], - [458400, 5934400, 3000], + [458000, 5934000, -1000], # X, Y, Z coordinates + [458100, 5934100, -1500], + [458200, 5934200, -2000], + [458300, 5934300, -2500], + [458400, 5934400, -3000], ] - well_path = project.well_path_collection.import_fixed_trajectory_well_path( + well_path = project.well_path_collection().import_well_path_from_points( name="ExampleWell", coordinates=coordinates ) print(f"Created well path: {well_path.name}") # Get example well log data - log_data = create_example_well_log_data() + measured_depth, channel_data = create_example_well_log_data() - # Import each well log channel - for log_name, values in log_data.items(): - well_log = well_path.add_well_log(name=log_name, values=values) - print(f"Added well log: {well_log.name} with {len(values)} data points") + # Import well log with multiple channels using the new API + well_log = well_path.add_well_log( + name="ComprehensiveWellLog", + measured_depth=measured_depth, + channel_data=channel_data, + ) - # Verify all logs were added - well_logs = well_path.well_logs() - print(f"\\nWell path now has {len(well_logs)} well logs:") - for log in well_logs: - print(f" - {log.name}") + print(f"Added well log: {well_log.name}") + print( + f" - Measured depth range: {min(measured_depth):.1f} - {max(measured_depth):.1f} m" + ) + print(f" - Number of data points: {len(measured_depth)}") + print(f" - Channels imported: {', '.join(channel_data.keys())}") - print("\\nWell log import completed successfully!") + # You can also import additional well logs with different data + # For example, a well log with different measured depth intervals + additional_measured_depth = [1100, 1300, 1500, 1700, 1900, 2100, 2300, 2500, 2700] + additional_channel_data = { + "caliper": [8.5, 8.2, 8.7, 8.3, 8.1, 8.4, 8.6, 8.2, 8.3], + "neutron": [0.18, 0.22, 0.25, 0.28, 0.31, 0.29, 0.26, 0.23, 0.20], + } + + additional_well_log = well_path.add_well_log( + name="AdditionalWellLog", + measured_depth=additional_measured_depth, + channel_data=additional_channel_data, + ) + + print(f"Added additional well log: {additional_well_log.name}") + print( + f" - MD range: {min(additional_measured_depth):.1f} - {max(additional_measured_depth):.1f} m" + ) + print(f" - Channels imported: {', '.join(additional_channel_data.keys())}") if __name__ == "__main__": diff --git a/GrpcInterface/Python/rips/tests/test_well_log_import.py b/GrpcInterface/Python/rips/tests/test_well_log_import.py index 0588e5f4ed..ad6f505a7b 100644 --- a/GrpcInterface/Python/rips/tests/test_well_log_import.py +++ b/GrpcInterface/Python/rips/tests/test_well_log_import.py @@ -1,7 +1,4 @@ import pytest -import rips - -import os @pytest.fixture @@ -30,10 +27,16 @@ def test_add_well_log_basic(project): # Test data name = "TestLog" - values = [10.5, 20.3, 30.8, 40.2, 15.7] + measured_depth = [1000, 1500, 2000, 2500, 3000] + channel_data = { + "porosity": [0.10, 0.15, 0.20, 0.18, 0.12], + "permeability": [10.5, 25.3, 45.8, 35.2, 18.7], + } # Import well log - well_log = well_path.add_well_log(name=name, values=values) + well_log = well_path.add_well_log( + name=name, measured_depth=measured_depth, channel_data=channel_data + ) # Verify well log was created assert well_log is not None @@ -44,60 +47,176 @@ def test_add_well_log_empty_name(project): """Test that empty name raises ValueError""" proj, well_path = project - values = [1.0, 2.0, 3.0] + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} with pytest.raises(ValueError, match="Name cannot be empty"): - well_path.add_well_log(name="", values=values) + well_path.add_well_log( + name="", measured_depth=measured_depth, channel_data=channel_data + ) -def test_add_well_log_empty_values(project): - """Test that empty values raises ValueError""" +def test_add_well_log_empty_measured_depth(project): + """Test that empty measured depth raises ValueError""" proj, well_path = project - with pytest.raises(ValueError, match="Values list cannot be empty"): - well_path.add_well_log(name="TestLog", values=[]) + channel_data = {"porosity": [0.1, 0.2, 0.3]} + + with pytest.raises(ValueError, match="Measured depth list cannot be empty"): + well_path.add_well_log( + name="TestLog", measured_depth=[], channel_data=channel_data + ) -def test_add_well_log_invalid_values_type(project): - """Test that non-list values raises TypeError""" +def test_add_well_log_empty_channel_data(project): + """Test that empty channel data raises ValueError""" proj, well_path = project - with pytest.raises(TypeError, match="Values must be a list"): - well_path.add_well_log(name="TestLog", values="not a list") + measured_depth = [1000, 1500, 2000] + + with pytest.raises(ValueError, match="Channel data dictionary cannot be empty"): + well_path.add_well_log( + name="TestLog", measured_depth=measured_depth, channel_data={} + ) -def test_add_well_log_non_numeric_values(project): - """Test that non-numeric values raises TypeError""" +def test_add_well_log_invalid_measured_depth_type(project): + """Test that non-list measured depth raises TypeError""" proj, well_path = project - values = [1.0, "invalid", 3.0] + channel_data = {"porosity": [0.1, 0.2, 0.3]} - with pytest.raises(TypeError, match="All values must be numeric"): - well_path.add_well_log(name="TestLog", values=values) + with pytest.raises(TypeError, match="Measured depth must be a list"): + well_path.add_well_log( + name="TestLog", measured_depth="not a list", channel_data=channel_data + ) + + +def test_add_well_log_invalid_channel_data_type(project): + """Test that non-dict channel data raises TypeError""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000] + + with pytest.raises(TypeError, match="Channel data must be a dictionary"): + well_path.add_well_log( + name="TestLog", measured_depth=measured_depth, channel_data="not a dict" + ) + + +def test_add_well_log_non_numeric_measured_depth(project): + """Test that non-numeric measured depth values raise TypeError""" + proj, well_path = project + + measured_depth = [1000, "invalid", 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} + + with pytest.raises(TypeError, match="All measured depth values must be numeric"): + well_path.add_well_log( + name="TestLog", measured_depth=measured_depth, channel_data=channel_data + ) + + +def test_add_well_log_non_numeric_channel_values(project): + """Test that non-numeric channel values raise TypeError""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, "invalid", 0.3]} + + with pytest.raises( + TypeError, match="All values in channel 'porosity' must be numeric" + ): + well_path.add_well_log( + name="TestLog", measured_depth=measured_depth, channel_data=channel_data + ) + + +def test_add_well_log_mismatched_lengths(project): + """Test that mismatched lengths raise ValueError""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2]} # Only 2 values + + with pytest.raises( + ValueError, + match="Channel 'porosity' has 2 values but measured depth has 3 values", + ): + well_path.add_well_log( + name="TestLog", measured_depth=measured_depth, channel_data=channel_data + ) + + +def test_add_well_log_empty_channel_name(project): + """Test that empty channel name raises ValueError""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000] + channel_data = {"": [0.1, 0.2, 0.3]} # Empty channel name + + with pytest.raises(ValueError, match="Channel name cannot be empty"): + well_path.add_well_log( + name="TestLog", measured_depth=measured_depth, channel_data=channel_data + ) def test_add_well_log_mixed_numeric_types(project): """Test that mixed numeric types work correctly""" proj, well_path = project - # Mix of int, float - values = [1, 2.5, 3, 4.8, 5] + # Mix of int, float in both measured depth and channel data + measured_depth = [1000, 1500.5, 2000, 2500.25, 3000] + channel_data = { + "porosity": [0.1, 0.15, 0.2, 0.18, 0.12], + "gamma_ray": [75, 80.5, 85, 82.3, 78], + } - well_log = well_path.add_well_log(name="MixedLog", values=values) + well_log = well_path.add_well_log( + name="MixedLog", measured_depth=measured_depth, channel_data=channel_data + ) assert well_log is not None assert well_log.name == "MixedLog" +def test_add_well_log_multiple_channels(project): + """Test adding well log with multiple channels""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000, 2500, 3000] + channel_data = { + "porosity": [0.10, 0.15, 0.20, 0.18, 0.12], + "permeability": [10.5, 25.3, 45.8, 35.2, 18.7], + "gamma_ray": [75.2, 82.1, 88.5, 85.3, 79.8], + "resistivity": [2.5, 3.2, 1.8, 2.1, 2.8], + } + + well_log = well_path.add_well_log( + name="MultiChannelLog", measured_depth=measured_depth, channel_data=channel_data + ) + + assert well_log is not None + assert well_log.name == "MultiChannelLog" + + def test_add_multiple_well_logs(project): """Test adding multiple well logs to same well path""" proj, well_path = project # Add first well log - log1 = well_path.add_well_log(name="Log1", values=[1.0, 2.0, 3.0]) + measured_depth1 = [1000, 1500, 2000] + channel_data1 = {"porosity": [0.1, 0.2, 0.3]} + log1 = well_path.add_well_log( + name="Log1", measured_depth=measured_depth1, channel_data=channel_data1 + ) # Add second well log - log2 = well_path.add_well_log(name="Log2", values=[10.0, 20.0, 30.0]) + measured_depth2 = [1200, 1700, 2200, 2700] + channel_data2 = {"gamma_ray": [75, 80, 85, 82]} + log2 = well_path.add_well_log( + name="Log2", measured_depth=measured_depth2, channel_data=channel_data2 + ) # Verify both logs exist assert log1 is not None @@ -105,18 +224,21 @@ def test_add_multiple_well_logs(project): assert log1.name == "Log1" assert log2.name == "Log2" - # Both logs were created successfully (verified by assertions above) - # Note: well_logs() method not currently exposed in Python API - def test_add_well_log_large_dataset(project): """Test importing large dataset""" proj, well_path = project # Generate large dataset - values = [float(i) * 0.1 for i in range(1000)] + measured_depth = [float(i * 10) for i in range(1000)] # 0, 10, 20, ..., 9990 + channel_data = { + "porosity": [0.1 + (i % 100) * 0.001 for i in range(1000)], + "gamma_ray": [70.0 + (i % 50) * 0.5 for i in range(1000)], + } - well_log = well_path.add_well_log(name="LargeLog", values=values) + well_log = well_path.add_well_log( + name="LargeLog", measured_depth=measured_depth, channel_data=channel_data + ) assert well_log is not None assert well_log.name == "LargeLog" @@ -126,9 +248,12 @@ def test_add_well_log_single_value(project): """Test adding well log with single value""" proj, well_path = project - values = [42.0] + measured_depth = [1000.0] + channel_data = {"porosity": [0.15]} - well_log = well_path.add_well_log(name="SingleValue", values=values) + well_log = well_path.add_well_log( + name="SingleValue", measured_depth=measured_depth, channel_data=channel_data + ) assert well_log is not None assert well_log.name == "SingleValue" @@ -138,9 +263,180 @@ def test_add_well_log_special_numeric_values(project): """Test adding well log with special numeric values""" proj, well_path = project - values = [0.0, -5.5, 1e6, 1.23e-4, 99.999] + measured_depth = [0.0, 1000.5, 2000.0, 3000.25, 4000.0] + channel_data = { + "special_values": [0.0, -5.5, 1e6, 1.23e-4, 99.999], + "porosity": [0.0, 0.15, 0.25, 0.18, 0.0], + } - well_log = well_path.add_well_log(name="SpecialValues", values=values) + well_log = well_path.add_well_log( + name="SpecialValues", measured_depth=measured_depth, channel_data=channel_data + ) assert well_log is not None assert well_log.name == "SpecialValues" + + +def test_add_well_log_with_tvd_msl(project): + """Test adding well log with TVD MSL values""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000, 2500, 3000] + channel_data = {"porosity": [0.10, 0.15, 0.20, 0.18, 0.12]} + tvd_msl = [980, 1480, 1980, 2480, 2980] # TVD MSL values + + well_log = well_path.add_well_log( + name="WithTvdMsl", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_msl=tvd_msl, + ) + + assert well_log is not None + assert well_log.name == "WithTvdMsl" + + +def test_add_well_log_with_tvd_rkb(project): + """Test adding well log with TVD RKB values""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000, 2500, 3000] + channel_data = {"porosity": [0.10, 0.15, 0.20, 0.18, 0.12]} + tvd_rkb = [1020, 1520, 2020, 2520, 3020] # TVD RKB values + + well_log = well_path.add_well_log( + name="WithTvdRkb", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_rkb=tvd_rkb, + ) + + assert well_log is not None + assert well_log.name == "WithTvdRkb" + + +def test_add_well_log_with_both_tvd(project): + """Test adding well log with both TVD MSL and TVD RKB values""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000, 2500, 3000] + channel_data = {"porosity": [0.10, 0.15, 0.20, 0.18, 0.12]} + tvd_msl = [980, 1480, 1980, 2480, 2980] # TVD MSL values + tvd_rkb = [1020, 1520, 2020, 2520, 3020] # TVD RKB values + + well_log = well_path.add_well_log( + name="WithBothTvd", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_msl=tvd_msl, + tvd_rkb=tvd_rkb, + ) + + assert well_log is not None + assert well_log.name == "WithBothTvd" + + +def test_add_well_log_tvd_msl_length_mismatch(project): + """Test that TVD MSL length mismatch raises ValueError""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} + tvd_msl = [980, 1480] # Only 2 values for 3 measured depths + + with pytest.raises( + ValueError, match="tvd_msl has 2 values but measured depth has 3 values" + ): + well_path.add_well_log( + name="TestLog", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_msl=tvd_msl, + ) + + +def test_add_well_log_tvd_rkb_length_mismatch(project): + """Test that TVD RKB length mismatch raises ValueError""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} + tvd_rkb = [1020, 1520, 2020, 2520] # 4 values for 3 measured depths + + with pytest.raises( + ValueError, match="tvd_rkb has 4 values but measured depth has 3 values" + ): + well_path.add_well_log( + name="TestLog", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_rkb=tvd_rkb, + ) + + +def test_add_well_log_tvd_msl_non_numeric(project): + """Test that non-numeric TVD MSL values raise TypeError""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} + tvd_msl = [980, "invalid", 1980] + + with pytest.raises(TypeError, match="All tvd_msl values must be numeric"): + well_path.add_well_log( + name="TestLog", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_msl=tvd_msl, + ) + + +def test_add_well_log_tvd_rkb_non_numeric(project): + """Test that non-numeric TVD RKB values raise TypeError""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} + tvd_rkb = [1020, 1520, "invalid"] + + with pytest.raises(TypeError, match="All tvd_rkb values must be numeric"): + well_path.add_well_log( + name="TestLog", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_rkb=tvd_rkb, + ) + + +def test_add_well_log_tvd_msl_invalid_type(project): + """Test that non-list TVD MSL raises TypeError""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} + tvd_msl = "not a list" + + with pytest.raises(TypeError, match="tvd_msl must be a list"): + well_path.add_well_log( + name="TestLog", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_msl=tvd_msl, + ) + + +def test_add_well_log_tvd_rkb_invalid_type(project): + """Test that non-list TVD RKB raises TypeError""" + proj, well_path = project + + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} + tvd_rkb = "not a list" + + with pytest.raises(TypeError, match="tvd_rkb must be a list"): + well_path.add_well_log( + name="TestLog", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_rkb=tvd_rkb, + ) diff --git a/GrpcInterface/Python/rips/tests/test_well_log_persistence.py b/GrpcInterface/Python/rips/tests/test_well_log_persistence.py new file mode 100644 index 0000000000..073d8b220c --- /dev/null +++ b/GrpcInterface/Python/rips/tests/test_well_log_persistence.py @@ -0,0 +1,73 @@ +import pytest +import tempfile +import os + + +@pytest.fixture +def project(rips_instance): + """Create a test project with a well path""" + # Create well path from coordinates + coordinates = [ + [1000, 2000, 0], + [1000, 2000, 1000], + [1100, 2100, 2000], + [1200, 2200, 3000], + ] + + well_path = ( + rips_instance.project.well_path_collection().import_well_path_from_points( + name="TestWell", coordinates=coordinates + ) + ) + + return rips_instance.project, well_path + + +def test_well_log_project_file_persistence(project): + """Test that well log data is persisted in project files""" + proj, well_path = project + + # Test data with TVD values + name = "PersistenceTestLog" + measured_depth = [1000, 1500, 2000, 2500, 3000] + channel_data = { + "porosity": [0.10, 0.15, 0.20, 0.18, 0.12], + "permeability": [10.5, 25.3, 45.8, 35.2, 18.7], + } + tvd_msl = [980, 1480, 1980, 2480, 2980] + tvd_rkb = [1020, 1520, 2020, 2520, 3020] + + # Import well log + well_log = well_path.add_well_log( + name=name, + measured_depth=measured_depth, + channel_data=channel_data, + tvd_msl=tvd_msl, + tvd_rkb=tvd_rkb, + ) + + # Verify well log was created + assert well_log is not None + assert well_log.name == name + + # Save project to temporary file + with tempfile.NamedTemporaryFile(suffix=".rsp", delete=False) as temp_file: + temp_path = temp_file.name + + try: + # Save the project + proj.save(temp_path) + + # The project should save without errors, indicating PDM persistence is working + # This test primarily verifies that the PDM objects can be serialized + assert os.path.exists(temp_path) + assert os.path.getsize(temp_path) > 0 + + print( + f"Project saved successfully to {temp_path} ({os.path.getsize(temp_path)} bytes)" + ) + + finally: + # Clean up temp file + if os.path.exists(temp_path): + os.remove(temp_path) diff --git a/GrpcInterface/Python/rips/well_path.py b/GrpcInterface/Python/rips/well_path.py index 92e8ab8945..e5ae461ded 100644 --- a/GrpcInterface/Python/rips/well_path.py +++ b/GrpcInterface/Python/rips/well_path.py @@ -4,7 +4,7 @@ from .pdmobject import add_method from .project import Project from .resinsight_classes import WellPath -from typing import Dict, List +from typing import Dict, List, Optional @add_method(WellPath) @@ -63,12 +63,22 @@ def trajectory_properties( @add_method(WellPath) -def add_well_log(self: WellPath, name: str, values: List[float]) -> object: +def add_well_log( + self: WellPath, + name: str, + measured_depth: List[float], + channel_data: Dict[str, List[float]], + tvd_msl: Optional[List[float]] = None, + tvd_rkb: Optional[List[float]] = None, +) -> object: """Add imported well log data to well path. Arguments: - name (str): Name of the well log channel. - values (List[float]): Array of well log values. + name (str): Name of the well log. + measured_depth (List[float]): Array of measured depth values. + channel_data (Dict[str, List[float]]): Dictionary mapping channel names to their values. + tvd_msl (List[float], optional): Array of TVD MSL values. Must match measured_depth length if provided. + tvd_rkb (List[float], optional): Array of TVD RKB values. Must match measured_depth length if provided. Returns: The created well log object. @@ -77,33 +87,135 @@ def add_well_log(self: WellPath, name: str, values: List[float]) -> object: if not name: raise ValueError("Name cannot be empty") - if not values: - raise ValueError("Values list cannot be empty") + if not measured_depth: + raise ValueError("Measured depth list cannot be empty") - if not isinstance(values, list): - raise TypeError("Values must be a list") + if not isinstance(measured_depth, list): + raise TypeError("Measured depth must be a list") - # Validate that all values are numeric + if not channel_data: + raise ValueError("Channel data dictionary cannot be empty") + + if not isinstance(channel_data, dict): + raise TypeError("Channel data must be a dictionary") + + # Validate measured depth values are numeric try: - float_values = [float(v) for v in values] + float_measured_depth = [float(v) for v in measured_depth] except (ValueError, TypeError) as e: - raise TypeError("All values must be numeric") from e + raise TypeError("All measured depth values must be numeric") from e - # Generate temporary key for values - temp_key = f"{uuid.uuid4()}_welllog_values" + # Validate optional TVD parameters + validated_tvd_msl = None + validated_tvd_rkb = None - # Store values in key-value store + if tvd_msl is not None: + if not isinstance(tvd_msl, list): + raise TypeError("tvd_msl must be a list") + + if len(tvd_msl) != len(measured_depth): + raise ValueError( + f"tvd_msl has {len(tvd_msl)} values but measured depth has {len(measured_depth)} values" + ) + + try: + validated_tvd_msl = [float(v) for v in tvd_msl] + except (ValueError, TypeError) as e: + raise TypeError("All tvd_msl values must be numeric") from e + + if tvd_rkb is not None: + if not isinstance(tvd_rkb, list): + raise TypeError("tvd_rkb must be a list") + + if len(tvd_rkb) != len(measured_depth): + raise ValueError( + f"tvd_rkb has {len(tvd_rkb)} values but measured depth has {len(measured_depth)} values" + ) + + try: + validated_tvd_rkb = [float(v) for v in tvd_rkb] + except (ValueError, TypeError) as e: + raise TypeError("All tvd_rkb values must be numeric") from e + + # Validate channel data + validated_channels = {} + for channel_name, values in channel_data.items(): + if not channel_name: + raise ValueError("Channel name cannot be empty") + + if not isinstance(values, list): + raise TypeError(f"Values for channel '{channel_name}' must be a list") + + if not values: + raise ValueError( + f"Values list for channel '{channel_name}' cannot be empty" + ) + + if len(values) != len(measured_depth): + raise ValueError( + f"Channel '{channel_name}' has {len(values)} values but measured depth has {len(measured_depth)} values" + ) + + # Validate that all values are numeric + try: + validated_channels[channel_name] = [float(v) for v in values] + except (ValueError, TypeError) as e: + raise TypeError( + f"All values in channel '{channel_name}' must be numeric" + ) from e + + # Store data in key-value store and prepare for GRPC call project = self.ancestor(Project) if not project: raise RuntimeError("Could not find project") - project.set_key_values(temp_key, float_values) + # Generate unique keys for data transfer + shared_uuid = uuid.uuid4() + measured_depth_key = f"{shared_uuid}_measured_depth" + tvd_msl_key = f"{shared_uuid}_tvd_msl" if validated_tvd_msl is not None else "" + tvd_rkb_key = f"{shared_uuid}_tvd_rkb" if validated_tvd_rkb is not None else "" + + # Store measured depth + project.set_key_values(measured_depth_key, float_measured_depth) + + # Store optional TVD data + if validated_tvd_msl is not None: + project.set_key_values(tvd_msl_key, validated_tvd_msl) + if validated_tvd_rkb is not None: + project.set_key_values(tvd_rkb_key, validated_tvd_rkb) + + # Store all channels and build channel mappings + temp_keys = [measured_depth_key] + if validated_tvd_msl is not None: + temp_keys.append(tvd_msl_key) + if validated_tvd_rkb is not None: + temp_keys.append(tvd_rkb_key) + channel_mappings = [] try: + for channel_name, values in validated_channels.items(): + channel_key = f"{shared_uuid}_channel_{channel_name}" + project.set_key_values(channel_key, values) + temp_keys.append(channel_key) + channel_mappings.append(f"{channel_name}:{channel_key}") + + # Create comma-separated string for channel mappings + channel_keys_csv = ",".join(channel_mappings) + # Call the internal GRPC method - well_log = self.add_well_log_internal(name=name, values_key=temp_key) + well_log = self.add_well_log_internal( + name=name, + measured_depth_key=measured_depth_key, + channel_keys_csv=channel_keys_csv, + tvd_msl_key=tvd_msl_key, + tvd_rkb_key=tvd_rkb_key, + ) return well_log except Exception as e: - # Clean up key-value store on failure - project.remove_key_values(temp_key) + # Clean up all temporary keys on failure + for temp_key in temp_keys: + try: + project.remove_key_values(temp_key) + except Exception: + pass # Ignore cleanup errors raise RuntimeError(f"Failed to create well log: {str(e)}") from e