well-log-toolkit 0.1.129__tar.gz → 0.1.130__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.130}/PKG-INFO +1 -1
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/pyproject.toml +1 -1
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit/manager.py +9 -1
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit/visualization.py +176 -32
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit.egg-info/PKG-INFO +1 -1
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/README.md +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/setup.cfg +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit/__init__.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit/exceptions.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit/las_file.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit/operations.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit/property.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit/regression.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit/statistics.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit/utils.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit/well.py +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit.egg-info/requires.txt +0 -0
- {well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/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.130"
|
|
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,7 +3005,11 @@ class Crossplot:
|
|
|
3003
3005
|
# Store parameters
|
|
3004
3006
|
self.x = x
|
|
3005
3007
|
self.y = y
|
|
3006
|
-
|
|
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
|
|
3007
3013
|
self.color = color
|
|
3008
3014
|
self.size = size
|
|
3009
3015
|
self.colortemplate = colortemplate
|
|
@@ -3679,6 +3685,36 @@ class Crossplot:
|
|
|
3679
3685
|
if update_legend and self.ax is not None:
|
|
3680
3686
|
self._update_regression_legend()
|
|
3681
3687
|
|
|
3688
|
+
def _is_categorical_color(self, color_values: np.ndarray) -> bool:
|
|
3689
|
+
"""
|
|
3690
|
+
Determine if color values should be treated as categorical vs continuous.
|
|
3691
|
+
|
|
3692
|
+
Returns True if:
|
|
3693
|
+
- Less than 50 unique values
|
|
3694
|
+
|
|
3695
|
+
This helps distinguish between:
|
|
3696
|
+
- Categorical: well names, facies, zones, labels
|
|
3697
|
+
- Continuous: depth, porosity, saturation
|
|
3698
|
+
"""
|
|
3699
|
+
# Remove NaN values for analysis
|
|
3700
|
+
valid_values = color_values[~pd.isna(color_values)]
|
|
3701
|
+
|
|
3702
|
+
if len(valid_values) == 0:
|
|
3703
|
+
return False
|
|
3704
|
+
|
|
3705
|
+
unique_values = np.unique(valid_values)
|
|
3706
|
+
n_unique = len(unique_values)
|
|
3707
|
+
|
|
3708
|
+
# Check if values are numeric - if not, it's categorical
|
|
3709
|
+
try:
|
|
3710
|
+
# Try to convert to float - if this fails, it's categorical (strings)
|
|
3711
|
+
_ = unique_values.astype(float)
|
|
3712
|
+
except (ValueError, TypeError):
|
|
3713
|
+
return True
|
|
3714
|
+
|
|
3715
|
+
# Apply the criteria
|
|
3716
|
+
return n_unique < 50
|
|
3717
|
+
|
|
3682
3718
|
def plot(self) -> 'Crossplot':
|
|
3683
3719
|
"""Generate the crossplot figure."""
|
|
3684
3720
|
# Prepare data
|
|
@@ -3778,13 +3814,41 @@ class Crossplot:
|
|
|
3778
3814
|
y_vals = data['y'].values
|
|
3779
3815
|
|
|
3780
3816
|
# Determine colors
|
|
3817
|
+
is_categorical = False
|
|
3781
3818
|
if self.color:
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
if
|
|
3785
|
-
|
|
3819
|
+
c_vals_raw = data['color_val'].values
|
|
3820
|
+
|
|
3821
|
+
# Check if color data is categorical
|
|
3822
|
+
is_categorical = self._is_categorical_color(c_vals_raw)
|
|
3823
|
+
|
|
3824
|
+
if is_categorical:
|
|
3825
|
+
# Handle categorical colors with discrete palette
|
|
3826
|
+
unique_categories = pd.Series(c_vals_raw).dropna().unique()
|
|
3827
|
+
n_categories = len(unique_categories)
|
|
3828
|
+
|
|
3829
|
+
# Create color map for categories
|
|
3830
|
+
if n_categories <= len(DEFAULT_COLORS):
|
|
3831
|
+
color_palette = DEFAULT_COLORS
|
|
3832
|
+
else:
|
|
3833
|
+
# Use colormap for many categories
|
|
3834
|
+
cmap_obj = cm.get_cmap(self.colortemplate, n_categories)
|
|
3835
|
+
color_palette = [cmap_obj(i) for i in range(n_categories)]
|
|
3836
|
+
|
|
3837
|
+
category_colors = {cat: color_palette[i % len(color_palette)]
|
|
3838
|
+
for i, cat in enumerate(unique_categories)}
|
|
3839
|
+
|
|
3840
|
+
# Map each value to its color
|
|
3841
|
+
c_vals = [category_colors.get(val, DEFAULT_COLORS[0]) for val in c_vals_raw]
|
|
3842
|
+
cmap = None
|
|
3843
|
+
vmin = vmax = None
|
|
3786
3844
|
else:
|
|
3787
|
-
|
|
3845
|
+
# Handle continuous colors
|
|
3846
|
+
c_vals = c_vals_raw
|
|
3847
|
+
cmap = self.colortemplate
|
|
3848
|
+
if self.color_range:
|
|
3849
|
+
vmin, vmax = self.color_range
|
|
3850
|
+
else:
|
|
3851
|
+
vmin, vmax = np.nanmin(c_vals), np.nanmax(c_vals)
|
|
3788
3852
|
else:
|
|
3789
3853
|
c_vals = DEFAULT_COLORS[0]
|
|
3790
3854
|
cmap = None
|
|
@@ -3817,11 +3881,33 @@ class Crossplot:
|
|
|
3817
3881
|
marker=self.marker
|
|
3818
3882
|
)
|
|
3819
3883
|
|
|
3820
|
-
# Add colorbar
|
|
3821
|
-
if self.color
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3884
|
+
# Add colorbar or legend based on color type
|
|
3885
|
+
if self.color:
|
|
3886
|
+
if is_categorical and self.show_legend:
|
|
3887
|
+
# Create legend for categorical colors
|
|
3888
|
+
c_vals_raw = data['color_val'].values
|
|
3889
|
+
unique_categories = pd.Series(c_vals_raw).dropna().unique()
|
|
3890
|
+
|
|
3891
|
+
# Create custom legend handles
|
|
3892
|
+
legend_elements = [Patch(facecolor=category_colors[cat],
|
|
3893
|
+
edgecolor=self.edge_color,
|
|
3894
|
+
label=str(cat))
|
|
3895
|
+
for cat in unique_categories]
|
|
3896
|
+
|
|
3897
|
+
colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
|
|
3898
|
+
legend = self.ax.legend(handles=legend_elements,
|
|
3899
|
+
title=colorbar_label,
|
|
3900
|
+
loc='best',
|
|
3901
|
+
frameon=True,
|
|
3902
|
+
framealpha=0.9,
|
|
3903
|
+
edgecolor='black')
|
|
3904
|
+
legend.get_title().set_fontweight('bold')
|
|
3905
|
+
self.ax.add_artist(legend)
|
|
3906
|
+
elif not is_categorical and self.show_colorbar:
|
|
3907
|
+
# Add colorbar for continuous colors
|
|
3908
|
+
self.colorbar = self.fig.colorbar(self.scatter, ax=self.ax)
|
|
3909
|
+
colorbar_label = self.color if self.color != "depth" else "Depth"
|
|
3910
|
+
self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
|
|
3825
3911
|
|
|
3826
3912
|
def _plot_by_groups(self, data: pd.DataFrame) -> None:
|
|
3827
3913
|
"""Plot data grouped by shape/well."""
|
|
@@ -3836,6 +3922,27 @@ class Crossplot:
|
|
|
3836
3922
|
# Define markers for different groups
|
|
3837
3923
|
markers = ['o', 's', '^', 'D', 'v', '<', '>', 'p', '*', 'h']
|
|
3838
3924
|
|
|
3925
|
+
# Check if colors are categorical (check once for all data)
|
|
3926
|
+
is_categorical = False
|
|
3927
|
+
category_colors = {}
|
|
3928
|
+
if self.color:
|
|
3929
|
+
c_vals_all = data['color_val'].values
|
|
3930
|
+
is_categorical = self._is_categorical_color(c_vals_all)
|
|
3931
|
+
|
|
3932
|
+
if is_categorical:
|
|
3933
|
+
# Prepare color mapping for categorical values
|
|
3934
|
+
unique_categories = pd.Series(c_vals_all).dropna().unique()
|
|
3935
|
+
n_categories = len(unique_categories)
|
|
3936
|
+
|
|
3937
|
+
if n_categories <= len(DEFAULT_COLORS):
|
|
3938
|
+
color_palette = DEFAULT_COLORS
|
|
3939
|
+
else:
|
|
3940
|
+
cmap_obj = cm.get_cmap(self.colortemplate, n_categories)
|
|
3941
|
+
color_palette = [cmap_obj(i) for i in range(n_categories)]
|
|
3942
|
+
|
|
3943
|
+
category_colors = {cat: color_palette[i % len(color_palette)]
|
|
3944
|
+
for i, cat in enumerate(unique_categories)}
|
|
3945
|
+
|
|
3839
3946
|
# Track for colorbar (use first scatter)
|
|
3840
3947
|
first_scatter = None
|
|
3841
3948
|
|
|
@@ -3848,13 +3955,22 @@ class Crossplot:
|
|
|
3848
3955
|
|
|
3849
3956
|
# Determine colors
|
|
3850
3957
|
if self.color:
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
if
|
|
3854
|
-
|
|
3958
|
+
c_vals_raw = group_data['color_val'].values
|
|
3959
|
+
|
|
3960
|
+
if is_categorical:
|
|
3961
|
+
# Map categorical values to colors
|
|
3962
|
+
c_vals = [category_colors.get(val, DEFAULT_COLORS[0]) for val in c_vals_raw]
|
|
3963
|
+
cmap = None
|
|
3964
|
+
vmin = vmax = None
|
|
3855
3965
|
else:
|
|
3856
|
-
# Use
|
|
3857
|
-
|
|
3966
|
+
# Use continuous color mapping
|
|
3967
|
+
c_vals = c_vals_raw
|
|
3968
|
+
cmap = self.colortemplate
|
|
3969
|
+
if self.color_range:
|
|
3970
|
+
vmin, vmax = self.color_range
|
|
3971
|
+
else:
|
|
3972
|
+
# Use global range from all data
|
|
3973
|
+
vmin, vmax = np.nanmin(data['color_val']), np.nanmax(data['color_val'])
|
|
3858
3974
|
else:
|
|
3859
3975
|
c_vals = DEFAULT_COLORS[idx % len(DEFAULT_COLORS)]
|
|
3860
3976
|
cmap = None
|
|
@@ -3888,7 +4004,7 @@ class Crossplot:
|
|
|
3888
4004
|
label=str(group_name)
|
|
3889
4005
|
)
|
|
3890
4006
|
|
|
3891
|
-
if first_scatter is None and self.color:
|
|
4007
|
+
if first_scatter is None and self.color and not is_categorical:
|
|
3892
4008
|
first_scatter = scatter
|
|
3893
4009
|
|
|
3894
4010
|
# Add legend with smart placement
|
|
@@ -3911,11 +4027,39 @@ class Crossplot:
|
|
|
3911
4027
|
# Store the primary legend so it persists when regression legend is added
|
|
3912
4028
|
self.ax.add_artist(legend)
|
|
3913
4029
|
|
|
3914
|
-
# Add colorbar
|
|
3915
|
-
if self.color
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
4030
|
+
# Add colorbar or color legend based on color type
|
|
4031
|
+
if self.color:
|
|
4032
|
+
if is_categorical and self.show_legend:
|
|
4033
|
+
# Create separate legend for categorical colors
|
|
4034
|
+
c_vals_all = data['color_val'].values
|
|
4035
|
+
unique_categories = pd.Series(c_vals_all).dropna().unique()
|
|
4036
|
+
|
|
4037
|
+
# Create custom legend handles
|
|
4038
|
+
legend_elements = [Patch(facecolor=category_colors[cat],
|
|
4039
|
+
edgecolor=self.edge_color,
|
|
4040
|
+
label=str(cat))
|
|
4041
|
+
for cat in unique_categories]
|
|
4042
|
+
|
|
4043
|
+
colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
|
|
4044
|
+
|
|
4045
|
+
# Find a good location for the color legend (opposite corner from shape legend)
|
|
4046
|
+
if self._data is not None:
|
|
4047
|
+
_, secondary_loc = self._find_best_legend_locations(self._data)
|
|
4048
|
+
else:
|
|
4049
|
+
secondary_loc = 'upper right'
|
|
4050
|
+
|
|
4051
|
+
color_legend = self.ax.legend(handles=legend_elements,
|
|
4052
|
+
title=colorbar_label,
|
|
4053
|
+
loc=secondary_loc,
|
|
4054
|
+
frameon=True,
|
|
4055
|
+
framealpha=0.9,
|
|
4056
|
+
edgecolor='black')
|
|
4057
|
+
color_legend.get_title().set_fontweight('bold')
|
|
4058
|
+
elif not is_categorical and self.show_colorbar and first_scatter:
|
|
4059
|
+
# Add continuous colorbar
|
|
4060
|
+
self.colorbar = self.fig.colorbar(first_scatter, ax=self.ax)
|
|
4061
|
+
colorbar_label = self.color if self.color != "depth" else "Depth"
|
|
4062
|
+
self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
|
|
3919
4063
|
|
|
3920
4064
|
def add_regression(
|
|
3921
4065
|
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.130}/well_log_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.129 → well_log_toolkit-0.1.130}/well_log_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|