well-log-toolkit 0.1.126__tar.gz → 0.1.128__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/PKG-INFO +1 -1
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/pyproject.toml +1 -1
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit/visualization.py +233 -90
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit.egg-info/PKG-INFO +1 -1
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/README.md +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/setup.cfg +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit/__init__.py +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit/exceptions.py +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit/las_file.py +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit/manager.py +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit/operations.py +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit/property.py +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit/regression.py +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit/statistics.py +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit/utils.py +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit/well.py +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit.egg-info/requires.txt +0 -0
- {well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "well-log-toolkit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.128"
|
|
8
8
|
description = "Fast LAS file processing with lazy loading and filtering for well log analysis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -24,6 +24,7 @@ from .regression import (
|
|
|
24
24
|
LinearRegression, LogarithmicRegression, ExponentialRegression,
|
|
25
25
|
PolynomialRegression, PowerRegression
|
|
26
26
|
)
|
|
27
|
+
from .exceptions import PropertyNotFoundError
|
|
27
28
|
|
|
28
29
|
# Default color palettes
|
|
29
30
|
DEFAULT_COLORS = [
|
|
@@ -2760,24 +2761,34 @@ class Crossplot:
|
|
|
2760
2761
|
Create beautiful, modern crossplots for well log analysis.
|
|
2761
2762
|
|
|
2762
2763
|
Supports single and multi-well crossplots with extensive customization options
|
|
2763
|
-
including color mapping, size mapping, shape mapping,
|
|
2764
|
+
including color mapping, size mapping, shape mapping, regression analysis, and
|
|
2765
|
+
multi-layer plotting for combining different data types (e.g., Core vs Sidewall).
|
|
2764
2766
|
|
|
2765
2767
|
Parameters
|
|
2766
2768
|
----------
|
|
2767
2769
|
wells : Well or list of Well
|
|
2768
2770
|
Single well or list of wells to plot
|
|
2769
|
-
x : str
|
|
2770
|
-
Name of property for x-axis
|
|
2771
|
-
y : str
|
|
2772
|
-
Name of property for y-axis
|
|
2771
|
+
x : str, optional
|
|
2772
|
+
Name of property for x-axis. Required if layers is not provided.
|
|
2773
|
+
y : str, optional
|
|
2774
|
+
Name of property for y-axis. Required if layers is not provided.
|
|
2775
|
+
layers : dict[str, list[str]], optional
|
|
2776
|
+
Dictionary mapping layer labels to [x_property, y_property] lists.
|
|
2777
|
+
Use this to combine multiple property pairs in a single plot.
|
|
2778
|
+
Example: {"Core": ["CorePor", "CorePerm"], "Sidewall": ["SWPor", "SWPerm"]}
|
|
2779
|
+
When using layers, you can use "label" for color/shape/size to differentiate
|
|
2780
|
+
between layer types. Default: None
|
|
2773
2781
|
shape : str, optional
|
|
2774
|
-
Property name for shape mapping. Use "well" to map shapes by well name
|
|
2782
|
+
Property name for shape mapping. Use "well" to map shapes by well name,
|
|
2783
|
+
or "label" (when using layers) to map shapes by layer type.
|
|
2775
2784
|
Default: None (single shape)
|
|
2776
2785
|
color : str, optional
|
|
2777
|
-
Property name for color mapping. Use "depth" to color by depth
|
|
2786
|
+
Property name for color mapping. Use "depth" to color by depth,
|
|
2787
|
+
or "label" (when using layers) to color by layer type.
|
|
2778
2788
|
Default: None (single color)
|
|
2779
2789
|
size : str, optional
|
|
2780
|
-
Property name for size mapping
|
|
2790
|
+
Property name for size mapping, or "label" (when using layers) to
|
|
2791
|
+
size by layer type.
|
|
2781
2792
|
Default: None (constant size)
|
|
2782
2793
|
colortemplate : str, optional
|
|
2783
2794
|
Matplotlib colormap name (e.g., "viridis", "plasma", "coolwarm")
|
|
@@ -2883,6 +2894,38 @@ class Crossplot:
|
|
|
2883
2894
|
... )
|
|
2884
2895
|
>>> plot.show()
|
|
2885
2896
|
|
|
2897
|
+
Combining multiple data types with layers (Core + Sidewall):
|
|
2898
|
+
|
|
2899
|
+
>>> plot = manager.Crossplot(
|
|
2900
|
+
... layers={
|
|
2901
|
+
... "Core": ["CorePor_obds", "CorePerm_obds"],
|
|
2902
|
+
... "Sidewall": ["SidewallPor_ob", "SidewallPerm_ob"]
|
|
2903
|
+
... },
|
|
2904
|
+
... shape="label", # Different shapes for Core vs Sidewall
|
|
2905
|
+
... y_log=True
|
|
2906
|
+
... )
|
|
2907
|
+
>>> plot.show()
|
|
2908
|
+
|
|
2909
|
+
Using add_layer method with method chaining:
|
|
2910
|
+
|
|
2911
|
+
>>> manager.Crossplot(y_log=True) \\
|
|
2912
|
+
... .add_layer("CorePor_obds", "CorePerm_obds", label="Core") \\
|
|
2913
|
+
... .add_layer("SidewallPor_ob", "SidewallPerm_ob", label="Sidewall") \\
|
|
2914
|
+
... .show()
|
|
2915
|
+
|
|
2916
|
+
Layers with regression by color (single trend per well):
|
|
2917
|
+
|
|
2918
|
+
>>> plot = manager.Crossplot(
|
|
2919
|
+
... layers={
|
|
2920
|
+
... "Core": ["CorePor_obds", "CorePerm_obds"],
|
|
2921
|
+
... "Sidewall": ["SidewallPor_ob", "SidewallPerm_ob"]
|
|
2922
|
+
... },
|
|
2923
|
+
... shape="label", # Different shapes for data types
|
|
2924
|
+
... color="well", # Color by well
|
|
2925
|
+
... regression_by_color="linear" # One trend per well (combining both data types)
|
|
2926
|
+
... )
|
|
2927
|
+
>>> plot.show()
|
|
2928
|
+
|
|
2886
2929
|
Access regression objects:
|
|
2887
2930
|
|
|
2888
2931
|
>>> linear_regs = plot.regression("linear")
|
|
@@ -2893,8 +2936,9 @@ class Crossplot:
|
|
|
2893
2936
|
def __init__(
|
|
2894
2937
|
self,
|
|
2895
2938
|
wells: Union['Well', list['Well']],
|
|
2896
|
-
x: str,
|
|
2897
|
-
y: str,
|
|
2939
|
+
x: Optional[str] = None,
|
|
2940
|
+
y: Optional[str] = None,
|
|
2941
|
+
layers: Optional[dict[str, list[str]]] = None,
|
|
2898
2942
|
shape: Optional[str] = None,
|
|
2899
2943
|
color: Optional[str] = None,
|
|
2900
2944
|
size: Optional[str] = None,
|
|
@@ -2931,6 +2975,31 @@ class Crossplot:
|
|
|
2931
2975
|
else:
|
|
2932
2976
|
self.wells = wells
|
|
2933
2977
|
|
|
2978
|
+
# Validate input: either (x, y) or layers must be provided
|
|
2979
|
+
if layers is None and (x is None or y is None):
|
|
2980
|
+
raise ValueError("Either (x, y) or layers must be provided")
|
|
2981
|
+
|
|
2982
|
+
# Initialize layer tracking
|
|
2983
|
+
self._layers = []
|
|
2984
|
+
|
|
2985
|
+
# If layers dict provided, convert to internal format
|
|
2986
|
+
if layers is not None:
|
|
2987
|
+
for label, props in layers.items():
|
|
2988
|
+
if len(props) != 2:
|
|
2989
|
+
raise ValueError(f"Layer '{label}' must have exactly 2 properties [x, y]")
|
|
2990
|
+
self._layers.append({
|
|
2991
|
+
'label': label,
|
|
2992
|
+
'x': props[0],
|
|
2993
|
+
'y': props[1]
|
|
2994
|
+
})
|
|
2995
|
+
# If x and y provided, create a default layer
|
|
2996
|
+
elif x is not None and y is not None:
|
|
2997
|
+
self._layers.append({
|
|
2998
|
+
'label': None,
|
|
2999
|
+
'x': x,
|
|
3000
|
+
'y': y
|
|
3001
|
+
})
|
|
3002
|
+
|
|
2934
3003
|
# Store parameters
|
|
2935
3004
|
self.x = x
|
|
2936
3005
|
self.y = y
|
|
@@ -2941,8 +3010,20 @@ class Crossplot:
|
|
|
2941
3010
|
self.color_range = color_range
|
|
2942
3011
|
self.size_range = size_range
|
|
2943
3012
|
self.title = title
|
|
2944
|
-
|
|
2945
|
-
|
|
3013
|
+
# Set axis labels - use provided labels, or property names, or generic labels for layers
|
|
3014
|
+
if xlabel:
|
|
3015
|
+
self.xlabel = xlabel
|
|
3016
|
+
elif x:
|
|
3017
|
+
self.xlabel = x
|
|
3018
|
+
else:
|
|
3019
|
+
self.xlabel = "X"
|
|
3020
|
+
|
|
3021
|
+
if ylabel:
|
|
3022
|
+
self.ylabel = ylabel
|
|
3023
|
+
elif y:
|
|
3024
|
+
self.ylabel = y
|
|
3025
|
+
else:
|
|
3026
|
+
self.ylabel = "Y"
|
|
2946
3027
|
self.figsize = figsize
|
|
2947
3028
|
self.dpi = dpi
|
|
2948
3029
|
self.marker = marker
|
|
@@ -2981,6 +3062,50 @@ class Crossplot:
|
|
|
2981
3062
|
# Data cache
|
|
2982
3063
|
self._data = None
|
|
2983
3064
|
|
|
3065
|
+
def add_layer(self, x: str, y: str, label: str):
|
|
3066
|
+
"""
|
|
3067
|
+
Add a new data layer to the crossplot.
|
|
3068
|
+
|
|
3069
|
+
This allows combining multiple property pairs in a single plot, useful for
|
|
3070
|
+
comparing different data types (e.g., Core vs Sidewall data).
|
|
3071
|
+
|
|
3072
|
+
Parameters
|
|
3073
|
+
----------
|
|
3074
|
+
x : str
|
|
3075
|
+
Name of property for x-axis for this layer
|
|
3076
|
+
y : str
|
|
3077
|
+
Name of property for y-axis for this layer
|
|
3078
|
+
label : str
|
|
3079
|
+
Label for this layer (used in legend and available as "label" property
|
|
3080
|
+
for color/shape mapping)
|
|
3081
|
+
|
|
3082
|
+
Returns
|
|
3083
|
+
-------
|
|
3084
|
+
self
|
|
3085
|
+
Returns self to allow method chaining
|
|
3086
|
+
|
|
3087
|
+
Examples
|
|
3088
|
+
--------
|
|
3089
|
+
>>> plot = manager.Crossplot(y_log=True)
|
|
3090
|
+
>>> plot.add_layer('CorePor_obds', 'CorePerm_obds', label='Core')
|
|
3091
|
+
>>> plot.add_layer('SidewallPor_ob', 'SidewallPerm_ob', label='Sidewall')
|
|
3092
|
+
>>> plot.show()
|
|
3093
|
+
|
|
3094
|
+
With method chaining:
|
|
3095
|
+
>>> manager.Crossplot(y_log=True) \\
|
|
3096
|
+
... .add_layer('CorePor_obds', 'CorePerm_obds', label='Core') \\
|
|
3097
|
+
... .add_layer('SidewallPor_ob', 'SidewallPerm_ob', label='Sidewall') \\
|
|
3098
|
+
... .show()
|
|
3099
|
+
"""
|
|
3100
|
+
self._layers.append({
|
|
3101
|
+
'label': label,
|
|
3102
|
+
'x': x,
|
|
3103
|
+
'y': y
|
|
3104
|
+
})
|
|
3105
|
+
# Clear data cache since we're adding new data
|
|
3106
|
+
self._data = None
|
|
3107
|
+
return self
|
|
3108
|
+
|
|
2984
3109
|
def _prepare_data(self) -> pd.DataFrame:
|
|
2985
3110
|
"""Prepare data from wells for plotting."""
|
|
2986
3111
|
if self._data is not None:
|
|
@@ -2988,86 +3113,104 @@ class Crossplot:
|
|
|
2988
3113
|
|
|
2989
3114
|
all_data = []
|
|
2990
3115
|
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
#
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3116
|
+
# Helper function to check if alignment is needed
|
|
3117
|
+
def needs_alignment(prop_depth, ref_depth):
|
|
3118
|
+
"""Quick check if depths need alignment."""
|
|
3119
|
+
if len(prop_depth) != len(ref_depth):
|
|
3120
|
+
return True
|
|
3121
|
+
# Fast check: if arrays are identical objects or first/last don't match
|
|
3122
|
+
if prop_depth is ref_depth:
|
|
3123
|
+
return False
|
|
3124
|
+
if prop_depth[0] != ref_depth[0] or prop_depth[-1] != ref_depth[-1]:
|
|
3125
|
+
return True
|
|
3126
|
+
# Only do expensive allclose if needed
|
|
3127
|
+
return not np.allclose(prop_depth, ref_depth)
|
|
3128
|
+
|
|
3129
|
+
# Loop through each layer
|
|
3130
|
+
for layer in self._layers:
|
|
3131
|
+
layer_x = layer['x']
|
|
3132
|
+
layer_y = layer['y']
|
|
3133
|
+
layer_label = layer['label']
|
|
3134
|
+
|
|
3135
|
+
for well in self.wells:
|
|
3136
|
+
try:
|
|
3137
|
+
# Get x and y properties for this layer
|
|
3138
|
+
x_prop = well.get_property(layer_x)
|
|
3139
|
+
y_prop = well.get_property(layer_y)
|
|
3140
|
+
|
|
3141
|
+
# Get depths - use x property's depth
|
|
3142
|
+
depths = x_prop.depth
|
|
3143
|
+
x_values = x_prop.values
|
|
3144
|
+
y_values = y_prop.values
|
|
3145
|
+
|
|
3146
|
+
# Align y values to x depth grid if needed
|
|
3147
|
+
if needs_alignment(y_prop.depth, depths):
|
|
3148
|
+
y_values = np.interp(depths, y_prop.depth, y_prop.values, left=np.nan, right=np.nan)
|
|
3149
|
+
|
|
3150
|
+
# Create dataframe for this well and layer
|
|
3151
|
+
df = pd.DataFrame({
|
|
3152
|
+
'depth': depths,
|
|
3153
|
+
'x': x_values,
|
|
3154
|
+
'y': y_values,
|
|
3155
|
+
'well': well.name,
|
|
3156
|
+
'label': layer_label # Add layer label
|
|
3157
|
+
})
|
|
3158
|
+
|
|
3159
|
+
# Add color property if specified
|
|
3160
|
+
if self.color == "label":
|
|
3161
|
+
# Use layer label for color
|
|
3162
|
+
df['color_val'] = layer_label
|
|
3163
|
+
elif self.color and self.color != "depth":
|
|
3164
|
+
try:
|
|
3165
|
+
color_prop = well.get_property(self.color)
|
|
3166
|
+
color_values = color_prop.values
|
|
3167
|
+
# Align to x depth grid
|
|
3168
|
+
if needs_alignment(color_prop.depth, depths):
|
|
3169
|
+
color_values = np.interp(depths, color_prop.depth, color_prop.values, left=np.nan, right=np.nan)
|
|
3170
|
+
df['color_val'] = color_values
|
|
3171
|
+
except (AttributeError, KeyError, PropertyNotFoundError):
|
|
3172
|
+
# Silently use depth as fallback
|
|
3173
|
+
df['color_val'] = depths
|
|
3174
|
+
elif self.color == "depth":
|
|
3038
3175
|
df['color_val'] = depths
|
|
3039
|
-
elif self.color == "depth":
|
|
3040
|
-
df['color_val'] = depths
|
|
3041
3176
|
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
size_values =
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3177
|
+
# Add size property if specified
|
|
3178
|
+
if self.size == "label":
|
|
3179
|
+
# Use layer label for size (will need special handling in plot)
|
|
3180
|
+
df['size_val'] = layer_label
|
|
3181
|
+
elif self.size:
|
|
3182
|
+
try:
|
|
3183
|
+
size_prop = well.get_property(self.size)
|
|
3184
|
+
size_values = size_prop.values
|
|
3185
|
+
# Align to x depth grid
|
|
3186
|
+
if needs_alignment(size_prop.depth, depths):
|
|
3187
|
+
size_values = np.interp(depths, size_prop.depth, size_prop.values, left=np.nan, right=np.nan)
|
|
3188
|
+
df['size_val'] = size_values
|
|
3189
|
+
except (AttributeError, KeyError, PropertyNotFoundError):
|
|
3190
|
+
# Silently skip if size property not found
|
|
3191
|
+
pass
|
|
3192
|
+
|
|
3193
|
+
# Add shape property if specified
|
|
3194
|
+
if self.shape == "label":
|
|
3195
|
+
# Use layer label for shape
|
|
3196
|
+
df['shape_val'] = layer_label
|
|
3197
|
+
elif self.shape and self.shape != "well":
|
|
3198
|
+
try:
|
|
3199
|
+
shape_prop = well.get_property(self.shape)
|
|
3200
|
+
shape_values = shape_prop.values
|
|
3201
|
+
# Align to x depth grid
|
|
3202
|
+
if needs_alignment(shape_prop.depth, depths):
|
|
3203
|
+
shape_values = np.interp(depths, shape_prop.depth, shape_prop.values, left=np.nan, right=np.nan)
|
|
3204
|
+
df['shape_val'] = shape_values
|
|
3205
|
+
except (AttributeError, KeyError, PropertyNotFoundError):
|
|
3206
|
+
# Silently skip if shape property not found
|
|
3207
|
+
pass
|
|
3208
|
+
|
|
3209
|
+
all_data.append(df)
|
|
3210
|
+
|
|
3211
|
+
except (AttributeError, KeyError, PropertyNotFoundError):
|
|
3212
|
+
# Silently skip wells that don't have the required properties
|
|
3213
|
+
continue
|
|
3071
3214
|
|
|
3072
3215
|
if not all_data:
|
|
3073
3216
|
raise ValueError("No valid data found in any wells")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.126 → well_log_toolkit-0.1.128}/well_log_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|