well-log-toolkit 0.1.129__tar.gz → 0.1.131__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.129 → well_log_toolkit-0.1.131}/PKG-INFO +1 -1
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/pyproject.toml +1 -1
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit/manager.py +9 -1
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit/visualization.py +184 -33
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit.egg-info/PKG-INFO +1 -1
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/README.md +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/setup.cfg +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit/__init__.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit/exceptions.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit/las_file.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit/operations.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit/property.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit/regression.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit/statistics.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit/utils.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit/well.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit.egg-info/requires.txt +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/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.131"
|
|
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"
|
|
@@ -2598,7 +2598,7 @@ class WellDataManager:
|
|
|
2598
2598
|
y: Optional[str] = None,
|
|
2599
2599
|
wells: Optional[list[str]] = None,
|
|
2600
2600
|
layers: Optional[dict[str, list[str]]] = None,
|
|
2601
|
-
shape: str =
|
|
2601
|
+
shape: Optional[str] = None,
|
|
2602
2602
|
color: Optional[str] = None,
|
|
2603
2603
|
size: Optional[str] = None,
|
|
2604
2604
|
colortemplate: str = "viridis",
|
|
@@ -2761,6 +2761,14 @@ class WellDataManager:
|
|
|
2761
2761
|
if not well_objects:
|
|
2762
2762
|
raise ValueError("No wells available for crossplot")
|
|
2763
2763
|
|
|
2764
|
+
# Set default shape: "well" when no layers, "label" when layers provided
|
|
2765
|
+
if shape is None and layers is None:
|
|
2766
|
+
shape = "well"
|
|
2767
|
+
|
|
2768
|
+
# Set default color: "well" when shape defaults to "label" (i.e., when layers provided)
|
|
2769
|
+
if color is None and layers is not None and shape is None:
|
|
2770
|
+
color = "well"
|
|
2771
|
+
|
|
2764
2772
|
return CrossplotClass(
|
|
2765
2773
|
wells=well_objects,
|
|
2766
2774
|
x=x,
|
|
@@ -12,9 +12,10 @@ import warnings
|
|
|
12
12
|
import numpy as np
|
|
13
13
|
import pandas as pd
|
|
14
14
|
import matplotlib.pyplot as plt
|
|
15
|
+
import matplotlib.cm as cm
|
|
15
16
|
from matplotlib.collections import PolyCollection
|
|
16
17
|
from matplotlib.colors import Normalize
|
|
17
|
-
from matplotlib.patches import Rectangle
|
|
18
|
+
from matplotlib.patches import Rectangle, Patch
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
21
|
from .well import Well
|
|
@@ -2776,16 +2777,16 @@ class Crossplot:
|
|
|
2776
2777
|
Dictionary mapping layer labels to [x_property, y_property] lists.
|
|
2777
2778
|
Use this to combine multiple property pairs in a single plot.
|
|
2778
2779
|
Example: {"Core": ["CorePor", "CorePerm"], "Sidewall": ["SWPor", "SWPerm"]}
|
|
2779
|
-
When using layers,
|
|
2780
|
-
|
|
2780
|
+
When using layers, shape defaults to "label" and color defaults to "well" for
|
|
2781
|
+
easy visualization of both layer types and wells. Default: None
|
|
2781
2782
|
shape : str, optional
|
|
2782
2783
|
Property name for shape mapping. Use "well" to map shapes by well name,
|
|
2783
2784
|
or "label" (when using layers) to map shapes by layer type.
|
|
2784
|
-
Default: None
|
|
2785
|
+
Default: "well" for multi-well plots, "label" when layers provided, None otherwise
|
|
2785
2786
|
color : str, optional
|
|
2786
2787
|
Property name for color mapping. Use "depth" to color by depth,
|
|
2787
|
-
or "label" (when using layers) to color by layer type.
|
|
2788
|
-
Default: None
|
|
2788
|
+
"well" to color by well, or "label" (when using layers) to color by layer type.
|
|
2789
|
+
Default: "well" when layers provided, None otherwise
|
|
2789
2790
|
size : str, optional
|
|
2790
2791
|
Property name for size mapping, or "label" (when using layers) to
|
|
2791
2792
|
size by layer type.
|
|
@@ -2901,8 +2902,9 @@ class Crossplot:
|
|
|
2901
2902
|
... "Core": ["CorePor_obds", "CorePerm_obds"],
|
|
2902
2903
|
... "Sidewall": ["SidewallPor_ob", "SidewallPerm_ob"]
|
|
2903
2904
|
... },
|
|
2904
|
-
... shape="label", # Different shapes for Core vs Sidewall
|
|
2905
2905
|
... y_log=True
|
|
2906
|
+
... # shape defaults to "label" - different shapes for Core vs Sidewall
|
|
2907
|
+
... # color defaults to "well" - different colors for each well
|
|
2906
2908
|
... )
|
|
2907
2909
|
>>> plot.show()
|
|
2908
2910
|
|
|
@@ -2912,6 +2914,7 @@ class Crossplot:
|
|
|
2912
2914
|
... .add_layer("CorePor_obds", "CorePerm_obds", label="Core") \\
|
|
2913
2915
|
... .add_layer("SidewallPor_ob", "SidewallPerm_ob", label="Sidewall") \\
|
|
2914
2916
|
... .show()
|
|
2917
|
+
... # Automatically uses shape="label" and color="well"
|
|
2915
2918
|
|
|
2916
2919
|
Layers with regression by color (single trend per well):
|
|
2917
2920
|
|
|
@@ -2920,9 +2923,8 @@ class Crossplot:
|
|
|
2920
2923
|
... "Core": ["CorePor_obds", "CorePerm_obds"],
|
|
2921
2924
|
... "Sidewall": ["SidewallPor_ob", "SidewallPerm_ob"]
|
|
2922
2925
|
... },
|
|
2923
|
-
... shape="label", # Different shapes for data types
|
|
2924
|
-
... color="well", # Color by well
|
|
2925
2926
|
... regression_by_color="linear" # One trend per well (combining both data types)
|
|
2927
|
+
... # Defaults: shape="label" (different shapes), color="well" (different colors)
|
|
2926
2928
|
... )
|
|
2927
2929
|
>>> plot.show()
|
|
2928
2930
|
|
|
@@ -3003,8 +3005,16 @@ class Crossplot:
|
|
|
3003
3005
|
# Store parameters
|
|
3004
3006
|
self.x = x
|
|
3005
3007
|
self.y = y
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
+
# Default shape to "label" when layers are provided
|
|
3009
|
+
if shape is None and layers is not None:
|
|
3010
|
+
self.shape = "label"
|
|
3011
|
+
else:
|
|
3012
|
+
self.shape = shape
|
|
3013
|
+
# Default color to "well" when layers are provided (for multi-well visualization)
|
|
3014
|
+
if color is None and layers is not None and len(self.wells) > 1:
|
|
3015
|
+
self.color = "well"
|
|
3016
|
+
else:
|
|
3017
|
+
self.color = color
|
|
3008
3018
|
self.size = size
|
|
3009
3019
|
self.colortemplate = colortemplate
|
|
3010
3020
|
self.color_range = color_range
|
|
@@ -3160,6 +3170,9 @@ class Crossplot:
|
|
|
3160
3170
|
if self.color == "label":
|
|
3161
3171
|
# Use layer label for color
|
|
3162
3172
|
df['color_val'] = layer_label
|
|
3173
|
+
elif self.color == "well":
|
|
3174
|
+
# Use well name for color (categorical)
|
|
3175
|
+
df['color_val'] = well.name
|
|
3163
3176
|
elif self.color and self.color != "depth":
|
|
3164
3177
|
try:
|
|
3165
3178
|
color_prop = well.get_property(self.color)
|
|
@@ -3679,6 +3692,36 @@ class Crossplot:
|
|
|
3679
3692
|
if update_legend and self.ax is not None:
|
|
3680
3693
|
self._update_regression_legend()
|
|
3681
3694
|
|
|
3695
|
+
def _is_categorical_color(self, color_values: np.ndarray) -> bool:
|
|
3696
|
+
"""
|
|
3697
|
+
Determine if color values should be treated as categorical vs continuous.
|
|
3698
|
+
|
|
3699
|
+
Returns True if:
|
|
3700
|
+
- Less than 50 unique values
|
|
3701
|
+
|
|
3702
|
+
This helps distinguish between:
|
|
3703
|
+
- Categorical: well names, facies, zones, labels
|
|
3704
|
+
- Continuous: depth, porosity, saturation
|
|
3705
|
+
"""
|
|
3706
|
+
# Remove NaN values for analysis
|
|
3707
|
+
valid_values = color_values[~pd.isna(color_values)]
|
|
3708
|
+
|
|
3709
|
+
if len(valid_values) == 0:
|
|
3710
|
+
return False
|
|
3711
|
+
|
|
3712
|
+
unique_values = np.unique(valid_values)
|
|
3713
|
+
n_unique = len(unique_values)
|
|
3714
|
+
|
|
3715
|
+
# Check if values are numeric - if not, it's categorical
|
|
3716
|
+
try:
|
|
3717
|
+
# Try to convert to float - if this fails, it's categorical (strings)
|
|
3718
|
+
_ = unique_values.astype(float)
|
|
3719
|
+
except (ValueError, TypeError):
|
|
3720
|
+
return True
|
|
3721
|
+
|
|
3722
|
+
# Apply the criteria
|
|
3723
|
+
return n_unique < 50
|
|
3724
|
+
|
|
3682
3725
|
def plot(self) -> 'Crossplot':
|
|
3683
3726
|
"""Generate the crossplot figure."""
|
|
3684
3727
|
# Prepare data
|
|
@@ -3778,13 +3821,41 @@ class Crossplot:
|
|
|
3778
3821
|
y_vals = data['y'].values
|
|
3779
3822
|
|
|
3780
3823
|
# Determine colors
|
|
3824
|
+
is_categorical = False
|
|
3781
3825
|
if self.color:
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
if
|
|
3785
|
-
|
|
3826
|
+
c_vals_raw = data['color_val'].values
|
|
3827
|
+
|
|
3828
|
+
# Check if color data is categorical
|
|
3829
|
+
is_categorical = self._is_categorical_color(c_vals_raw)
|
|
3830
|
+
|
|
3831
|
+
if is_categorical:
|
|
3832
|
+
# Handle categorical colors with discrete palette
|
|
3833
|
+
unique_categories = pd.Series(c_vals_raw).dropna().unique()
|
|
3834
|
+
n_categories = len(unique_categories)
|
|
3835
|
+
|
|
3836
|
+
# Create color map for categories
|
|
3837
|
+
if n_categories <= len(DEFAULT_COLORS):
|
|
3838
|
+
color_palette = DEFAULT_COLORS
|
|
3839
|
+
else:
|
|
3840
|
+
# Use colormap for many categories
|
|
3841
|
+
cmap_obj = cm.get_cmap(self.colortemplate, n_categories)
|
|
3842
|
+
color_palette = [cmap_obj(i) for i in range(n_categories)]
|
|
3843
|
+
|
|
3844
|
+
category_colors = {cat: color_palette[i % len(color_palette)]
|
|
3845
|
+
for i, cat in enumerate(unique_categories)}
|
|
3846
|
+
|
|
3847
|
+
# Map each value to its color
|
|
3848
|
+
c_vals = [category_colors.get(val, DEFAULT_COLORS[0]) for val in c_vals_raw]
|
|
3849
|
+
cmap = None
|
|
3850
|
+
vmin = vmax = None
|
|
3786
3851
|
else:
|
|
3787
|
-
|
|
3852
|
+
# Handle continuous colors
|
|
3853
|
+
c_vals = c_vals_raw
|
|
3854
|
+
cmap = self.colortemplate
|
|
3855
|
+
if self.color_range:
|
|
3856
|
+
vmin, vmax = self.color_range
|
|
3857
|
+
else:
|
|
3858
|
+
vmin, vmax = np.nanmin(c_vals), np.nanmax(c_vals)
|
|
3788
3859
|
else:
|
|
3789
3860
|
c_vals = DEFAULT_COLORS[0]
|
|
3790
3861
|
cmap = None
|
|
@@ -3817,11 +3888,33 @@ class Crossplot:
|
|
|
3817
3888
|
marker=self.marker
|
|
3818
3889
|
)
|
|
3819
3890
|
|
|
3820
|
-
# Add colorbar
|
|
3821
|
-
if self.color
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3891
|
+
# Add colorbar or legend based on color type
|
|
3892
|
+
if self.color:
|
|
3893
|
+
if is_categorical and self.show_legend:
|
|
3894
|
+
# Create legend for categorical colors
|
|
3895
|
+
c_vals_raw = data['color_val'].values
|
|
3896
|
+
unique_categories = pd.Series(c_vals_raw).dropna().unique()
|
|
3897
|
+
|
|
3898
|
+
# Create custom legend handles
|
|
3899
|
+
legend_elements = [Patch(facecolor=category_colors[cat],
|
|
3900
|
+
edgecolor=self.edge_color,
|
|
3901
|
+
label=str(cat))
|
|
3902
|
+
for cat in unique_categories]
|
|
3903
|
+
|
|
3904
|
+
colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
|
|
3905
|
+
legend = self.ax.legend(handles=legend_elements,
|
|
3906
|
+
title=colorbar_label,
|
|
3907
|
+
loc='best',
|
|
3908
|
+
frameon=True,
|
|
3909
|
+
framealpha=0.9,
|
|
3910
|
+
edgecolor='black')
|
|
3911
|
+
legend.get_title().set_fontweight('bold')
|
|
3912
|
+
self.ax.add_artist(legend)
|
|
3913
|
+
elif not is_categorical and self.show_colorbar:
|
|
3914
|
+
# Add colorbar for continuous colors
|
|
3915
|
+
self.colorbar = self.fig.colorbar(self.scatter, ax=self.ax)
|
|
3916
|
+
colorbar_label = self.color if self.color != "depth" else "Depth"
|
|
3917
|
+
self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
|
|
3825
3918
|
|
|
3826
3919
|
def _plot_by_groups(self, data: pd.DataFrame) -> None:
|
|
3827
3920
|
"""Plot data grouped by shape/well."""
|
|
@@ -3836,6 +3929,27 @@ class Crossplot:
|
|
|
3836
3929
|
# Define markers for different groups
|
|
3837
3930
|
markers = ['o', 's', '^', 'D', 'v', '<', '>', 'p', '*', 'h']
|
|
3838
3931
|
|
|
3932
|
+
# Check if colors are categorical (check once for all data)
|
|
3933
|
+
is_categorical = False
|
|
3934
|
+
category_colors = {}
|
|
3935
|
+
if self.color:
|
|
3936
|
+
c_vals_all = data['color_val'].values
|
|
3937
|
+
is_categorical = self._is_categorical_color(c_vals_all)
|
|
3938
|
+
|
|
3939
|
+
if is_categorical:
|
|
3940
|
+
# Prepare color mapping for categorical values
|
|
3941
|
+
unique_categories = pd.Series(c_vals_all).dropna().unique()
|
|
3942
|
+
n_categories = len(unique_categories)
|
|
3943
|
+
|
|
3944
|
+
if n_categories <= len(DEFAULT_COLORS):
|
|
3945
|
+
color_palette = DEFAULT_COLORS
|
|
3946
|
+
else:
|
|
3947
|
+
cmap_obj = cm.get_cmap(self.colortemplate, n_categories)
|
|
3948
|
+
color_palette = [cmap_obj(i) for i in range(n_categories)]
|
|
3949
|
+
|
|
3950
|
+
category_colors = {cat: color_palette[i % len(color_palette)]
|
|
3951
|
+
for i, cat in enumerate(unique_categories)}
|
|
3952
|
+
|
|
3839
3953
|
# Track for colorbar (use first scatter)
|
|
3840
3954
|
first_scatter = None
|
|
3841
3955
|
|
|
@@ -3848,13 +3962,22 @@ class Crossplot:
|
|
|
3848
3962
|
|
|
3849
3963
|
# Determine colors
|
|
3850
3964
|
if self.color:
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
if
|
|
3854
|
-
|
|
3965
|
+
c_vals_raw = group_data['color_val'].values
|
|
3966
|
+
|
|
3967
|
+
if is_categorical:
|
|
3968
|
+
# Map categorical values to colors
|
|
3969
|
+
c_vals = [category_colors.get(val, DEFAULT_COLORS[0]) for val in c_vals_raw]
|
|
3970
|
+
cmap = None
|
|
3971
|
+
vmin = vmax = None
|
|
3855
3972
|
else:
|
|
3856
|
-
# Use
|
|
3857
|
-
|
|
3973
|
+
# Use continuous color mapping
|
|
3974
|
+
c_vals = c_vals_raw
|
|
3975
|
+
cmap = self.colortemplate
|
|
3976
|
+
if self.color_range:
|
|
3977
|
+
vmin, vmax = self.color_range
|
|
3978
|
+
else:
|
|
3979
|
+
# Use global range from all data
|
|
3980
|
+
vmin, vmax = np.nanmin(data['color_val']), np.nanmax(data['color_val'])
|
|
3858
3981
|
else:
|
|
3859
3982
|
c_vals = DEFAULT_COLORS[idx % len(DEFAULT_COLORS)]
|
|
3860
3983
|
cmap = None
|
|
@@ -3888,7 +4011,7 @@ class Crossplot:
|
|
|
3888
4011
|
label=str(group_name)
|
|
3889
4012
|
)
|
|
3890
4013
|
|
|
3891
|
-
if first_scatter is None and self.color:
|
|
4014
|
+
if first_scatter is None and self.color and not is_categorical:
|
|
3892
4015
|
first_scatter = scatter
|
|
3893
4016
|
|
|
3894
4017
|
# Add legend with smart placement
|
|
@@ -3911,11 +4034,39 @@ class Crossplot:
|
|
|
3911
4034
|
# Store the primary legend so it persists when regression legend is added
|
|
3912
4035
|
self.ax.add_artist(legend)
|
|
3913
4036
|
|
|
3914
|
-
# Add colorbar
|
|
3915
|
-
if self.color
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
4037
|
+
# Add colorbar or color legend based on color type
|
|
4038
|
+
if self.color:
|
|
4039
|
+
if is_categorical and self.show_legend:
|
|
4040
|
+
# Create separate legend for categorical colors
|
|
4041
|
+
c_vals_all = data['color_val'].values
|
|
4042
|
+
unique_categories = pd.Series(c_vals_all).dropna().unique()
|
|
4043
|
+
|
|
4044
|
+
# Create custom legend handles
|
|
4045
|
+
legend_elements = [Patch(facecolor=category_colors[cat],
|
|
4046
|
+
edgecolor=self.edge_color,
|
|
4047
|
+
label=str(cat))
|
|
4048
|
+
for cat in unique_categories]
|
|
4049
|
+
|
|
4050
|
+
colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
|
|
4051
|
+
|
|
4052
|
+
# Find a good location for the color legend (opposite corner from shape legend)
|
|
4053
|
+
if self._data is not None:
|
|
4054
|
+
_, secondary_loc = self._find_best_legend_locations(self._data)
|
|
4055
|
+
else:
|
|
4056
|
+
secondary_loc = 'upper right'
|
|
4057
|
+
|
|
4058
|
+
color_legend = self.ax.legend(handles=legend_elements,
|
|
4059
|
+
title=colorbar_label,
|
|
4060
|
+
loc=secondary_loc,
|
|
4061
|
+
frameon=True,
|
|
4062
|
+
framealpha=0.9,
|
|
4063
|
+
edgecolor='black')
|
|
4064
|
+
color_legend.get_title().set_fontweight('bold')
|
|
4065
|
+
elif not is_categorical and self.show_colorbar and first_scatter:
|
|
4066
|
+
# Add continuous colorbar
|
|
4067
|
+
self.colorbar = self.fig.colorbar(first_scatter, ax=self.ax)
|
|
4068
|
+
colorbar_label = self.color if self.color != "depth" else "Depth"
|
|
4069
|
+
self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
|
|
3919
4070
|
|
|
3920
4071
|
def add_regression(
|
|
3921
4072
|
self,
|
|
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.129 → well_log_toolkit-0.1.131}/well_log_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.129 → well_log_toolkit-0.1.131}/well_log_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|