well-log-toolkit 0.1.138__py3-none-any.whl → 0.1.140__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.
@@ -14,7 +14,7 @@ import pandas as pd
14
14
  import matplotlib.pyplot as plt
15
15
  import matplotlib.cm as cm
16
16
  from matplotlib.collections import PolyCollection
17
- from matplotlib.colors import Normalize
17
+ from matplotlib.colors import Normalize, LogNorm
18
18
  from matplotlib.patches import Rectangle, Patch
19
19
 
20
20
  if TYPE_CHECKING:
@@ -1120,17 +1120,18 @@ class WellView:
1120
1120
  )
1121
1121
 
1122
1122
  # Collect all tops data from all tops groups
1123
- all_tops_data = {} # depth -> name mapping
1123
+ all_tops_list = []
1124
1124
  for tops_group in self.tops:
1125
- tops_data = tops_group['tops']
1126
- all_tops_data.update(tops_data)
1125
+ entries = tops_group.get('entries', [])
1126
+ for entry in entries:
1127
+ all_tops_list.append((entry['depth'], entry['name']))
1127
1128
 
1128
1129
  # Find depths for specified tops
1129
1130
  tops_depths = []
1130
1131
  not_found = []
1131
1132
  for top_name in tops_list:
1132
1133
  found = False
1133
- for depth, name in all_tops_data.items():
1134
+ for depth, name in all_tops_list:
1134
1135
  if name == top_name:
1135
1136
  tops_depths.append(depth)
1136
1137
  found = True
@@ -1140,7 +1141,7 @@ class WellView:
1140
1141
 
1141
1142
  # Only raise error if NONE of the tops were found
1142
1143
  if not tops_depths:
1143
- available_tops = list(set(all_tops_data.values()))
1144
+ available_tops = list(set(name for _, name in all_tops_list))
1144
1145
  raise ValueError(
1145
1146
  f"None of the specified formation tops were found: {tops_list}. "
1146
1147
  f"Available tops: {available_tops}"
@@ -1338,11 +1339,8 @@ class WellView:
1338
1339
  if property_name is not None and tops_dict is not None:
1339
1340
  raise ValueError("Cannot specify both 'property_name' and 'tops_dict'")
1340
1341
 
1341
- # Get tops data
1342
- tops_data = {} # depth -> formation name
1343
- color_data = {} # depth -> color
1344
- style_data = {} # depth -> line style
1345
- thickness_data = {} # depth -> line thickness
1342
+ # Get tops data as list of entries (supports multiple tops at same depth)
1343
+ tops_entries = [] # List of {'depth': d, 'name': n, 'color': c, 'style': s, 'thickness': t}
1346
1344
 
1347
1345
  if property_name is not None:
1348
1346
  # Load from discrete property
@@ -1375,7 +1373,7 @@ class WellView:
1375
1373
  if valid_values[i] != valid_values[i-1]:
1376
1374
  boundaries.append(i)
1377
1375
 
1378
- # Build tops dictionary
1376
+ # Build tops entries list
1379
1377
  for idx in boundaries:
1380
1378
  depth = float(valid_depth[idx])
1381
1379
  value = int(valid_values[idx])
@@ -1386,42 +1384,43 @@ class WellView:
1386
1384
  else:
1387
1385
  formation_name = f"Zone {value}"
1388
1386
 
1389
- tops_data[depth] = formation_name
1387
+ entry = {'depth': depth, 'name': formation_name}
1390
1388
 
1391
1389
  # Get color if available (colors parameter overrides property colors)
1392
1390
  if colors is not None and value in colors:
1393
- color_data[depth] = colors[value]
1391
+ entry['color'] = colors[value]
1394
1392
  elif prop.colors and value in prop.colors:
1395
- color_data[depth] = prop.colors[value]
1393
+ entry['color'] = prop.colors[value]
1396
1394
 
1397
1395
  # Get style if available (styles parameter overrides property styles)
1398
1396
  if styles is not None and value in styles:
1399
- style_data[depth] = styles[value]
1397
+ entry['style'] = styles[value]
1400
1398
  elif prop.styles and value in prop.styles:
1401
- style_data[depth] = prop.styles[value]
1399
+ entry['style'] = prop.styles[value]
1402
1400
 
1403
1401
  # Get thickness if available (thicknesses parameter overrides property thicknesses)
1404
1402
  if thicknesses is not None and value in thicknesses:
1405
- thickness_data[depth] = thicknesses[value]
1403
+ entry['thickness'] = thicknesses[value]
1406
1404
  elif prop.thicknesses and value in prop.thicknesses:
1407
- thickness_data[depth] = prop.thicknesses[value]
1405
+ entry['thickness'] = prop.thicknesses[value]
1406
+
1407
+ tops_entries.append(entry)
1408
1408
 
1409
1409
  else:
1410
- # Use provided dictionary
1411
- tops_data = tops_dict
1412
- if colors is not None:
1413
- color_data = colors
1414
- if styles is not None:
1415
- style_data = styles
1416
- if thicknesses is not None:
1417
- thickness_data = thicknesses
1410
+ # Use provided dictionary - convert to list format
1411
+ for depth, name in tops_dict.items():
1412
+ entry = {'depth': depth, 'name': name}
1413
+ if colors is not None and depth in colors:
1414
+ entry['color'] = colors[depth]
1415
+ if styles is not None and depth in styles:
1416
+ entry['style'] = styles[depth]
1417
+ if thicknesses is not None and depth in thicknesses:
1418
+ entry['thickness'] = thicknesses[depth]
1419
+ tops_entries.append(entry)
1418
1420
 
1419
1421
  # Store tops for rendering
1420
1422
  self.tops.append({
1421
- 'tops': tops_data,
1422
- 'colors': color_data if color_data else None,
1423
- 'styles': style_data if style_data else None,
1424
- 'thicknesses': thickness_data if thickness_data else None
1423
+ 'entries': tops_entries
1425
1424
  })
1426
1425
 
1427
1426
  def _get_depth_mask(self, depth: np.ndarray) -> np.ndarray:
@@ -1451,34 +1450,21 @@ class WellView:
1451
1450
 
1452
1451
  # For each tops group
1453
1452
  for tops_group in self.tops:
1454
- tops_data = tops_group['tops']
1455
- colors_data = tops_group['colors']
1456
- styles_data = tops_group['styles']
1457
- thicknesses_data = tops_group['thicknesses']
1453
+ entries = tops_group.get('entries', [])
1458
1454
 
1459
1455
  # Draw each top
1460
- for depth, formation_name in tops_data.items():
1456
+ for entry in entries:
1457
+ depth = entry['depth']
1458
+ formation_name = entry['name']
1459
+
1461
1460
  # Skip tops outside depth range
1462
1461
  if depth < self.depth_range[0] or depth > self.depth_range[1]:
1463
1462
  continue
1464
1463
 
1465
- # Get color for this top
1466
- if colors_data and depth in colors_data:
1467
- color = colors_data[depth]
1468
- else:
1469
- color = 'black' # Default color
1470
-
1471
- # Get line style for this top
1472
- if styles_data and depth in styles_data:
1473
- linestyle = styles_data[depth]
1474
- else:
1475
- linestyle = 'solid' # Default style
1476
-
1477
- # Get line thickness for this top
1478
- if thicknesses_data and depth in thicknesses_data:
1479
- linewidth = thicknesses_data[depth]
1480
- else:
1481
- linewidth = 1.5 # Default thickness
1464
+ # Get color, style, thickness from entry (with defaults)
1465
+ color = entry.get('color', 'black')
1466
+ linestyle = entry.get('style', 'solid')
1467
+ linewidth = entry.get('thickness', 1.5)
1482
1468
 
1483
1469
  # Draw line across all non-depth tracks
1484
1470
  for ax in non_depth_axes:
@@ -2151,6 +2137,20 @@ class WellView:
2151
2137
  crossover_mask = left_values > right_values
2152
2138
  left_values = np.where(crossover_mask, right_values, left_values)
2153
2139
 
2140
+ # Create valid mask - skip points where boundary values are NaN
2141
+ boundary_valid_mask = ~(np.isnan(left_values) | np.isnan(right_values))
2142
+
2143
+ # Filter arrays to only valid points
2144
+ if not np.all(boundary_valid_mask):
2145
+ left_values = left_values[boundary_valid_mask]
2146
+ right_values = right_values[boundary_valid_mask]
2147
+ depth_for_fill = depth_for_fill[boundary_valid_mask]
2148
+ n_points = len(depth_for_fill)
2149
+
2150
+ if n_points < 2:
2151
+ # Not enough valid points to draw fill
2152
+ return
2153
+
2154
2154
  # Apply fill
2155
2155
  fill_color = fill.get("color", "lightblue")
2156
2156
  fill_alpha = fill.get("alpha", 0.3)
@@ -2185,6 +2185,10 @@ class WellView:
2185
2185
  warnings.warn("Cannot determine colormap values (no curve specified for left or right)")
2186
2186
  return
2187
2187
 
2188
+ # Apply same boundary mask to colormap values
2189
+ if not np.all(boundary_valid_mask):
2190
+ colormap_values = colormap_values[boundary_valid_mask]
2191
+
2188
2192
  # Get color range for normalization
2189
2193
  # Check if we have valid values
2190
2194
  valid_mask = ~np.isnan(colormap_values)
@@ -2193,7 +2197,17 @@ class WellView:
2193
2197
  return
2194
2198
 
2195
2199
  color_range = fill.get("color_range", [np.nanmin(colormap_values), np.nanmax(colormap_values)])
2196
- norm = Normalize(vmin=color_range[0], vmax=color_range[1])
2200
+ # Default color_log to track's log_scale setting
2201
+ color_log = fill.get("color_log", track_log_scale)
2202
+
2203
+ # Use LogNorm for log scale colormap, Normalize for linear
2204
+ if color_log:
2205
+ # Ensure positive values for log scale
2206
+ vmin = max(color_range[0], 1e-10)
2207
+ vmax = max(color_range[1], vmin * 10)
2208
+ norm = LogNorm(vmin=vmin, vmax=vmax)
2209
+ else:
2210
+ norm = Normalize(vmin=color_range[0], vmax=color_range[1])
2197
2211
  cmap = plt.get_cmap(cmap_name)
2198
2212
 
2199
2213
  # Create horizontal bands - each depth interval gets a color based on the curve value
@@ -2288,7 +2302,8 @@ class WellView:
2288
2302
  ax: plt.Axes,
2289
2303
  fill: dict,
2290
2304
  plotted_curves: dict,
2291
- depth: np.ndarray
2305
+ depth: np.ndarray,
2306
+ track_log_scale: bool = False
2292
2307
  ) -> None:
2293
2308
  """
2294
2309
  Add fill between curves or values.
@@ -2363,6 +2378,20 @@ class WellView:
2363
2378
  crossover_mask = left_values > right_values
2364
2379
  left_values = np.where(crossover_mask, right_values, left_values)
2365
2380
 
2381
+ # Create valid mask - skip points where boundary values are NaN
2382
+ boundary_valid_mask = ~(np.isnan(left_values) | np.isnan(right_values))
2383
+
2384
+ # Filter arrays to only valid points
2385
+ depth_for_fill = depth # Use local variable for consistency
2386
+ if not np.all(boundary_valid_mask):
2387
+ left_values = left_values[boundary_valid_mask]
2388
+ right_values = right_values[boundary_valid_mask]
2389
+ depth_for_fill = depth[boundary_valid_mask]
2390
+
2391
+ if len(depth_for_fill) < 2:
2392
+ # Not enough valid points to draw fill
2393
+ return
2394
+
2366
2395
  # Apply fill
2367
2396
  fill_color = fill.get("color", "lightblue")
2368
2397
  fill_alpha = fill.get("alpha", 0.3)
@@ -2397,6 +2426,10 @@ class WellView:
2397
2426
  warnings.warn("Cannot determine colormap values (no curve specified for left or right)")
2398
2427
  return
2399
2428
 
2429
+ # Apply same boundary mask to colormap values
2430
+ if not np.all(boundary_valid_mask):
2431
+ colormap_values = colormap_values[boundary_valid_mask]
2432
+
2400
2433
  # Get color range for normalization
2401
2434
  # Check if we have valid values
2402
2435
  valid_mask = ~np.isnan(colormap_values)
@@ -2405,12 +2438,22 @@ class WellView:
2405
2438
  return
2406
2439
 
2407
2440
  color_range = fill.get("color_range", [np.nanmin(colormap_values), np.nanmax(colormap_values)])
2408
- norm = Normalize(vmin=color_range[0], vmax=color_range[1])
2441
+ # Default color_log to track's log_scale setting
2442
+ color_log = fill.get("color_log", track_log_scale)
2443
+
2444
+ # Use LogNorm for log scale colormap, Normalize for linear
2445
+ if color_log:
2446
+ # Ensure positive values for log scale
2447
+ vmin = max(color_range[0], 1e-10)
2448
+ vmax = max(color_range[1], vmin * 10)
2449
+ norm = LogNorm(vmin=vmin, vmax=vmax)
2450
+ else:
2451
+ norm = Normalize(vmin=color_range[0], vmax=color_range[1])
2409
2452
  cmap = plt.get_cmap(cmap_name)
2410
2453
 
2411
2454
  # Create horizontal bands - each depth interval gets a color based on the curve value
2412
2455
  # Use PolyCollection for performance (1000x faster than loop with fill_betweenx)
2413
- n_intervals = len(depth) - 1
2456
+ n_intervals = len(depth_for_fill) - 1
2414
2457
 
2415
2458
  # Compute color values for each interval (average of adjacent points)
2416
2459
  color_values = (colormap_values[:-1] + colormap_values[1:]) / 2
@@ -2421,10 +2464,10 @@ class WellView:
2421
2464
  verts = []
2422
2465
  for i in range(n_intervals):
2423
2466
  verts.append([
2424
- (left_values[i], depth[i]),
2425
- (right_values[i], depth[i]),
2426
- (right_values[i+1], depth[i+1]),
2427
- (left_values[i+1], depth[i+1])
2467
+ (left_values[i], depth_for_fill[i]),
2468
+ (right_values[i], depth_for_fill[i]),
2469
+ (right_values[i+1], depth_for_fill[i+1]),
2470
+ (left_values[i+1], depth_for_fill[i+1])
2428
2471
  ])
2429
2472
 
2430
2473
  # Create PolyCollection with all polygons at once
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.138
3
+ Version: 0.1.140
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
@@ -463,7 +463,10 @@ template.add_track(track_type="continuous", logs=[...], title="Resistivity")
463
463
  template.add_track(track_type="discrete", logs=[...], title="Facies")
464
464
  template.add_track(track_type="depth", width=0.3, title="Depth")
465
465
 
466
- # Save for reuse
466
+ # Add to project (saves with manager.save())
467
+ manager.add_template(template) # Uses template name "reservoir"
468
+
469
+ # Or save standalone file
467
470
  template.save("reservoir_template.json")
468
471
  ```
469
472
 
@@ -811,8 +814,8 @@ view.show()
811
814
 
812
815
  **Option 2: Store in manager (recommended for multi-well projects)**
813
816
  ```python
814
- # Store template in manager
815
- manager.set_template("reservoir", template)
817
+ # Store template in manager (uses template.name automatically)
818
+ manager.add_template(template)
816
819
 
817
820
  # Use by name in any well
818
821
  view = well.WellView(depth_range=[2800, 3000], template="reservoir")
@@ -859,8 +862,8 @@ template.remove_track(2)
859
862
  template.add_track(track_type="continuous", logs=[{"name": "RT"}])
860
863
 
861
864
  # Save changes
862
- manager.set_template("reservoir", template) # Update in manager
863
- template.save("updated_template.json") # Save to file
865
+ manager.add_template(template) # Update in manager (uses template.name)
866
+ template.save("updated_template.json") # Save to file
864
867
  ```
865
868
 
866
869
  ### Customization
@@ -1006,8 +1009,8 @@ template.add_track(track_type="depth", width=0.3, title="MD (m)")
1006
1009
  # Add formation tops spanning all tracks
1007
1010
  template.add_tops(property_name='Zone')
1008
1011
 
1009
- # Save and display
1010
- manager.set_template("comprehensive", template)
1012
+ # Add to project and display
1013
+ manager.add_template(template)
1011
1014
  view = well.WellView(depth_range=[2800, 3200], template="comprehensive")
1012
1015
  view.save("comprehensive_log.png", dpi=300)
1013
1016
  ```
@@ -1886,7 +1889,8 @@ from well_log_toolkit import WellDataManager, Well, Property, LasFile
1886
1889
  - `remove_well(name)` - Remove well
1887
1890
  - `save(directory)` - Save project
1888
1891
  - `load(directory)` - Load project
1889
- - `set_template(name, template)` - Store template
1892
+ - `add_template(template)` - Store template (uses template.name)
1893
+ - `set_template(name, template)` - Store template with custom name
1890
1894
  - `get_template(name)` - Retrieve template
1891
1895
  - `list_templates()` - List template names
1892
1896
  - `Crossplot(x, y, wells=None, shape="well", ...)` - Create multi-well crossplot
@@ -2053,7 +2057,7 @@ template.add_track(
2053
2057
  logs=[{"name": "GR", "x_range": [0, 150], "color": "green"}],
2054
2058
  title="Gamma Ray"
2055
2059
  )
2056
- manager.set_template("custom", template)
2060
+ manager.add_template(template) # Stored as "custom"
2057
2061
  view = well.WellView(template="custom")
2058
2062
  view.save("log.png", dpi=300)
2059
2063
  ```
@@ -7,9 +7,9 @@ well_log_toolkit/property.py,sha256=ol_E4r31ButSnQEzAZjI00ofaRePPcJstRLOFFcgtp4,
7
7
  well_log_toolkit/regression.py,sha256=JDcRxaODJnFikAdPJyTq8eUV7iY0vCDmvnGufqlojxs,31625
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=HsTpd4UQCbu6pluVJ3AD9WWRg5mue8XGnlNhJUVIhF8,202585
10
+ well_log_toolkit/visualization.py,sha256=nnpmFmbj44TbP0fsnLMR1GaKRkqKCEpI6Fd8Cp0oqBc,204716
11
11
  well_log_toolkit/well.py,sha256=Aav5Y-rui8YsJdvk7BFndNPUu1O9mcjwDApAGyqV9kw,104535
12
- well_log_toolkit-0.1.138.dist-info/METADATA,sha256=foX8i4aDHbERhEqZJOdE6E0rNT7yF4imIm8gKhudIqo,61149
13
- well_log_toolkit-0.1.138.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
- well_log_toolkit-0.1.138.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.138.dist-info/RECORD,,
12
+ well_log_toolkit-0.1.140.dist-info/METADATA,sha256=Mchq-GINJbLb3Bul25DQ_AI6FjRJWZtsQKra27L1TOk,61388
13
+ well_log_toolkit-0.1.140.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
+ well_log_toolkit-0.1.140.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.140.dist-info/RECORD,,