well-log-toolkit 0.1.126__py3-none-any.whl → 0.1.128__py3-none-any.whl

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.
@@ -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, and regression analysis.
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
- self.xlabel = xlabel if xlabel else x
2945
- self.ylabel = ylabel if ylabel else y
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
- for well in self.wells:
2992
- try:
2993
- # Get x and y properties
2994
- x_prop = well.get_property(self.x)
2995
- y_prop = well.get_property(self.y)
2996
-
2997
- # Get depths - use x property's depth
2998
- depths = x_prop.depth
2999
- x_values = x_prop.values
3000
- y_values = y_prop.values
3001
-
3002
- # Helper function to check if alignment is needed
3003
- def needs_alignment(prop_depth, ref_depth):
3004
- """Quick check if depths need alignment."""
3005
- if len(prop_depth) != len(ref_depth):
3006
- return True
3007
- # Fast check: if arrays are identical objects or first/last don't match
3008
- if prop_depth is ref_depth:
3009
- return False
3010
- if prop_depth[0] != ref_depth[0] or prop_depth[-1] != ref_depth[-1]:
3011
- return True
3012
- # Only do expensive allclose if needed
3013
- return not np.allclose(prop_depth, ref_depth)
3014
-
3015
- # Align y values to x depth grid if needed
3016
- if needs_alignment(y_prop.depth, depths):
3017
- y_values = np.interp(depths, y_prop.depth, y_prop.values, left=np.nan, right=np.nan)
3018
-
3019
- # Create dataframe for this well
3020
- df = pd.DataFrame({
3021
- 'depth': depths,
3022
- 'x': x_values,
3023
- 'y': y_values,
3024
- 'well': well.name
3025
- })
3026
-
3027
- # Add color property if specified
3028
- if self.color and self.color != "depth":
3029
- try:
3030
- color_prop = well.get_property(self.color)
3031
- color_values = color_prop.values
3032
- # Align to x depth grid
3033
- if needs_alignment(color_prop.depth, depths):
3034
- color_values = np.interp(depths, color_prop.depth, color_prop.values, left=np.nan, right=np.nan)
3035
- df['color_val'] = color_values
3036
- except (AttributeError, KeyError):
3037
- warnings.warn(f"Color property '{self.color}' not found in well '{well.name}', using depth")
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
- # Add size property if specified
3043
- if self.size:
3044
- try:
3045
- size_prop = well.get_property(self.size)
3046
- size_values = size_prop.values
3047
- # Align to x depth grid
3048
- if needs_alignment(size_prop.depth, depths):
3049
- size_values = np.interp(depths, size_prop.depth, size_prop.values, left=np.nan, right=np.nan)
3050
- df['size_val'] = size_values
3051
- except (AttributeError, KeyError):
3052
- warnings.warn(f"Size property '{self.size}' not found in well '{well.name}'")
3053
-
3054
- # Add shape property if specified and not "well"
3055
- if self.shape and self.shape != "well":
3056
- try:
3057
- shape_prop = well.get_property(self.shape)
3058
- shape_values = shape_prop.values
3059
- # Align to x depth grid
3060
- if needs_alignment(shape_prop.depth, depths):
3061
- shape_values = np.interp(depths, shape_prop.depth, shape_prop.values, left=np.nan, right=np.nan)
3062
- df['shape_val'] = shape_values
3063
- except (AttributeError, KeyError):
3064
- warnings.warn(f"Shape property '{self.shape}' not found in well '{well.name}'")
3065
-
3066
- all_data.append(df)
3067
-
3068
- except (AttributeError, KeyError) as e:
3069
- warnings.warn(f"Could not get properties for well '{well.name}': {e}")
3070
- continue
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.126
3
+ Version: 0.1.128
4
4
  Summary: Fast LAS file processing with lazy loading and filtering for well log analysis
5
5
  Author-email: Kristian dF Kollsgård <kkollsg@gmail.com>
6
6
  License: MIT
@@ -7,9 +7,9 @@ well_log_toolkit/property.py,sha256=WOzoNQcmHCQ8moIKsnSyLgVC8s4LBu2x5IBXtFzmMe8,
7
7
  well_log_toolkit/regression.py,sha256=7D3oI-1XVlFb-mOoHTxTTtUHERFyvQSBAzJzAGVoZnk,25192
8
8
  well_log_toolkit/statistics.py,sha256=_huPMbv2H3o9ezunjEM94mJknX5wPK8V4nDv2lIZZRw,16814
9
9
  well_log_toolkit/utils.py,sha256=O2KPq4htIoUlL74V2zKftdqqTjRfezU9M-568zPLme0,6866
10
- well_log_toolkit/visualization.py,sha256=xb870FG5FghU2gEkqdn1b2NbWNu07oDmFDN1Cx1HIi0,157280
10
+ well_log_toolkit/visualization.py,sha256=oyjENj3milpJ7O0Wc0Kraucu8HmklHbw--s7Z4_r-Ig,162865
11
11
  well_log_toolkit/well.py,sha256=7RzbC7zud5M53zZ8FmuQP0GPhUP5Y6RiFjTuf4_oMWE,104419
12
- well_log_toolkit-0.1.126.dist-info/METADATA,sha256=gI9Y9FEF-4TcwgLTybyf67Su2pfqg-texJfiJEvpKAM,59810
13
- well_log_toolkit-0.1.126.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- well_log_toolkit-0.1.126.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.126.dist-info/RECORD,,
12
+ well_log_toolkit-0.1.128.dist-info/METADATA,sha256=vz6aD0ot3BPiUj48YukYx8McWFesQ77DGIsOhO-TZVg,59810
13
+ well_log_toolkit-0.1.128.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ well_log_toolkit-0.1.128.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.128.dist-info/RECORD,,