#13908 Python: Add API to read well log information

Expose well logs attached to a well path through the Python API. A new
`well_path.well_logs()` returns the list of well logs, and each
`WellLog` gains `channel_names()` and `well_log_data()` for reading
channel names, measured depth, optional TVD MSL/RKB, and channel
values.

The read methods are registered on the abstract `RimWellLog` base
class and use the common `RigWellLogData` interface so they work for
imported, LAS and OSDU well logs alike.
This commit is contained in:
Kristian Bendiksen committed 2026-04-23 19:50:05 +02:00
1 parent f91f77d7db
commit 2d88be506d
13 files changed
+531 -9

No files matched your search

@@ -26,6 +26,9 @@
#include "RimWellPathCollection.h"
#include "RimWellPlotTools.h"
#include "cafPdmFieldScriptingCapability.h"
#include "cafPdmObjectScriptingCapability.h"
#include <QString>
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 );
@@ -27,6 +27,8 @@
#include "RiaFieldHandleTools.h"
#include "RiaQDateTimeTools.h"
#include "cafPdmFieldScriptingCapability.h"
#include "cafPdmObjectScriptingCapability.h"
#include "cafPdmUiDateEditor.h"
#include <QString>
@@ -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" );
@@ -18,6 +18,9 @@
#include "RimWellLogFile.h"
#include "cafPdmFieldScriptingCapability.h"
#include "cafPdmObjectScriptingCapability.h"
#include <QString>
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 );
}
@@ -36,6 +36,9 @@
#include "Riu3DMainWindowTools.h"
#include "cafPdmFieldScriptingCapability.h"
#include "cafPdmObjectScriptingCapability.h"
#include <QFileInfo>
#include <QString>
#include <QStringList>
@@ -57,13 +60,13 @@ void caf::AppEnum<RimWellLogLasFile::WellFlowCondition>::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 );
@@ -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" );
@@ -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
@@ -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 <http://www.gnu.org/licenses/gpl.html>
// 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 <QStringList>
namespace
{
std::vector<float> toFloatVector( const std::vector<double>& values )
{
std::vector<float> result;
result.reserve( values.size() );
for ( double v : values )
{
result.push_back( static_cast<float>( v ) );
}
return result;
}
void writeFloatArrayToKeyValueStore( const QString& key, const std::vector<double>& 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<caf::PdmObjectHandle*, QString> RimcWellLog_channelNames::execute()
{
auto wellLog = self<RimWellLog>();
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<QString> 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<caf::PdmObjectHandle*, QString> RimcWellLog_readWellLogDataInternal::execute()
{
auto wellLog = self<RimWellLog>();
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;
}
@@ -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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafPdmField.h"
#include "cafPdmObjectHandle.h"
#include "cafPdmObjectMethod.h"
#include <QString>
//==================================================================================================
///
//==================================================================================================
class RimcWellLog_channelNames : public caf::PdmObjectMethod
{
CAF_PDM_HEADER_INIT;
public:
RimcWellLog_channelNames( caf::PdmObjectHandle* self );
std::expected<caf::PdmObjectHandle*, QString> execute() override;
QString classKeywordReturnedType() const override;
};
//==================================================================================================
///
//==================================================================================================
class RimcWellLog_readWellLogDataInternal : public caf::PdmVoidObjectMethod
{
CAF_PDM_HEADER_INIT;
public:
RimcWellLog_readWellLogDataInternal( caf::PdmObjectHandle* self );
std::expected<caf::PdmObjectHandle*, QString> execute() override;
private:
caf::PdmField<QString> m_measuredDepthKey;
caf::PdmField<QString> m_tvdMslKey;
caf::PdmField<QString> m_tvdRkbKey;
caf::PdmField<QString> m_channelKeysCsv;
};
@@ -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()
+1
View File
@@ -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
+3
View File
@@ -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
@@ -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])
+81
View File
@@ -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