mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-25 22:50:25 -05:00
#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:
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;
|
||||
};
|
||||
+16
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
@@ -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
|
||||
Reference in new issue
Block a user