well-log-toolkit 0.1.130__py3-none-any.whl → 0.1.132__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.
@@ -642,7 +642,7 @@ class Property(PropertyOperationsMixin):
642
642
 
643
643
  def resample(self, target_depth: Union[np.ndarray, 'Property']) -> 'Property':
644
644
  """
645
- Resample property to a new depth grid using linear interpolation.
645
+ Resample property to a new depth grid using appropriate interpolation.
646
646
 
647
647
  This method creates a new Property object with values interpolated to match
648
648
  the target depth grid. This is required when combining properties with
@@ -663,7 +663,10 @@ class Property(PropertyOperationsMixin):
663
663
  Notes
664
664
  -----
665
665
  - Uses linear interpolation for continuous data
666
- - Uses nearest-neighbor interpolation for discrete data
666
+ - Uses forward-fill (previous) for discrete data - geological zones extend
667
+ from their top/boundary until the next boundary is encountered. For example,
668
+ "Cerisa West top" at 2929.93m remains active until "Cerisa West SST 1 top"
669
+ at 2955.10m is intercepted.
667
670
  - Values outside the original depth range are set to NaN
668
671
  - NaN values in original data are excluded from interpolation
669
672
 
@@ -728,7 +731,11 @@ class Property(PropertyOperationsMixin):
728
731
 
729
732
  # Choose interpolation method based on type
730
733
  if self.type == 'discrete':
731
- kind = 'nearest'
734
+ # Use 'previous' (forward-fill) for discrete properties
735
+ # This ensures geological zones extend from their top/boundary
736
+ # until the next top is encountered (e.g., "Cerisa West top" at 2929.93
737
+ # remains active until "Cerisa West SST 1 top" at 2955.10)
738
+ kind = 'previous'
732
739
  else:
733
740
  kind = 'linear'
734
741
 
@@ -3010,7 +3010,11 @@ class Crossplot:
3010
3010
  self.shape = "label"
3011
3011
  else:
3012
3012
  self.shape = shape
3013
- self.color = color
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
3014
3018
  self.size = size
3015
3019
  self.colortemplate = colortemplate
3016
3020
  self.color_range = color_range
@@ -3068,6 +3072,10 @@ class Crossplot:
3068
3072
  # Data cache
3069
3073
  self._data = None
3070
3074
 
3075
+ # Discrete property labels storage
3076
+ # Maps property role ('shape', 'color', 'size') to labels dict {0: 'label0', 1: 'label1', ...}
3077
+ self._discrete_labels = {}
3078
+
3071
3079
  def add_layer(self, x: str, y: str, label: str):
3072
3080
  """
3073
3081
  Add a new data layer to the crossplot.
@@ -3132,6 +3140,32 @@ class Crossplot:
3132
3140
  # Only do expensive allclose if needed
3133
3141
  return not np.allclose(prop_depth, ref_depth)
3134
3142
 
3143
+ # Helper function to align property values to target depth grid
3144
+ def align_property(prop, target_depth):
3145
+ """
3146
+ Align property values to target depth grid.
3147
+
3148
+ Uses appropriate interpolation based on property type:
3149
+ - Discrete properties: forward-fill/previous (geological zones extend from
3150
+ their top/boundary until the next boundary is encountered)
3151
+ - Continuous properties: linear interpolation
3152
+
3153
+ Args:
3154
+ prop: Property object to align
3155
+ target_depth: Target depth array
3156
+
3157
+ Returns:
3158
+ Aligned values array
3159
+ """
3160
+ if prop.type == 'discrete':
3161
+ # Use Property's resample method which handles discrete properties correctly
3162
+ # (forward-fill to preserve integer codes and geological zone logic)
3163
+ resampled = prop.resample(target_depth)
3164
+ return resampled.values
3165
+ else:
3166
+ # For continuous properties, use linear interpolation
3167
+ return np.interp(target_depth, prop.depth, prop.values, left=np.nan, right=np.nan)
3168
+
3135
3169
  # Loop through each layer
3136
3170
  for layer in self._layers:
3137
3171
  layer_x = layer['x']
@@ -3149,9 +3183,9 @@ class Crossplot:
3149
3183
  x_values = x_prop.values
3150
3184
  y_values = y_prop.values
3151
3185
 
3152
- # Align y values to x depth grid if needed
3186
+ # Align y values to x depth grid if needed using appropriate method
3153
3187
  if needs_alignment(y_prop.depth, depths):
3154
- y_values = np.interp(depths, y_prop.depth, y_prop.values, left=np.nan, right=np.nan)
3188
+ y_values = align_property(y_prop, depths)
3155
3189
 
3156
3190
  # Create dataframe for this well and layer
3157
3191
  df = pd.DataFrame({
@@ -3166,13 +3200,20 @@ class Crossplot:
3166
3200
  if self.color == "label":
3167
3201
  # Use layer label for color
3168
3202
  df['color_val'] = layer_label
3203
+ elif self.color == "well":
3204
+ # Use well name for color (categorical)
3205
+ df['color_val'] = well.name
3169
3206
  elif self.color and self.color != "depth":
3170
3207
  try:
3171
3208
  color_prop = well.get_property(self.color)
3172
- color_values = color_prop.values
3173
- # Align to x depth grid
3209
+ # Store labels if discrete property (only once)
3210
+ if 'color' not in self._discrete_labels:
3211
+ self._store_discrete_labels(color_prop, 'color')
3212
+ # Align to x depth grid using appropriate method for property type
3174
3213
  if needs_alignment(color_prop.depth, depths):
3175
- color_values = np.interp(depths, color_prop.depth, color_prop.values, left=np.nan, right=np.nan)
3214
+ color_values = align_property(color_prop, depths)
3215
+ else:
3216
+ color_values = color_prop.values
3176
3217
  df['color_val'] = color_values
3177
3218
  except (AttributeError, KeyError, PropertyNotFoundError):
3178
3219
  # Silently use depth as fallback
@@ -3187,10 +3228,14 @@ class Crossplot:
3187
3228
  elif self.size:
3188
3229
  try:
3189
3230
  size_prop = well.get_property(self.size)
3190
- size_values = size_prop.values
3191
- # Align to x depth grid
3231
+ # Store labels if discrete property (only once)
3232
+ if 'size' not in self._discrete_labels:
3233
+ self._store_discrete_labels(size_prop, 'size')
3234
+ # Align to x depth grid using appropriate method for property type
3192
3235
  if needs_alignment(size_prop.depth, depths):
3193
- size_values = np.interp(depths, size_prop.depth, size_prop.values, left=np.nan, right=np.nan)
3236
+ size_values = align_property(size_prop, depths)
3237
+ else:
3238
+ size_values = size_prop.values
3194
3239
  df['size_val'] = size_values
3195
3240
  except (AttributeError, KeyError, PropertyNotFoundError):
3196
3241
  # Silently skip if size property not found
@@ -3203,10 +3248,14 @@ class Crossplot:
3203
3248
  elif self.shape and self.shape != "well":
3204
3249
  try:
3205
3250
  shape_prop = well.get_property(self.shape)
3206
- shape_values = shape_prop.values
3207
- # Align to x depth grid
3251
+ # Store labels if discrete property (only once)
3252
+ if 'shape' not in self._discrete_labels:
3253
+ self._store_discrete_labels(shape_prop, 'shape')
3254
+ # Align to x depth grid using appropriate method for property type
3208
3255
  if needs_alignment(shape_prop.depth, depths):
3209
- shape_values = np.interp(depths, shape_prop.depth, shape_prop.values, left=np.nan, right=np.nan)
3256
+ shape_values = align_property(shape_prop, depths)
3257
+ else:
3258
+ shape_values = shape_prop.values
3210
3259
  df['shape_val'] = shape_values
3211
3260
  except (AttributeError, KeyError, PropertyNotFoundError):
3212
3261
  # Silently skip if shape property not found
@@ -3370,6 +3419,201 @@ class Crossplot:
3370
3419
 
3371
3420
  return best_pos, second_best_pos
3372
3421
 
3422
+ def _store_discrete_labels(self, prop, role: str) -> None:
3423
+ """
3424
+ Store labels from a discrete property for later use in legends.
3425
+
3426
+ Args:
3427
+ prop: Property object (must have type and labels attributes)
3428
+ role: Property role - 'shape', 'color', or 'size'
3429
+ """
3430
+ if hasattr(prop, 'type') and prop.type == 'discrete' and hasattr(prop, 'labels') and prop.labels:
3431
+ self._discrete_labels[role] = prop.labels.copy()
3432
+
3433
+ def _get_display_label(self, value, role: str) -> str:
3434
+ """
3435
+ Get display label for a value, using stored labels for discrete properties.
3436
+
3437
+ For discrete properties with labels, converts integer codes to readable names.
3438
+ For continuous properties or discrete without labels, returns string value.
3439
+
3440
+ Args:
3441
+ value: The value to get label for (could be int, float, or string)
3442
+ role: Property role - 'shape', 'color', or 'size'
3443
+
3444
+ Returns:
3445
+ Display label string
3446
+
3447
+ Examples:
3448
+ >>> # For discrete property with labels {0: 'Agat top', 1: 'Cerisa Main top'}
3449
+ >>> self._get_display_label(0.0, 'shape')
3450
+ 'Agat top'
3451
+
3452
+ >>> # For continuous property or no labels
3453
+ >>> self._get_display_label(2.5, 'color')
3454
+ '2.5'
3455
+ """
3456
+ if role in self._discrete_labels:
3457
+ # Try to convert to integer and look up label
3458
+ try:
3459
+ int_val = int(np.round(float(value)))
3460
+ return self._discrete_labels[role].get(int_val, str(value))
3461
+ except (ValueError, TypeError):
3462
+ return str(value)
3463
+ return str(value)
3464
+
3465
+ def _is_edge_location(self, location: str) -> bool:
3466
+ """Check if a legend location is on the left or right edge.
3467
+
3468
+ Args:
3469
+ location: Matplotlib location string
3470
+
3471
+ Returns:
3472
+ True if on left or right edge (for vertical stacking)
3473
+ """
3474
+ edge_locations = ['upper left', 'center left', 'lower left',
3475
+ 'upper right', 'center right', 'lower right']
3476
+ return location in edge_locations
3477
+
3478
+ def _create_grouped_legends(self,
3479
+ shape_handles, shape_title: str,
3480
+ color_handles, color_title: str,
3481
+ location: str) -> None:
3482
+ """Create grouped legends in the same region, stacked or side-by-side.
3483
+
3484
+ When both shape and color legends are needed, this groups them in the same
3485
+ 1/9th section without overlap. Stacks vertically on edges, side-by-side elsewhere.
3486
+
3487
+ Args:
3488
+ shape_handles: List of handles for shape legend
3489
+ shape_title: Title for shape legend
3490
+ color_handles: List of handles for color legend
3491
+ color_title: Title for color legend
3492
+ location: Matplotlib location string for positioning
3493
+ """
3494
+ is_edge = self._is_edge_location(location)
3495
+
3496
+ # Determine base anchor point from location string
3497
+ # Map location to (x, y) coordinates in figure space
3498
+ anchor_map = {
3499
+ 'upper left': (0.02, 0.98),
3500
+ 'upper center': (0.5, 0.98),
3501
+ 'upper right': (0.98, 0.98),
3502
+ 'center left': (0.02, 0.5),
3503
+ 'center': (0.5, 0.5),
3504
+ 'center right': (0.98, 0.5),
3505
+ 'lower left': (0.02, 0.02),
3506
+ 'lower center': (0.5, 0.02),
3507
+ 'lower right': (0.98, 0.02),
3508
+ }
3509
+
3510
+ base_x, base_y = anchor_map.get(location, (0.98, 0.98))
3511
+
3512
+ if is_edge:
3513
+ # Stack vertically on edges
3514
+ # Position shape legend at the top
3515
+ shape_legend = self.ax.legend(
3516
+ handles=shape_handles,
3517
+ title=shape_title,
3518
+ loc=location,
3519
+ frameon=True,
3520
+ framealpha=0.9,
3521
+ edgecolor='black',
3522
+ bbox_to_anchor=(base_x, base_y),
3523
+ bbox_transform=self.fig.transFigure
3524
+ )
3525
+ shape_legend.get_title().set_fontweight('bold')
3526
+ self.ax.add_artist(shape_legend)
3527
+
3528
+ # Calculate offset for color legend below shape legend
3529
+ # Estimate shape legend height and add spacing
3530
+ shape_height = len(shape_handles) * 0.025 + 0.05 # Rough estimate
3531
+
3532
+ # Adjust y position for color legend
3533
+ if 'upper' in location:
3534
+ color_y = base_y - shape_height - 0.02 # Stack below
3535
+ elif 'lower' in location:
3536
+ color_y = base_y + shape_height + 0.02 # Stack above
3537
+ else: # center
3538
+ color_y = base_y - shape_height / 2 - 0.01 # Stack below
3539
+
3540
+ color_legend = self.ax.legend(
3541
+ handles=color_handles,
3542
+ title=color_title,
3543
+ loc=location,
3544
+ frameon=True,
3545
+ framealpha=0.9,
3546
+ edgecolor='black',
3547
+ bbox_to_anchor=(base_x, color_y),
3548
+ bbox_transform=self.fig.transFigure
3549
+ )
3550
+ color_legend.get_title().set_fontweight('bold')
3551
+ else:
3552
+ # Place side by side for non-edge locations (top, bottom, center)
3553
+ # Estimate width of each legend
3554
+ legend_width = 0.15
3555
+
3556
+ if 'center' in location and location != 'center left' and location != 'center right':
3557
+ # For center positions, place them side by side
3558
+ shape_x = base_x - legend_width / 2 - 0.01
3559
+ color_x = base_x + legend_width / 2 + 0.01
3560
+
3561
+ shape_legend = self.ax.legend(
3562
+ handles=shape_handles,
3563
+ title=shape_title,
3564
+ loc='center',
3565
+ frameon=True,
3566
+ framealpha=0.9,
3567
+ edgecolor='black',
3568
+ bbox_to_anchor=(shape_x, base_y),
3569
+ bbox_transform=self.fig.transFigure
3570
+ )
3571
+ shape_legend.get_title().set_fontweight('bold')
3572
+ self.ax.add_artist(shape_legend)
3573
+
3574
+ color_legend = self.ax.legend(
3575
+ handles=color_handles,
3576
+ title=color_title,
3577
+ loc='center',
3578
+ frameon=True,
3579
+ framealpha=0.9,
3580
+ edgecolor='black',
3581
+ bbox_to_anchor=(color_x, base_y),
3582
+ bbox_transform=self.fig.transFigure
3583
+ )
3584
+ color_legend.get_title().set_fontweight('bold')
3585
+ else:
3586
+ # For other positions, fall back to stacking
3587
+ shape_legend = self.ax.legend(
3588
+ handles=shape_handles,
3589
+ title=shape_title,
3590
+ loc=location,
3591
+ frameon=True,
3592
+ framealpha=0.9,
3593
+ edgecolor='black'
3594
+ )
3595
+ shape_legend.get_title().set_fontweight('bold')
3596
+ self.ax.add_artist(shape_legend)
3597
+
3598
+ # Estimate offset
3599
+ shape_height = len(shape_handles) * 0.025 + 0.05
3600
+ if 'upper' in location:
3601
+ color_y = base_y - shape_height - 0.02
3602
+ else:
3603
+ color_y = base_y + shape_height + 0.02
3604
+
3605
+ color_legend = self.ax.legend(
3606
+ handles=color_handles,
3607
+ title=color_title,
3608
+ loc=location,
3609
+ frameon=True,
3610
+ framealpha=0.9,
3611
+ edgecolor='black',
3612
+ bbox_to_anchor=(base_x, color_y),
3613
+ bbox_transform=self.fig.transFigure
3614
+ )
3615
+ color_legend.get_title().set_fontweight('bold')
3616
+
3373
3617
  def _format_regression_label(self, name: str, reg, include_equation: bool = None, include_r2: bool = None) -> str:
3374
3618
  """Format a modern, compact regression label.
3375
3619
 
@@ -3891,7 +4135,7 @@ class Crossplot:
3891
4135
  # Create custom legend handles
3892
4136
  legend_elements = [Patch(facecolor=category_colors[cat],
3893
4137
  edgecolor=self.edge_color,
3894
- label=str(cat))
4138
+ label=self._get_display_label(cat, 'color'))
3895
4139
  for cat in unique_categories]
3896
4140
 
3897
4141
  colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
@@ -4001,20 +4245,53 @@ class Crossplot:
4001
4245
  edgecolors=self.edge_color,
4002
4246
  linewidths=self.edge_width,
4003
4247
  marker=marker,
4004
- label=str(group_name)
4248
+ label=self._get_display_label(group_name, 'shape')
4005
4249
  )
4006
4250
 
4007
4251
  if first_scatter is None and self.color and not is_categorical:
4008
4252
  first_scatter = scatter
4009
4253
 
4010
- # Add legend with smart placement
4011
- if self.show_legend:
4254
+ # Check if we need both shape and color legends (grouped layout)
4255
+ need_shape_legend = self.show_legend
4256
+ need_color_legend = self.color and is_categorical and self.show_legend
4257
+
4258
+ if need_shape_legend and need_color_legend:
4259
+ # Create grouped legends in the same region
4012
4260
  # Get best location based on data density
4013
4261
  if self._data is not None:
4014
4262
  primary_loc, _ = self._find_best_legend_locations(self._data)
4015
4263
  else:
4016
4264
  primary_loc = 'best'
4017
4265
 
4266
+ # Prepare shape legend handles (from scatter plots)
4267
+ shape_handles, _ = self.ax.get_legend_handles_labels()
4268
+
4269
+ # Prepare color legend handles
4270
+ c_vals_all = data['color_val'].values
4271
+ unique_categories = pd.Series(c_vals_all).dropna().unique()
4272
+ color_handles = [Patch(facecolor=category_colors[cat],
4273
+ edgecolor=self.edge_color,
4274
+ label=self._get_display_label(cat, 'color'))
4275
+ for cat in unique_categories]
4276
+
4277
+ colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
4278
+
4279
+ # Create grouped legends
4280
+ self._create_grouped_legends(
4281
+ shape_handles=shape_handles,
4282
+ shape_title=group_label,
4283
+ color_handles=color_handles,
4284
+ color_title=colorbar_label,
4285
+ location=primary_loc
4286
+ )
4287
+
4288
+ elif need_shape_legend:
4289
+ # Only shape legend needed
4290
+ if self._data is not None:
4291
+ primary_loc, _ = self._find_best_legend_locations(self._data)
4292
+ else:
4293
+ primary_loc = 'best'
4294
+
4018
4295
  legend = self.ax.legend(
4019
4296
  title=group_label,
4020
4297
  loc=primary_loc,
@@ -4023,43 +4300,15 @@ class Crossplot:
4023
4300
  edgecolor='black'
4024
4301
  )
4025
4302
  legend.get_title().set_fontweight('bold')
4026
-
4027
4303
  # Store the primary legend so it persists when regression legend is added
4028
4304
  self.ax.add_artist(legend)
4029
4305
 
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')
4306
+ # Add colorbar for continuous color mapping
4307
+ if self.color and not is_categorical and self.show_colorbar and first_scatter:
4308
+ # Add continuous colorbar
4309
+ self.colorbar = self.fig.colorbar(first_scatter, ax=self.ax)
4310
+ colorbar_label = self.color if self.color != "depth" else "Depth"
4311
+ self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
4063
4312
 
4064
4313
  def add_regression(
4065
4314
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.130
3
+ Version: 0.1.132
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
@@ -3,13 +3,13 @@ well_log_toolkit/exceptions.py,sha256=X_fzC7d4yaBFO9Vx74dEIB6xmI9Agi6_bTU3MPxn6k
3
3
  well_log_toolkit/las_file.py,sha256=Tj0mRfX1aX2s6uug7BBlY1m_mu3G50EGxHGzD0eEedE,53876
4
4
  well_log_toolkit/manager.py,sha256=PuHF8rqypirNIN77STHcvg8WneExikpq6ZvkcRbcQpg,109776
5
5
  well_log_toolkit/operations.py,sha256=z8j8fGBOwoJGUQFy-Vawjq9nm3OD_dUt0oaNh8yuG7o,18515
6
- well_log_toolkit/property.py,sha256=WOzoNQcmHCQ8moIKsnSyLgVC8s4LBu2x5IBXtFzmMe8,76236
6
+ well_log_toolkit/property.py,sha256=B-3mXNJmvIqjjMdsu1kgVSwMgEwbJ36wn_n_oppdJFw,76769
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=2Wmydurnll31chjhFlx6xr1UpySvi0OBu4ClLUte1BA,169609
10
+ well_log_toolkit/visualization.py,sha256=204l_LFgcSfnpHti1Xc1iGF-KUVEyxkJNS88gkUDeXA,179959
11
11
  well_log_toolkit/well.py,sha256=Aav5Y-rui8YsJdvk7BFndNPUu1O9mcjwDApAGyqV9kw,104535
12
- well_log_toolkit-0.1.130.dist-info/METADATA,sha256=GT1zxMtQKy_wI9XVrgNGerOGzmzLS-K7XiAeVS4uUH8,59810
13
- well_log_toolkit-0.1.130.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- well_log_toolkit-0.1.130.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.130.dist-info/RECORD,,
12
+ well_log_toolkit-0.1.132.dist-info/METADATA,sha256=5IVBXhf2t9MQW4Gswct9F1IHl5JJnKOWDZxK4g2vUq4,59810
13
+ well_log_toolkit-0.1.132.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ well_log_toolkit-0.1.132.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.132.dist-info/RECORD,,