diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimOsduWellLog.cpp b/ApplicationLibCode/ProjectDataModel/WellLog/RimOsduWellLog.cpp index 919a37483b..18daf56539 100644 --- a/ApplicationLibCode/ProjectDataModel/WellLog/RimOsduWellLog.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimOsduWellLog.cpp @@ -26,6 +26,9 @@ #include "RimWellPathCollection.h" #include "RimWellPlotTools.h" +#include "cafPdmFieldScriptingCapability.h" +#include "cafPdmObjectScriptingCapability.h" + #include CAF_PDM_SOURCE_INIT( RimOsduWellLog, "OsduWellLog" ); @@ -35,9 +38,9 @@ CAF_PDM_SOURCE_INIT( RimOsduWellLog, "OsduWellLog" ); //-------------------------------------------------------------------------------------------------- RimOsduWellLog::RimOsduWellLog() { - CAF_PDM_InitObject( "OSDU Well Log", ":/LasFile16x16.png" ); + CAF_PDM_InitScriptableObject( "OSDU Well Log", ":/LasFile16x16.png" ); - CAF_PDM_InitFieldNoDefault( &m_name, "Name", "" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_name, "Name", "" ); m_name.uiCapability()->setUiReadOnly( true ); m_date.uiCapability()->setUiReadOnly( true ); diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLog.cpp b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLog.cpp index fcdd9656fa..5d853dce10 100644 --- a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLog.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLog.cpp @@ -27,6 +27,8 @@ #include "RiaFieldHandleTools.h" #include "RiaQDateTimeTools.h" +#include "cafPdmFieldScriptingCapability.h" +#include "cafPdmObjectScriptingCapability.h" #include "cafPdmUiDateEditor.h" #include @@ -43,7 +45,7 @@ const QDateTime RimWellLog::DEFAULT_DATE_TIME = RiaQDateTimeTools::createUtcDate //-------------------------------------------------------------------------------------------------- RimWellLog::RimWellLog() { - CAF_PDM_InitObject( "Well File Info", ":/LasFile16x16.png" ); + CAF_PDM_InitScriptableObject( "Well File Info", ":/LasFile16x16.png" ); CAF_PDM_InitFieldNoDefault( &m_date, "Date", "Date" ); diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogFile.cpp b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogFile.cpp index 6c0435711d..49484ff26d 100644 --- a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogFile.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogFile.cpp @@ -18,6 +18,9 @@ #include "RimWellLogFile.h" +#include "cafPdmFieldScriptingCapability.h" +#include "cafPdmObjectScriptingCapability.h" + #include CAF_PDM_ABSTRACT_SOURCE_INIT( RimWellLogFile, "WellLogFileInterface" ); @@ -27,9 +30,9 @@ CAF_PDM_ABSTRACT_SOURCE_INIT( RimWellLogFile, "WellLogFileInterface" ); //-------------------------------------------------------------------------------------------------- RimWellLogFile::RimWellLogFile() { - CAF_PDM_InitObject( "Well File Info", ":/LasFile16x16.png" ); + CAF_PDM_InitScriptableObject( "Well File Info", ":/LasFile16x16.png" ); - CAF_PDM_InitFieldNoDefault( &m_fileName, "FileName", "Filename" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_fileName, "FileName", "Filename" ); m_fileName.uiCapability()->setUiReadOnly( true ); } diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogLasFile.cpp b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogLasFile.cpp index 653fba9e9d..2e4e009ddd 100644 --- a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogLasFile.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogLasFile.cpp @@ -36,6 +36,9 @@ #include "Riu3DMainWindowTools.h" +#include "cafPdmFieldScriptingCapability.h" +#include "cafPdmObjectScriptingCapability.h" + #include #include #include @@ -57,13 +60,13 @@ void caf::AppEnum::setUp() //-------------------------------------------------------------------------------------------------- RimWellLogLasFile::RimWellLogLasFile() { - CAF_PDM_InitObject( "Well LAS File Info", ":/LasFile16x16.png" ); + CAF_PDM_InitScriptableObjectWithNameAndComment( "Well LAS File Info", ":/LasFile16x16.png", "", "", "WellLogLasFile", "" ); - CAF_PDM_InitFieldNoDefault( &m_wellName, "WellName", "" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_wellName, "WellName", "" ); m_wellName.uiCapability()->setUiReadOnly( true ); RiaFieldHandleTools::disableWriteAndSetFieldHidden( &m_wellName ); - CAF_PDM_InitFieldNoDefault( &m_name, "Name", "" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_name, "Name", "" ); m_name.uiCapability()->setUiReadOnly( true ); RiaFieldHandleTools::disableWriteAndSetFieldHidden( &m_name ); diff --git a/ApplicationLibCode/ProjectDataModel/WellPath/RimWellPath.cpp b/ApplicationLibCode/ProjectDataModel/WellPath/RimWellPath.cpp index 8d05a78b77..5d433cd239 100644 --- a/ApplicationLibCode/ProjectDataModel/WellPath/RimWellPath.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellPath/RimWellPath.cpp @@ -134,7 +134,7 @@ RimWellPath::RimWellPath() CAF_PDM_InitScriptableFieldNoDefault( &m_completionSettings, "CompletionSettings", "Completion Settings" ); m_completionSettings = new RimWellPathCompletionSettings; - CAF_PDM_InitFieldNoDefault( &m_wellLogs, "WellLogs", "Well Logs" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_wellLogs, "WellLogs", "Well Logs" ); m_wellLogs.registerKeywordAlias( "WellLogFiles" ); CAF_PDM_InitFieldNoDefault( &m_3dWellLogCurves, "CollectionOf3dWellLogCurves", "3D Track" ); diff --git a/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake index cc9e7b2a29..757fd39821 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake @@ -13,6 +13,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RimcDataContainerDouble.h ${CMAKE_CURRENT_LIST_DIR}/RimcDataContainerString.h ${CMAKE_CURRENT_LIST_DIR}/RimcDataContainerTime.h + ${CMAKE_CURRENT_LIST_DIR}/RimcWellLog.h ${CMAKE_CURRENT_LIST_DIR}/RimcWellLogPlotCollection.h ${CMAKE_CURRENT_LIST_DIR}/RimcWellLogPlot.h ${CMAKE_CURRENT_LIST_DIR}/RimcWellLogTrack.h @@ -56,6 +57,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimcDataContainerDouble.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcDataContainerString.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcDataContainerTime.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimcWellLog.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcWellLogPlotCollection.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcWellLogPlot.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcWellLogTrack.cpp diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellLog.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcWellLog.cpp new file mode 100644 index 0000000000..f1355c991f --- /dev/null +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellLog.cpp @@ -0,0 +1,168 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimcWellLog.h" + +#include "KeyValueStore/RiaKeyValueStoreUtil.h" +#include "RiaApplication.h" + +#include "RimWellLog.h" +#include "RimcDataContainerString.h" + +#include "Well/RigWellLogData.h" + +#include "cafPdmFieldScriptingCapability.h" +#include "cafPdmObjectScriptingCapability.h" + +#include + +namespace +{ +std::vector toFloatVector( const std::vector& values ) +{ + std::vector result; + result.reserve( values.size() ); + for ( double v : values ) + { + result.push_back( static_cast( v ) ); + } + return result; +} + +void writeFloatArrayToKeyValueStore( const QString& key, const std::vector& values ) +{ + auto floatValues = toFloatVector( values ); + RiaApplication::instance()->keyValueStore()->set( key.toStdString(), RiaKeyValueStoreUtil::convertToByteVector( floatValues ) ); +} +} // namespace + +CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimWellLog, RimcWellLog_channelNames, "ChannelNamesInternal" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcWellLog_channelNames::RimcWellLog_channelNames( caf::PdmObjectHandle* self ) + : PdmObjectMethod( self, PdmObjectMethod::NullPointerType::NULL_IS_INVALID, PdmObjectMethod::ResultType::PERSISTENT_FALSE ) +{ + CAF_PDM_InitObject( "Channel Names", "", "", "Channel names for this well log." ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RimcWellLog_channelNames::execute() +{ + auto wellLog = self(); + if ( !wellLog ) return std::unexpected( "No well log found" ); + + RigWellLogData* data = wellLog->wellLogData(); + if ( !data ) return std::unexpected( "Well log has no data" ); + + auto container = new RimcDataContainerString(); + + std::vector names; + for ( const QString& name : data->wellLogChannelNames() ) + { + names.push_back( name ); + } + container->m_stringValues = names; + + return container; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimcWellLog_channelNames::classKeywordReturnedType() const +{ + return RimcDataContainerString::classKeywordStatic(); +} + +CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimWellLog, RimcWellLog_readWellLogDataInternal, "ReadWellLogDataInternal" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcWellLog_readWellLogDataInternal::RimcWellLog_readWellLogDataInternal( caf::PdmObjectHandle* self ) + : caf::PdmVoidObjectMethod( self ) +{ + CAF_PDM_InitObject( "Read Well Log Data", "", "", "Read Well Log Data" ); + + CAF_PDM_InitScriptableField( &m_measuredDepthKey, "MeasuredDepthKey", QString(), "Measured Depth Key" ); + CAF_PDM_InitScriptableField( &m_tvdMslKey, "TvdMslKey", QString(), "TVD MSL Key" ); + CAF_PDM_InitScriptableField( &m_tvdRkbKey, "TvdRkbKey", QString(), "TVD RKB Key" ); + CAF_PDM_InitScriptableField( &m_channelKeysCsv, "ChannelKeysCsv", QString(), "Channel Keys CSV" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RimcWellLog_readWellLogDataInternal::execute() +{ + auto wellLog = self(); + if ( !wellLog ) return std::unexpected( "No well log found" ); + + RigWellLogData* data = wellLog->wellLogData(); + if ( !data ) return std::unexpected( "Well log has no data" ); + + if ( !m_measuredDepthKey().isEmpty() ) + { + writeFloatArrayToKeyValueStore( m_measuredDepthKey(), data->depthValues() ); + } + + if ( !m_tvdMslKey().isEmpty() && data->hasTvdMslChannel() ) + { + writeFloatArrayToKeyValueStore( m_tvdMslKey(), data->tvdMslValues() ); + } + + if ( !m_tvdRkbKey().isEmpty() && data->hasTvdRkbChannel() ) + { + writeFloatArrayToKeyValueStore( m_tvdRkbKey(), data->tvdRkbValues() ); + } + + if ( !m_channelKeysCsv().isEmpty() ) + { + QStringList availableChannels = data->wellLogChannelNames(); + QStringList channelMappings = m_channelKeysCsv().split( ",", Qt::SkipEmptyParts ); + 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 ) ); + } + + if ( !availableChannels.contains( channelName ) ) + { + return std::unexpected( QString( "Channel '%1' not found in well log" ).arg( channelName ) ); + } + + writeFloatArrayToKeyValueStore( channelKey, data->values( channelName ) ); + } + } + + return nullptr; +} diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellLog.h b/ApplicationLibCode/ProjectDataModelCommands/RimcWellLog.h new file mode 100644 index 0000000000..6b00f93d13 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellLog.h @@ -0,0 +1,58 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "cafPdmField.h" +#include "cafPdmObjectHandle.h" +#include "cafPdmObjectMethod.h" + +#include + +//================================================================================================== +/// +//================================================================================================== +class RimcWellLog_channelNames : public caf::PdmObjectMethod +{ + CAF_PDM_HEADER_INIT; + +public: + RimcWellLog_channelNames( caf::PdmObjectHandle* self ); + + std::expected execute() override; + QString classKeywordReturnedType() const override; +}; + +//================================================================================================== +/// +//================================================================================================== +class RimcWellLog_readWellLogDataInternal : public caf::PdmVoidObjectMethod +{ + CAF_PDM_HEADER_INIT; + +public: + RimcWellLog_readWellLogDataInternal( caf::PdmObjectHandle* self ); + + std::expected execute() override; + +private: + caf::PdmField m_measuredDepthKey; + caf::PdmField m_tvdMslKey; + caf::PdmField m_tvdRkbKey; + caf::PdmField m_channelKeysCsv; +}; 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 d0fc0a2046..dfc390a3fe 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 @@ -82,6 +82,22 @@ def main(): ) print(f" - Channels imported: {', '.join(additional_channel_data.keys())}") + # Read the well logs back using the well log read API + print("\nReading well logs back from the project:") + for log in well_path.well_logs(): + data = log.well_log_data() + md = data["measured_depth"] + channel_names = [ + key for key in data if key not in ("measured_depth", "tvd_msl", "tvd_rkb") + ] + print(f" {log.name}: {len(md)} samples, channels = {channel_names}") + + depth_keys = [k for k in ("measured_depth", "tvd_msl", "tvd_rkb") if k in data] + columns = depth_keys + channel_names + for label, idx in (("First", 0), ("Last", len(md) - 1)): + parts = [f"{key}={data[key][idx]:.3f}" for key in columns] + print(f" {label}: {', '.join(parts)}") + if __name__ == "__main__": main() diff --git a/GrpcInterface/Python/rips/__init__.py b/GrpcInterface/Python/rips/__init__.py index 4a23fdfc23..477d8637ae 100644 --- a/GrpcInterface/Python/rips/__init__.py +++ b/GrpcInterface/Python/rips/__init__.py @@ -22,6 +22,7 @@ from .contour_map import ( # noqa: E402 ) from .well_log_plot import WellLogPlot as WellLogPlot # noqa: E402 from .well_path import WellPath as WellPath # noqa: E402 +from . import well_log as well_log # noqa: F401, E402 from . import well_path_collection as well_path_collection # noqa: F401, E402 from .simulation_well import SimulationWell as SimulationWell # noqa: E402 from .exception import RipsError as RipsError # noqa: E402 diff --git a/GrpcInterface/Python/rips/mypy.ini b/GrpcInterface/Python/rips/mypy.ini index 9335d66804..7db0989e1c 100644 --- a/GrpcInterface/Python/rips/mypy.ini +++ b/GrpcInterface/Python/rips/mypy.ini @@ -43,6 +43,9 @@ disable_error_code = no-any-return, no-untyped-def [mypy-rips.project.*] disable_error_code = no-untyped-def, no-any-return, attr-defined +[mypy-rips.well_log.*] +disable_error_code = attr-defined, no-any-return + [mypy-rips.well_log_plot.*] disable_error_code = no-any-return diff --git a/GrpcInterface/Python/rips/tests/test_well_log_read.py b/GrpcInterface/Python/rips/tests/test_well_log_read.py new file mode 100644 index 0000000000..127b7bc02c --- /dev/null +++ b/GrpcInterface/Python/rips/tests/test_well_log_read.py @@ -0,0 +1,182 @@ +import pytest + + +@pytest.fixture +def project(rips_instance): + """Create a test project with a well path.""" + 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 _approx(expected): + return pytest.approx(expected, rel=1e-5, abs=1e-5) + + +def test_well_logs_empty(project): + """A fresh well path has no well logs.""" + _, well_path = project + assert well_path.well_logs() == [] + + +def test_well_logs_lists_added_log(project): + """Adding a well log makes it visible via well_logs().""" + _, well_path = project + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} + well_path.add_well_log( + name="Log1", measured_depth=measured_depth, channel_data=channel_data + ) + + logs = well_path.well_logs() + assert len(logs) == 1 + assert logs[0].name == "Log1" + + +def test_channel_names(project): + """channel_names() returns exactly the channels that were added.""" + _, well_path = project + channel_data = {"porosity": [0.1, 0.2, 0.3], "gamma_ray": [70.0, 75.0, 80.0]} + well_path.add_well_log( + name="Log1", measured_depth=[1000, 1500, 2000], channel_data=channel_data + ) + + log = well_path.well_logs()[0] + assert sorted(log.channel_names()) == sorted(channel_data.keys()) + + +def test_well_log_data_round_trip_no_tvd(project): + """Write a well log and read it back; measured depth and channels match.""" + _, well_path = project + measured_depth = [1000.0, 1500.0, 2000.0, 2500.0, 3000.0] + channel_data = { + "porosity": [0.10, 0.15, 0.20, 0.18, 0.12], + "permeability": [10.5, 25.3, 45.8, 35.2, 18.7], + } + well_path.add_well_log( + name="RoundTrip", measured_depth=measured_depth, channel_data=channel_data + ) + + data = well_path.well_logs()[0].well_log_data() + + assert set(data.keys()) == {"measured_depth", "porosity", "permeability"} + assert data["measured_depth"] == _approx(measured_depth) + assert data["porosity"] == _approx(channel_data["porosity"]) + assert data["permeability"] == _approx(channel_data["permeability"]) + assert "tvd_msl" not in data + assert "tvd_rkb" not in data + + +def test_well_log_data_with_tvd_msl(project): + """TVD MSL values are returned when the well log has them.""" + _, well_path = project + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} + tvd_msl = [980, 1480, 1980] + well_path.add_well_log( + name="WithMsl", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_msl=tvd_msl, + ) + + data = well_path.well_logs()[0].well_log_data() + + assert "tvd_msl" in data + assert data["tvd_msl"] == _approx(tvd_msl) + assert "tvd_rkb" not in data + + +def test_well_log_data_with_tvd_rkb(project): + """TVD RKB values are returned when the well log has them.""" + _, well_path = project + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} + tvd_rkb = [1020, 1520, 2020] + well_path.add_well_log( + name="WithRkb", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_rkb=tvd_rkb, + ) + + data = well_path.well_logs()[0].well_log_data() + + assert "tvd_rkb" in data + assert data["tvd_rkb"] == _approx(tvd_rkb) + assert "tvd_msl" not in data + + +def test_well_log_data_with_both_tvd(project): + """Both TVD MSL and TVD RKB values are returned when both are present.""" + _, well_path = project + measured_depth = [1000, 1500, 2000] + channel_data = {"porosity": [0.1, 0.2, 0.3]} + tvd_msl = [980, 1480, 1980] + tvd_rkb = [1020, 1520, 2020] + well_path.add_well_log( + name="Both", + measured_depth=measured_depth, + channel_data=channel_data, + tvd_msl=tvd_msl, + tvd_rkb=tvd_rkb, + ) + + data = well_path.well_logs()[0].well_log_data() + + assert data["tvd_msl"] == _approx(tvd_msl) + assert data["tvd_rkb"] == _approx(tvd_rkb) + + +def test_multiple_well_logs(project): + """Each well log is readable independently after adding multiple logs.""" + _, well_path = project + well_path.add_well_log( + name="LogA", + measured_depth=[1000, 1500, 2000], + channel_data={"porosity": [0.1, 0.2, 0.3]}, + ) + well_path.add_well_log( + name="LogB", + measured_depth=[1200, 1700, 2200, 2700], + channel_data={"gamma_ray": [70.0, 75.0, 80.0, 85.0]}, + ) + + logs = well_path.well_logs() + assert len(logs) == 2 + + by_name = {log.name: log for log in logs} + assert set(by_name.keys()) == {"LogA", "LogB"} + + data_a = by_name["LogA"].well_log_data() + data_b = by_name["LogB"].well_log_data() + + assert data_a["measured_depth"] == _approx([1000, 1500, 2000]) + assert data_a["porosity"] == _approx([0.1, 0.2, 0.3]) + assert data_b["measured_depth"] == _approx([1200, 1700, 2200, 2700]) + assert data_b["gamma_ray"] == _approx([70.0, 75.0, 80.0, 85.0]) + + +def test_well_log_single_value(project): + """1-element arrays survive a round-trip.""" + _, well_path = project + well_path.add_well_log( + name="Single", + measured_depth=[1234.5], + channel_data={"porosity": [0.17]}, + ) + + data = well_path.well_logs()[0].well_log_data() + assert data["measured_depth"] == _approx([1234.5]) + assert data["porosity"] == _approx([0.17]) diff --git a/GrpcInterface/Python/rips/well_log.py b/GrpcInterface/Python/rips/well_log.py new file mode 100644 index 0000000000..b8830a8dbf --- /dev/null +++ b/GrpcInterface/Python/rips/well_log.py @@ -0,0 +1,81 @@ +import uuid +from typing import Dict, List, Optional + +from .exception import RipsError +from .pdmobject import add_method +from .project import Project +from .resinsight_classes import WellLog + + +def _try_read_key(project: Project, key: str) -> Optional[List[float]]: + try: + return project.key_values(key) + except RipsError: + return None + + +@add_method(WellLog) +def channel_names(self: WellLog) -> List[str]: + """Get the list of channel names for this well log. + + Returns: + A list of channel name strings. + """ + return self.channel_names_internal().values + + +@add_method(WellLog) +def well_log_data(self: WellLog) -> Dict[str, List[float]]: + """Read all well log data (measured depth, optional TVD arrays, and channel values). + + Returns: + A dictionary with: + - "measured_depth" (always present): List of measured depth values. + - "tvd_msl" (only if the well log has TVD MSL values). + - "tvd_rkb" (only if the well log has TVD RKB values). + - One entry per channel name in `channel_names()`. + """ + project = self.ancestor(Project) + if project is None: + raise RipsError("Could not find parent project") + + names = self.channel_names() + + shared_uuid = uuid.uuid4() + md_key = f"{shared_uuid}_md" + tvd_msl_key = f"{shared_uuid}_tvd_msl" + tvd_rkb_key = f"{shared_uuid}_tvd_rkb" + channel_keys = {name: f"{shared_uuid}_ch_{name}" for name in names} + channel_keys_csv = ",".join(f"{name}:{key}" for name, key in channel_keys.items()) + + try: + self.read_well_log_data_internal( + measured_depth_key=md_key, + tvd_msl_key=tvd_msl_key, + tvd_rkb_key=tvd_rkb_key, + channel_keys_csv=channel_keys_csv, + ) + + result: Dict[str, List[float]] = { + "measured_depth": project.key_values(md_key), + } + + tvd_msl = _try_read_key(project, tvd_msl_key) + if tvd_msl: + result["tvd_msl"] = tvd_msl + + tvd_rkb = _try_read_key(project, tvd_rkb_key) + if tvd_rkb: + result["tvd_rkb"] = tvd_rkb + + for name, key in channel_keys.items(): + result[name] = project.key_values(key) + + return result + finally: + cleanup_keys = [md_key, tvd_msl_key, tvd_rkb_key, *channel_keys.values()] + for key in cleanup_keys: + try: + project.remove_key_values(key) + except Exception: + pass