well-log-toolkit 0.1.127__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.
@@ -2761,24 +2761,34 @@ class Crossplot:
2761
2761
  Create beautiful, modern crossplots for well log analysis.
2762
2762
 
2763
2763
  Supports single and multi-well crossplots with extensive customization options
2764
- 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).
2765
2766
 
2766
2767
  Parameters
2767
2768
  ----------
2768
2769
  wells : Well or list of Well
2769
2770
  Single well or list of wells to plot
2770
- x : str
2771
- Name of property for x-axis
2772
- y : str
2773
- 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
2774
2781
  shape : str, optional
2775
- 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.
2776
2784
  Default: None (single shape)
2777
2785
  color : str, optional
2778
- 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.
2779
2788
  Default: None (single color)
2780
2789
  size : str, optional
2781
- Property name for size mapping.
2790
+ Property name for size mapping, or "label" (when using layers) to
2791
+ size by layer type.
2782
2792
  Default: None (constant size)
2783
2793
  colortemplate : str, optional
2784
2794
  Matplotlib colormap name (e.g., "viridis", "plasma", "coolwarm")
@@ -2884,6 +2894,38 @@ class Crossplot:
2884
2894
  ... )
2885
2895
  >>> plot.show()
2886
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
+
2887
2929
  Access regression objects:
2888
2930
 
2889
2931
  >>> linear_regs = plot.regression("linear")
@@ -2894,8 +2936,9 @@ class Crossplot:
2894
2936
  def __init__(
2895
2937
  self,
2896
2938
  wells: Union['Well', list['Well']],
2897
- x: str,
2898
- y: str,
2939
+ x: Optional[str] = None,
2940
+ y: Optional[str] = None,
2941
+ layers: Optional[dict[str, list[str]]] = None,
2899
2942
  shape: Optional[str] = None,
2900
2943
  color: Optional[str] = None,
2901
2944
  size: Optional[str] = None,
@@ -2932,6 +2975,31 @@ class Crossplot:
2932
2975
  else:
2933
2976
  self.wells = wells
2934
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
+
2935
3003
  # Store parameters
2936
3004
  self.x = x
2937
3005
  self.y = y
@@ -2942,8 +3010,20 @@ class Crossplot:
2942
3010
  self.color_range = color_range
2943
3011
  self.size_range = size_range
2944
3012
  self.title = title
2945
- self.xlabel = xlabel if xlabel else x
2946
- 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"
2947
3027
  self.figsize = figsize
2948
3028
  self.dpi = dpi
2949
3029
  self.marker = marker
@@ -2982,6 +3062,50 @@ class Crossplot:
2982
3062
  # Data cache
2983
3063
  self._data = None
2984
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
+
2985
3109
  def _prepare_data(self) -> pd.DataFrame:
2986
3110
  """Prepare data from wells for plotting."""
2987
3111
  if self._data is not None:
@@ -2989,86 +3113,104 @@ class Crossplot:
2989
3113
 
2990
3114
  all_data = []
2991
3115
 
2992
- for well in self.wells:
2993
- try:
2994
- # Get x and y properties
2995
- x_prop = well.get_property(self.x)
2996
- y_prop = well.get_property(self.y)
2997
-
2998
- # Get depths - use x property's depth
2999
- depths = x_prop.depth
3000
- x_values = x_prop.values
3001
- y_values = y_prop.values
3002
-
3003
- # Helper function to check if alignment is needed
3004
- def needs_alignment(prop_depth, ref_depth):
3005
- """Quick check if depths need alignment."""
3006
- if len(prop_depth) != len(ref_depth):
3007
- return True
3008
- # Fast check: if arrays are identical objects or first/last don't match
3009
- if prop_depth is ref_depth:
3010
- return False
3011
- if prop_depth[0] != ref_depth[0] or prop_depth[-1] != ref_depth[-1]:
3012
- return True
3013
- # Only do expensive allclose if needed
3014
- return not np.allclose(prop_depth, ref_depth)
3015
-
3016
- # Align y values to x depth grid if needed
3017
- if needs_alignment(y_prop.depth, depths):
3018
- y_values = np.interp(depths, y_prop.depth, y_prop.values, left=np.nan, right=np.nan)
3019
-
3020
- # Create dataframe for this well
3021
- df = pd.DataFrame({
3022
- 'depth': depths,
3023
- 'x': x_values,
3024
- 'y': y_values,
3025
- 'well': well.name
3026
- })
3027
-
3028
- # Add color property if specified
3029
- if self.color and self.color != "depth":
3030
- try:
3031
- color_prop = well.get_property(self.color)
3032
- color_values = color_prop.values
3033
- # Align to x depth grid
3034
- if needs_alignment(color_prop.depth, depths):
3035
- color_values = np.interp(depths, color_prop.depth, color_prop.values, left=np.nan, right=np.nan)
3036
- df['color_val'] = color_values
3037
- except (AttributeError, KeyError, PropertyNotFoundError):
3038
- 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":
3039
3175
  df['color_val'] = depths
3040
- elif self.color == "depth":
3041
- df['color_val'] = depths
3042
3176
 
3043
- # Add size property if specified
3044
- if self.size:
3045
- try:
3046
- size_prop = well.get_property(self.size)
3047
- size_values = size_prop.values
3048
- # Align to x depth grid
3049
- if needs_alignment(size_prop.depth, depths):
3050
- size_values = np.interp(depths, size_prop.depth, size_prop.values, left=np.nan, right=np.nan)
3051
- df['size_val'] = size_values
3052
- except (AttributeError, KeyError, PropertyNotFoundError):
3053
- warnings.warn(f"Size property '{self.size}' not found in well '{well.name}'")
3054
-
3055
- # Add shape property if specified and not "well"
3056
- if self.shape and self.shape != "well":
3057
- try:
3058
- shape_prop = well.get_property(self.shape)
3059
- shape_values = shape_prop.values
3060
- # Align to x depth grid
3061
- if needs_alignment(shape_prop.depth, depths):
3062
- shape_values = np.interp(depths, shape_prop.depth, shape_prop.values, left=np.nan, right=np.nan)
3063
- df['shape_val'] = shape_values
3064
- except (AttributeError, KeyError, PropertyNotFoundError):
3065
- warnings.warn(f"Shape property '{self.shape}' not found in well '{well.name}'")
3066
-
3067
- all_data.append(df)
3068
-
3069
- except (AttributeError, KeyError, PropertyNotFoundError) as e:
3070
- warnings.warn(f"Could not get properties for well '{well.name}': {e}")
3071
- 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
3072
3214
 
3073
3215
  if not all_data:
3074
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.127
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=tNaS1BPr1kLmr73BKqCBOSMptDdsxVofDic6vJ1iyr4,157418
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.127.dist-info/METADATA,sha256=6WtUXbcWy0jWgfCF2Fv06MoBHIvEJREjksDth_Q-PL8,59810
13
- well_log_toolkit-0.1.127.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- well_log_toolkit-0.1.127.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.127.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,,