well-log-toolkit 0.1.143__py3-none-any.whl → 0.1.144__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.
@@ -1872,7 +1872,8 @@ class Property(PropertyOperationsMixin):
1872
1872
  Compute discrete statistics for each custom interval independently.
1873
1873
 
1874
1874
  This allows overlapping intervals where the same depths can
1875
- contribute to multiple zones.
1875
+ contribute to multiple zones. Zone-level metadata (depth_range, thickness)
1876
+ is shown at the interval level, and fractions are relative to zone thickness.
1876
1877
  """
1877
1878
  result = {}
1878
1879
 
@@ -1884,22 +1885,49 @@ class Property(PropertyOperationsMixin):
1884
1885
  # Create mask for this interval (top <= depth < base)
1885
1886
  interval_mask = (self.depth >= top) & (self.depth < base)
1886
1887
 
1888
+ # Calculate zone thickness for fraction calculation
1889
+ full_intervals = compute_intervals(self.depth)
1890
+ valid_mask = ~np.isnan(self.values) & interval_mask
1891
+ zone_thickness = float(np.sum(full_intervals[valid_mask]))
1892
+
1893
+ # Get actual depth range within the interval (where we have data)
1894
+ if np.any(valid_mask):
1895
+ zone_depths = self.depth[valid_mask]
1896
+ zone_depth_range = {
1897
+ 'min': round(float(np.min(zone_depths)), precision),
1898
+ 'max': round(float(np.max(zone_depths)), precision)
1899
+ }
1900
+ else:
1901
+ zone_depth_range = {'min': top, 'max': base}
1902
+
1903
+ # Build interval result with zone-level metadata
1904
+ interval_result = {
1905
+ 'depth_range': zone_depth_range,
1906
+ 'thickness': round(zone_thickness, precision)
1907
+ }
1908
+
1887
1909
  # If there are secondary properties, group within this interval
1888
1910
  if self.secondary_properties:
1889
- result[interval_name] = self._recursive_discrete_group(
1911
+ facies_stats = self._recursive_discrete_group(
1890
1912
  0,
1891
1913
  interval_mask,
1892
- gross_thickness=gross_thickness,
1893
- precision=precision
1914
+ gross_thickness=zone_thickness, # Use zone thickness for fractions
1915
+ precision=precision,
1916
+ include_depth_range=False # Don't include depth_range per facies
1894
1917
  )
1895
1918
  else:
1896
1919
  # No secondary properties, compute stats directly for interval
1897
- result[interval_name] = self._compute_discrete_stats(
1920
+ facies_stats = self._compute_discrete_stats(
1898
1921
  interval_mask,
1899
- gross_thickness=gross_thickness,
1900
- precision=precision
1922
+ gross_thickness=zone_thickness, # Use zone thickness for fractions
1923
+ precision=precision,
1924
+ include_depth_range=False # Don't include depth_range per facies
1901
1925
  )
1902
1926
 
1927
+ # Nest facies stats under 'facies' key for cleaner structure
1928
+ interval_result['facies'] = facies_stats
1929
+ result[interval_name] = interval_result
1930
+
1903
1931
  return result
1904
1932
 
1905
1933
  def _recursive_discrete_group(
@@ -1907,7 +1935,8 @@ class Property(PropertyOperationsMixin):
1907
1935
  filter_idx: int,
1908
1936
  mask: np.ndarray,
1909
1937
  gross_thickness: float,
1910
- precision: int = 6
1938
+ precision: int = 6,
1939
+ include_depth_range: bool = True
1911
1940
  ) -> dict:
1912
1941
  """
1913
1942
  Recursively group discrete statistics by secondary properties.
@@ -1922,6 +1951,8 @@ class Property(PropertyOperationsMixin):
1922
1951
  Total gross thickness for fraction calculation
1923
1952
  precision : int, default 6
1924
1953
  Number of decimal places for rounding
1954
+ include_depth_range : bool, default True
1955
+ Whether to include depth_range in per-facies stats
1925
1956
 
1926
1957
  Returns
1927
1958
  -------
@@ -1930,7 +1961,7 @@ class Property(PropertyOperationsMixin):
1930
1961
  """
1931
1962
  if filter_idx >= len(self.secondary_properties):
1932
1963
  # Base case: compute discrete statistics for this group
1933
- return self._compute_discrete_stats(mask, gross_thickness, precision)
1964
+ return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
1934
1965
 
1935
1966
  # Get unique values for current filter
1936
1967
  current_filter = self.secondary_properties[filter_idx]
@@ -1940,7 +1971,7 @@ class Property(PropertyOperationsMixin):
1940
1971
 
1941
1972
  if len(unique_vals) == 0:
1942
1973
  # No valid values, return stats for current mask
1943
- return self._compute_discrete_stats(mask, gross_thickness, precision)
1974
+ return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
1944
1975
 
1945
1976
  # Group by each unique value
1946
1977
  depth_array = self.depth
@@ -1976,7 +2007,7 @@ class Property(PropertyOperationsMixin):
1976
2007
  key = f"{current_filter.name}_{val:.2f}"
1977
2008
 
1978
2009
  result[key] = self._recursive_discrete_group(
1979
- filter_idx + 1, sub_mask, group_thickness, precision
2010
+ filter_idx + 1, sub_mask, group_thickness, precision, include_depth_range
1980
2011
  )
1981
2012
 
1982
2013
  return result
@@ -1985,7 +2016,8 @@ class Property(PropertyOperationsMixin):
1985
2016
  self,
1986
2017
  mask: np.ndarray,
1987
2018
  gross_thickness: float,
1988
- precision: int = 6
2019
+ precision: int = 6,
2020
+ include_depth_range: bool = True
1989
2021
  ) -> dict:
1990
2022
  """
1991
2023
  Compute categorical statistics for discrete property values.
@@ -1998,12 +2030,15 @@ class Property(PropertyOperationsMixin):
1998
2030
  Total gross thickness for fraction calculation
1999
2031
  precision : int, default 6
2000
2032
  Number of decimal places for rounding
2033
+ include_depth_range : bool, default True
2034
+ Whether to include depth_range in per-facies stats.
2035
+ Set to False when using filter_intervals (depth_range shown at zone level).
2001
2036
 
2002
2037
  Returns
2003
2038
  -------
2004
2039
  dict
2005
2040
  Dictionary with stats for each discrete value:
2006
- {value_label: {code, count, thickness, fraction, depth_range}}
2041
+ {value_label: {code, count, thickness, fraction, [depth_range]}}
2007
2042
  """
2008
2043
  values_array = self.values
2009
2044
  depth_array = self.depth
@@ -2049,12 +2084,13 @@ class Property(PropertyOperationsMixin):
2049
2084
  'code': int_val,
2050
2085
  'count': count,
2051
2086
  'thickness': round(thickness, precision),
2052
- 'fraction': round(fraction, precision),
2053
- 'depth_range': {
2087
+ 'fraction': round(fraction, precision)
2088
+ }
2089
+ if include_depth_range:
2090
+ stats['depth_range'] = {
2054
2091
  'min': round(float(np.min(val_depths)), precision),
2055
2092
  'max': round(float(np.max(val_depths)), precision)
2056
2093
  }
2057
- }
2058
2094
  if label is not None:
2059
2095
  stats['label'] = label
2060
2096
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.143
3
+ Version: 0.1.144
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
@@ -288,6 +288,74 @@ stats = well.PHIE.filter('Zone').filter('Facies').sums_avg()
288
288
  - `samples` - Number of valid measurements
289
289
  - `range`, `depth_range` - Min/max values and depths
290
290
 
291
+ ### Custom Interval Filtering
292
+
293
+ Define custom depth intervals without needing a discrete property in the well:
294
+
295
+ ```python
296
+ # Define intervals with name, top, and base
297
+ intervals = [
298
+ {"name": "Zone_A", "top": 2500, "base": 2650},
299
+ {"name": "Zone_B", "top": 2650, "base": 2800}
300
+ ]
301
+
302
+ # Use with sums_avg or discrete_summary
303
+ stats = well.PHIE.filter_intervals(intervals).sums_avg()
304
+ # → {'Zone_A': {'mean': 0.18, ...}, 'Zone_B': {'mean': 0.21, ...}}
305
+
306
+ facies_stats = well.Facies.filter_intervals(intervals).discrete_summary()
307
+ ```
308
+
309
+ **Overlapping intervals** are supported - each interval is calculated independently:
310
+
311
+ ```python
312
+ # These intervals overlap at 2600-2700m
313
+ intervals = [
314
+ {"name": "Full_Reservoir", "top": 2500, "base": 2800},
315
+ {"name": "Upper_Section", "top": 2500, "base": 2700}
316
+ ]
317
+ # Depths 2500-2700 are counted in BOTH zones
318
+ stats = well.PHIE.filter_intervals(intervals).sums_avg()
319
+ ```
320
+
321
+ **Save intervals for reuse:**
322
+
323
+ ```python
324
+ # Save intervals to the well
325
+ well.PHIE.filter_intervals(intervals, save="Reservoir_Zones")
326
+
327
+ # Use saved intervals by name
328
+ stats = well.PHIE.filter_intervals("Reservoir_Zones").sums_avg()
329
+
330
+ # List saved intervals
331
+ print(well.saved_intervals) # ['Reservoir_Zones']
332
+
333
+ # Retrieve intervals
334
+ intervals = well.get_intervals("Reservoir_Zones")
335
+ ```
336
+
337
+ **Save different intervals for multiple wells:**
338
+
339
+ ```python
340
+ # Define well-specific intervals
341
+ manager.well_A.PHIE.filter_intervals({
342
+ "Well_A": [{"name": "Zone_A", "top": 2500, "base": 2700}],
343
+ "Well_B": [{"name": "Zone_A", "top": 2600, "base": 2800}]
344
+ }, save="My_Zones")
345
+
346
+ # Both wells now have "My_Zones" saved with their respective intervals
347
+ ```
348
+
349
+ **Chain with other filters:**
350
+
351
+ ```python
352
+ # Combine custom intervals with property filters
353
+ stats = well.PHIE.filter_intervals(intervals).filter("NetFlag").sums_avg()
354
+ # → {'Zone_A': {'Net': {...}, 'NonNet': {...}}, 'Zone_B': {...}}
355
+ ```
356
+
357
+ > **💡 Key Difference:** Unlike `.filter('Well_Tops')` where each depth belongs to exactly one zone, `filter_intervals()` allows overlapping intervals where the same depths can contribute to multiple zones.
358
+
291
359
  ### Property Operations
292
360
 
293
361
  Create computed properties using natural mathematical syntax:
@@ -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=VIARJLkYhxqxgTqfVfAAZU6AVsAPkQWPOUE6RNGnIdY,110558
5
5
  well_log_toolkit/operations.py,sha256=z8j8fGBOwoJGUQFy-Vawjq9nm3OD_dUt0oaNh8yuG7o,18515
6
- well_log_toolkit/property.py,sha256=EGT7SRXM4V0freWbyPBWyP7-w08S_iGcRPs1lPiXQ2c,97868
6
+ well_log_toolkit/property.py,sha256=4g5-_WRdJ9HwDKkufU4s_oOnCh6Deg58ZX5jE9Uwx2c,99833
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
10
  well_log_toolkit/visualization.py,sha256=nnpmFmbj44TbP0fsnLMR1GaKRkqKCEpI6Fd8Cp0oqBc,204716
11
11
  well_log_toolkit/well.py,sha256=n6XfaGSjGtyXCIaAr0ytslIK0DMUY_fSPQ_VCqj8jaU,106173
12
- well_log_toolkit-0.1.143.dist-info/METADATA,sha256=7JbOxO2QEPh0KKSS-U03-U94O7DKZTYk96laFMOj--Q,61388
13
- well_log_toolkit-0.1.143.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
- well_log_toolkit-0.1.143.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.143.dist-info/RECORD,,
12
+ well_log_toolkit-0.1.144.dist-info/METADATA,sha256=5GdblMrgAGxLNG0LA9pKav9UHUl4iTps6Zl34xK-4CA,63473
13
+ well_log_toolkit-0.1.144.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
+ well_log_toolkit-0.1.144.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.144.dist-info/RECORD,,