well-log-toolkit 0.1.143__tar.gz → 0.1.145__tar.gz

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.
Files changed (20) hide show
  1. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/PKG-INFO +69 -1
  2. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/README.md +68 -0
  3. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/pyproject.toml +1 -1
  4. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit/property.py +94 -35
  5. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit.egg-info/PKG-INFO +69 -1
  6. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/setup.cfg +0 -0
  7. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit/__init__.py +0 -0
  8. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit/exceptions.py +0 -0
  9. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit/las_file.py +0 -0
  10. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit/manager.py +0 -0
  11. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit/operations.py +0 -0
  12. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit/regression.py +0 -0
  13. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit/statistics.py +0 -0
  14. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit/utils.py +0 -0
  15. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit/visualization.py +0 -0
  16. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit/well.py +0 -0
  17. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
  18. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
  19. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit.egg-info/requires.txt +0 -0
  20. {well_log_toolkit-0.1.143 → well_log_toolkit-0.1.145}/well_log_toolkit.egg-info/top_level.txt +0 -0
@@ -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.145
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:
@@ -250,6 +250,74 @@ stats = well.PHIE.filter('Zone').filter('Facies').sums_avg()
250
250
  - `samples` - Number of valid measurements
251
251
  - `range`, `depth_range` - Min/max values and depths
252
252
 
253
+ ### Custom Interval Filtering
254
+
255
+ Define custom depth intervals without needing a discrete property in the well:
256
+
257
+ ```python
258
+ # Define intervals with name, top, and base
259
+ intervals = [
260
+ {"name": "Zone_A", "top": 2500, "base": 2650},
261
+ {"name": "Zone_B", "top": 2650, "base": 2800}
262
+ ]
263
+
264
+ # Use with sums_avg or discrete_summary
265
+ stats = well.PHIE.filter_intervals(intervals).sums_avg()
266
+ # → {'Zone_A': {'mean': 0.18, ...}, 'Zone_B': {'mean': 0.21, ...}}
267
+
268
+ facies_stats = well.Facies.filter_intervals(intervals).discrete_summary()
269
+ ```
270
+
271
+ **Overlapping intervals** are supported - each interval is calculated independently:
272
+
273
+ ```python
274
+ # These intervals overlap at 2600-2700m
275
+ intervals = [
276
+ {"name": "Full_Reservoir", "top": 2500, "base": 2800},
277
+ {"name": "Upper_Section", "top": 2500, "base": 2700}
278
+ ]
279
+ # Depths 2500-2700 are counted in BOTH zones
280
+ stats = well.PHIE.filter_intervals(intervals).sums_avg()
281
+ ```
282
+
283
+ **Save intervals for reuse:**
284
+
285
+ ```python
286
+ # Save intervals to the well
287
+ well.PHIE.filter_intervals(intervals, save="Reservoir_Zones")
288
+
289
+ # Use saved intervals by name
290
+ stats = well.PHIE.filter_intervals("Reservoir_Zones").sums_avg()
291
+
292
+ # List saved intervals
293
+ print(well.saved_intervals) # ['Reservoir_Zones']
294
+
295
+ # Retrieve intervals
296
+ intervals = well.get_intervals("Reservoir_Zones")
297
+ ```
298
+
299
+ **Save different intervals for multiple wells:**
300
+
301
+ ```python
302
+ # Define well-specific intervals
303
+ manager.well_A.PHIE.filter_intervals({
304
+ "Well_A": [{"name": "Zone_A", "top": 2500, "base": 2700}],
305
+ "Well_B": [{"name": "Zone_A", "top": 2600, "base": 2800}]
306
+ }, save="My_Zones")
307
+
308
+ # Both wells now have "My_Zones" saved with their respective intervals
309
+ ```
310
+
311
+ **Chain with other filters:**
312
+
313
+ ```python
314
+ # Combine custom intervals with property filters
315
+ stats = well.PHIE.filter_intervals(intervals).filter("NetFlag").sums_avg()
316
+ # → {'Zone_A': {'Net': {...}, 'NonNet': {...}}, 'Zone_B': {...}}
317
+ ```
318
+
319
+ > **💡 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.
320
+
253
321
  ### Property Operations
254
322
 
255
323
  Create computed properties using natural mathematical syntax:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "well-log-toolkit"
7
- version = "0.1.143"
7
+ version = "0.1.145"
8
8
  description = "Fast LAS file processing with lazy loading and filtering for well log analysis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1789,7 +1789,11 @@ class Property(PropertyOperationsMixin):
1789
1789
 
1790
1790
  return result
1791
1791
 
1792
- def discrete_summary(self, precision: int = 6) -> dict:
1792
+ def discrete_summary(
1793
+ self,
1794
+ precision: int = 6,
1795
+ skip: Optional[list[str]] = None
1796
+ ) -> dict:
1793
1797
  """
1794
1798
  Compute summary statistics for discrete/categorical properties.
1795
1799
 
@@ -1801,6 +1805,9 @@ class Property(PropertyOperationsMixin):
1801
1805
  ----------
1802
1806
  precision : int, default 6
1803
1807
  Number of decimal places for rounding numeric results
1808
+ skip : list[str], optional
1809
+ List of field names to exclude from the output.
1810
+ Valid fields: 'code', 'count', 'thickness', 'fraction', 'depth_range'
1804
1811
 
1805
1812
  Returns
1806
1813
  -------
@@ -1808,8 +1815,7 @@ class Property(PropertyOperationsMixin):
1808
1815
  Nested dictionary with statistics for each discrete value.
1809
1816
  If secondary properties (filters) exist, the structure is hierarchical.
1810
1817
 
1811
- For each discrete value, includes:
1812
- - label: Human-readable name (if labels defined)
1818
+ For each discrete value, includes (unless skipped):
1813
1819
  - code: Numeric code for this category
1814
1820
  - count: Number of samples with this value
1815
1821
  - thickness: Total depth interval (meters) for this category
@@ -1824,6 +1830,10 @@ class Property(PropertyOperationsMixin):
1824
1830
  >>> # {'Sand': {'code': 1, 'count': 150, 'thickness': 25.5, 'fraction': 0.45, ...},
1825
1831
  >>> # 'Shale': {'code': 2, 'count': 180, 'thickness': 30.8, 'fraction': 0.55, ...}}
1826
1832
 
1833
+ >>> # Skip certain fields
1834
+ >>> stats = facies.discrete_summary(skip=['code', 'count'])
1835
+ >>> # {'Sand': {'thickness': 25.5, 'fraction': 0.45}, ...}
1836
+
1827
1837
  >>> # Grouped by zones
1828
1838
  >>> filtered = facies.filter('Well_Tops')
1829
1839
  >>> stats = filtered.discrete_summary()
@@ -1842,26 +1852,43 @@ class Property(PropertyOperationsMixin):
1842
1852
  # Check for custom intervals (from filter_intervals)
1843
1853
  # These are processed independently, allowing overlaps
1844
1854
  if hasattr(self, '_custom_intervals') and self._custom_intervals:
1845
- return self._compute_discrete_stats_by_intervals(
1855
+ result = self._compute_discrete_stats_by_intervals(
1846
1856
  gross_thickness=gross_thickness,
1847
1857
  precision=precision
1848
1858
  )
1849
-
1850
- if not self.secondary_properties:
1859
+ elif not self.secondary_properties:
1851
1860
  # No filters, compute stats for all discrete values
1852
- return self._compute_discrete_stats(
1861
+ result = self._compute_discrete_stats(
1862
+ np.ones(len(self.depth), dtype=bool),
1863
+ gross_thickness=gross_thickness,
1864
+ precision=precision
1865
+ )
1866
+ else:
1867
+ # Build hierarchical grouping
1868
+ result = self._recursive_discrete_group(
1869
+ 0,
1853
1870
  np.ones(len(self.depth), dtype=bool),
1854
1871
  gross_thickness=gross_thickness,
1855
1872
  precision=precision
1856
1873
  )
1857
1874
 
1858
- # Build hierarchical grouping
1859
- return self._recursive_discrete_group(
1860
- 0,
1861
- np.ones(len(self.depth), dtype=bool),
1862
- gross_thickness=gross_thickness,
1863
- precision=precision
1864
- )
1875
+ # Remove skipped fields from output
1876
+ if skip:
1877
+ result = self._remove_keys_recursive(result, skip)
1878
+
1879
+ return result
1880
+
1881
+ def _remove_keys_recursive(self, d: dict, keys_to_remove: list[str]) -> dict:
1882
+ """Recursively remove specified keys from nested dicts."""
1883
+ result = {}
1884
+ for key, value in d.items():
1885
+ if key in keys_to_remove:
1886
+ continue
1887
+ if isinstance(value, dict):
1888
+ result[key] = self._remove_keys_recursive(value, keys_to_remove)
1889
+ else:
1890
+ result[key] = value
1891
+ return result
1865
1892
 
1866
1893
  def _compute_discrete_stats_by_intervals(
1867
1894
  self,
@@ -1872,7 +1899,8 @@ class Property(PropertyOperationsMixin):
1872
1899
  Compute discrete statistics for each custom interval independently.
1873
1900
 
1874
1901
  This allows overlapping intervals where the same depths can
1875
- contribute to multiple zones.
1902
+ contribute to multiple zones. Zone-level metadata (depth_range, thickness)
1903
+ is shown at the interval level, and fractions are relative to zone thickness.
1876
1904
  """
1877
1905
  result = {}
1878
1906
 
@@ -1884,22 +1912,49 @@ class Property(PropertyOperationsMixin):
1884
1912
  # Create mask for this interval (top <= depth < base)
1885
1913
  interval_mask = (self.depth >= top) & (self.depth < base)
1886
1914
 
1915
+ # Calculate zone thickness for fraction calculation
1916
+ full_intervals = compute_intervals(self.depth)
1917
+ valid_mask = ~np.isnan(self.values) & interval_mask
1918
+ zone_thickness = float(np.sum(full_intervals[valid_mask]))
1919
+
1920
+ # Get actual depth range within the interval (where we have data)
1921
+ if np.any(valid_mask):
1922
+ zone_depths = self.depth[valid_mask]
1923
+ zone_depth_range = {
1924
+ 'min': round(float(np.min(zone_depths)), precision),
1925
+ 'max': round(float(np.max(zone_depths)), precision)
1926
+ }
1927
+ else:
1928
+ zone_depth_range = {'min': top, 'max': base}
1929
+
1930
+ # Build interval result with zone-level metadata
1931
+ interval_result = {
1932
+ 'depth_range': zone_depth_range,
1933
+ 'thickness': round(zone_thickness, precision)
1934
+ }
1935
+
1887
1936
  # If there are secondary properties, group within this interval
1888
1937
  if self.secondary_properties:
1889
- result[interval_name] = self._recursive_discrete_group(
1938
+ facies_stats = self._recursive_discrete_group(
1890
1939
  0,
1891
1940
  interval_mask,
1892
- gross_thickness=gross_thickness,
1893
- precision=precision
1941
+ gross_thickness=zone_thickness, # Use zone thickness for fractions
1942
+ precision=precision,
1943
+ include_depth_range=False # Don't include depth_range per facies
1894
1944
  )
1895
1945
  else:
1896
1946
  # No secondary properties, compute stats directly for interval
1897
- result[interval_name] = self._compute_discrete_stats(
1947
+ facies_stats = self._compute_discrete_stats(
1898
1948
  interval_mask,
1899
- gross_thickness=gross_thickness,
1900
- precision=precision
1949
+ gross_thickness=zone_thickness, # Use zone thickness for fractions
1950
+ precision=precision,
1951
+ include_depth_range=False # Don't include depth_range per facies
1901
1952
  )
1902
1953
 
1954
+ # Nest facies stats under 'facies' key for cleaner structure
1955
+ interval_result['facies'] = facies_stats
1956
+ result[interval_name] = interval_result
1957
+
1903
1958
  return result
1904
1959
 
1905
1960
  def _recursive_discrete_group(
@@ -1907,7 +1962,8 @@ class Property(PropertyOperationsMixin):
1907
1962
  filter_idx: int,
1908
1963
  mask: np.ndarray,
1909
1964
  gross_thickness: float,
1910
- precision: int = 6
1965
+ precision: int = 6,
1966
+ include_depth_range: bool = True
1911
1967
  ) -> dict:
1912
1968
  """
1913
1969
  Recursively group discrete statistics by secondary properties.
@@ -1922,6 +1978,8 @@ class Property(PropertyOperationsMixin):
1922
1978
  Total gross thickness for fraction calculation
1923
1979
  precision : int, default 6
1924
1980
  Number of decimal places for rounding
1981
+ include_depth_range : bool, default True
1982
+ Whether to include depth_range in per-facies stats
1925
1983
 
1926
1984
  Returns
1927
1985
  -------
@@ -1930,7 +1988,7 @@ class Property(PropertyOperationsMixin):
1930
1988
  """
1931
1989
  if filter_idx >= len(self.secondary_properties):
1932
1990
  # Base case: compute discrete statistics for this group
1933
- return self._compute_discrete_stats(mask, gross_thickness, precision)
1991
+ return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
1934
1992
 
1935
1993
  # Get unique values for current filter
1936
1994
  current_filter = self.secondary_properties[filter_idx]
@@ -1940,7 +1998,7 @@ class Property(PropertyOperationsMixin):
1940
1998
 
1941
1999
  if len(unique_vals) == 0:
1942
2000
  # No valid values, return stats for current mask
1943
- return self._compute_discrete_stats(mask, gross_thickness, precision)
2001
+ return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
1944
2002
 
1945
2003
  # Group by each unique value
1946
2004
  depth_array = self.depth
@@ -1976,7 +2034,7 @@ class Property(PropertyOperationsMixin):
1976
2034
  key = f"{current_filter.name}_{val:.2f}"
1977
2035
 
1978
2036
  result[key] = self._recursive_discrete_group(
1979
- filter_idx + 1, sub_mask, group_thickness, precision
2037
+ filter_idx + 1, sub_mask, group_thickness, precision, include_depth_range
1980
2038
  )
1981
2039
 
1982
2040
  return result
@@ -1985,7 +2043,8 @@ class Property(PropertyOperationsMixin):
1985
2043
  self,
1986
2044
  mask: np.ndarray,
1987
2045
  gross_thickness: float,
1988
- precision: int = 6
2046
+ precision: int = 6,
2047
+ include_depth_range: bool = True
1989
2048
  ) -> dict:
1990
2049
  """
1991
2050
  Compute categorical statistics for discrete property values.
@@ -1998,12 +2057,15 @@ class Property(PropertyOperationsMixin):
1998
2057
  Total gross thickness for fraction calculation
1999
2058
  precision : int, default 6
2000
2059
  Number of decimal places for rounding
2060
+ include_depth_range : bool, default True
2061
+ Whether to include depth_range in per-facies stats.
2062
+ Set to False when using filter_intervals (depth_range shown at zone level).
2001
2063
 
2002
2064
  Returns
2003
2065
  -------
2004
2066
  dict
2005
2067
  Dictionary with stats for each discrete value:
2006
- {value_label: {code, count, thickness, fraction, depth_range}}
2068
+ {value_label: {code, count, thickness, fraction, [depth_range]}}
2007
2069
  """
2008
2070
  values_array = self.values
2009
2071
  depth_array = self.depth
@@ -2036,27 +2098,24 @@ class Property(PropertyOperationsMixin):
2036
2098
  count = int(np.sum(val_mask))
2037
2099
  fraction = thickness / gross_thickness if gross_thickness > 0 else 0.0
2038
2100
 
2039
- # Determine the key and label
2101
+ # Determine the key (use label if available, otherwise name_code)
2040
2102
  int_val = int(val)
2041
2103
  if self.labels is not None and int_val in self.labels:
2042
2104
  key = self.labels[int_val]
2043
- label = self.labels[int_val]
2044
2105
  else:
2045
2106
  key = f"{self.name}_{int_val}"
2046
- label = None
2047
2107
 
2048
2108
  stats = {
2049
2109
  'code': int_val,
2050
2110
  'count': count,
2051
2111
  'thickness': round(thickness, precision),
2052
- 'fraction': round(fraction, precision),
2053
- 'depth_range': {
2112
+ 'fraction': round(fraction, precision)
2113
+ }
2114
+ if include_depth_range:
2115
+ stats['depth_range'] = {
2054
2116
  'min': round(float(np.min(val_depths)), precision),
2055
2117
  'max': round(float(np.max(val_depths)), precision)
2056
2118
  }
2057
- }
2058
- if label is not None:
2059
- stats['label'] = label
2060
2119
 
2061
2120
  result[key] = stats
2062
2121
 
@@ -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.145
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: