well-log-toolkit 0.1.147__py3-none-any.whl → 0.1.149__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.
@@ -1646,6 +1646,19 @@ class _ManagerMultiPropertyProxy:
1646
1646
  self._filters = filters or []
1647
1647
  self._custom_intervals = custom_intervals
1648
1648
 
1649
+ def __getattr__(self, name: str) -> '_ManagerMultiPropertyProxy':
1650
+ """
1651
+ Attribute access as shorthand for filter().
1652
+
1653
+ Allows: manager.properties(['A', 'B']).Facies.sums_avg()
1654
+ Same as: manager.properties(['A', 'B']).filter('Facies').sums_avg()
1655
+ """
1656
+ # Avoid recursion for private attributes
1657
+ if name.startswith('_'):
1658
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
1659
+ # Treat as filter
1660
+ return self.filter(name)
1661
+
1649
1662
  def filter(
1650
1663
  self,
1651
1664
  property_name: str,
@@ -1759,13 +1772,10 @@ class _ManagerMultiPropertyProxy:
1759
1772
 
1760
1773
  >>> manager.properties(['PHIE', 'PERM']).filter_intervals("Zones").sums_avg()
1761
1774
  >>> # Returns stats for both properties grouped by custom intervals
1762
- """
1763
- if not self._filters and not self._custom_intervals:
1764
- raise ValueError(
1765
- "sums_avg() requires at least one filter or filter_intervals(). "
1766
- "Use .filter('property_name') or .filter_intervals(...) first."
1767
- )
1768
1775
 
1776
+ >>> # No filters - compute stats for full well
1777
+ >>> manager.properties(['PHIE', 'PERM']).sums_avg()
1778
+ """
1769
1779
  result = {}
1770
1780
 
1771
1781
  for well_name, well in self._manager._wells.items():
@@ -1787,6 +1797,18 @@ class _ManagerMultiPropertyProxy:
1787
1797
  """
1788
1798
  Compute multi-property sums_avg for a single well.
1789
1799
  """
1800
+ # Check if this well has the required saved intervals (if using saved name)
1801
+ if self._custom_intervals:
1802
+ intervals = self._custom_intervals.get('intervals')
1803
+ if isinstance(intervals, str):
1804
+ # Saved filter name - check if this well has it
1805
+ if intervals not in well._saved_filter_intervals:
1806
+ return None # Skip wells that don't have this saved filter
1807
+ elif isinstance(intervals, dict):
1808
+ # Well-specific intervals - check if this well is in the dict
1809
+ if well.name not in intervals and well.sanitized_name not in intervals:
1810
+ return None # Skip wells not in the dict
1811
+
1790
1812
  # Collect results for each property
1791
1813
  property_results = {}
1792
1814
 
@@ -1798,7 +1820,7 @@ class _ManagerMultiPropertyProxy:
1798
1820
  if self._custom_intervals:
1799
1821
  prop = self._apply_filter_intervals(prop, well)
1800
1822
  if prop is None:
1801
- return None # Well doesn't have the saved intervals
1823
+ continue # Skip this property if intervals can't be applied
1802
1824
 
1803
1825
  # Apply all filters
1804
1826
  for filter_name, insert_boundaries in self._filters:
@@ -1815,13 +1837,17 @@ class _ManagerMultiPropertyProxy:
1815
1837
  )
1816
1838
  property_results[prop_name] = result
1817
1839
 
1818
- except (PropertyNotFoundError, PropertyTypeError, AttributeError, KeyError, ValueError):
1819
- # Property doesn't exist in this well, skip it
1840
+ except (PropertyNotFoundError, PropertyTypeError, AttributeError, KeyError):
1841
+ # Property doesn't exist in this well or filter error, skip it
1820
1842
  pass
1821
1843
 
1822
1844
  if not property_results:
1823
1845
  return None
1824
1846
 
1847
+ # If no filters/intervals, return simple merged result (no grouping)
1848
+ if not self._filters and not self._custom_intervals:
1849
+ return self._merge_flat_results(property_results)
1850
+
1825
1851
  # Merge results: nest property-specific stats, keep common stats at group level
1826
1852
  return self._merge_property_results(property_results)
1827
1853
 
@@ -1869,6 +1895,54 @@ class _ManagerMultiPropertyProxy:
1869
1895
  save=save
1870
1896
  )
1871
1897
 
1898
+ def _merge_flat_results(self, property_results: dict) -> dict:
1899
+ """
1900
+ Merge results when no filters are applied (flat structure).
1901
+
1902
+ Returns a single dict with property-specific stats nested under property
1903
+ names and common stats at the top level.
1904
+
1905
+ Parameters
1906
+ ----------
1907
+ property_results : dict
1908
+ {property_name: sums_avg_result}
1909
+
1910
+ Returns
1911
+ -------
1912
+ dict
1913
+ {
1914
+ "PropertyA": {"mean": ..., "median": ..., ...},
1915
+ "PropertyB": {"mean": ..., ...},
1916
+ "depth_range": {...},
1917
+ "samples": ...,
1918
+ ...
1919
+ }
1920
+ """
1921
+ if not property_results:
1922
+ return {}
1923
+
1924
+ result = {}
1925
+
1926
+ # Add property-specific stats for each property
1927
+ for prop_name, prop_result in property_results.items():
1928
+ if isinstance(prop_result, dict):
1929
+ # Extract property-specific stats
1930
+ prop_stats = {
1931
+ k: v for k, v in prop_result.items()
1932
+ if k in self.PROPERTY_STATS
1933
+ }
1934
+ if prop_stats:
1935
+ result[prop_name] = prop_stats
1936
+
1937
+ # Add common stats from first property
1938
+ first_result = next(iter(property_results.values()))
1939
+ if isinstance(first_result, dict):
1940
+ for k, v in first_result.items():
1941
+ if k in self.COMMON_STATS:
1942
+ result[k] = v
1943
+
1944
+ return result
1945
+
1872
1946
  def _merge_property_results(self, property_results: dict) -> dict:
1873
1947
  """
1874
1948
  Merge results from multiple properties.
@@ -2867,6 +2941,18 @@ class WellDataManager:
2867
2941
  # Delete sources marked for deletion
2868
2942
  well.delete_marked_sources(well_folder)
2869
2943
 
2944
+ # Save filter intervals if any exist
2945
+ if hasattr(well, '_saved_filter_intervals') and well._saved_filter_intervals:
2946
+ import json
2947
+ intervals_file = well_folder / "intervals.json"
2948
+ with open(intervals_file, 'w') as f:
2949
+ json.dump(well._saved_filter_intervals, f, indent=2)
2950
+ else:
2951
+ # Remove intervals file if no intervals (in case they were deleted)
2952
+ intervals_file = well_folder / "intervals.json"
2953
+ if intervals_file.exists():
2954
+ intervals_file.unlink()
2955
+
2870
2956
  # Save templates
2871
2957
  if self._templates:
2872
2958
  templates_folder = save_path / "templates"
@@ -2957,6 +3043,20 @@ class WellDataManager:
2957
3043
  for las_file in las_files:
2958
3044
  self.load_las(las_file, silent=True)
2959
3045
 
3046
+ # Load saved filter intervals if they exist
3047
+ intervals_file = well_folder / "intervals.json"
3048
+ if intervals_file.exists():
3049
+ import json
3050
+ try:
3051
+ with open(intervals_file, 'r') as f:
3052
+ saved_intervals = json.load(f)
3053
+ # Find the well for this folder and set its intervals
3054
+ well_key = well_folder.name # e.g., "well_35_9_16_A"
3055
+ if well_key in self._wells:
3056
+ self._wells[well_key]._saved_filter_intervals = saved_intervals
3057
+ except Exception as e:
3058
+ warnings.warn(f"Could not load intervals from {intervals_file}: {e}")
3059
+
2960
3060
  return self
2961
3061
 
2962
3062
  def add_well(self, well_name: str) -> Well:
@@ -11,6 +11,7 @@ from scipy.interpolate import interp1d
11
11
  from .exceptions import PropertyError, PropertyNotFoundError, PropertyTypeError, DepthAlignmentError
12
12
  from .statistics import (
13
13
  compute_intervals,
14
+ compute_zone_intervals,
14
15
  mean as stat_mean, sum as stat_sum, std as stat_std, percentile as stat_percentile
15
16
  )
16
17
  from .utils import filter_names
@@ -1429,8 +1430,8 @@ class Property(PropertyOperationsMixin):
1429
1430
  max_valid_depth = valid_depths.max()
1430
1431
 
1431
1432
  # Find boundaries that fall within the valid data range
1432
- # Filter to boundaries within valid range first
1433
- mask = (boundary_depths > min_valid_depth) & (boundary_depths < max_valid_depth)
1433
+ # Filter to boundaries within valid range (inclusive on both ends)
1434
+ mask = (boundary_depths >= min_valid_depth) & (boundary_depths <= max_valid_depth)
1434
1435
  potential_boundaries = boundary_depths[mask]
1435
1436
 
1436
1437
  if len(potential_boundaries) == 0:
@@ -1760,6 +1761,9 @@ class Property(PropertyOperationsMixin):
1760
1761
 
1761
1762
  This allows overlapping intervals where the same depths can
1762
1763
  contribute to multiple zones.
1764
+
1765
+ Uses zone-aware intervals that are truncated at zone boundaries to ensure
1766
+ thickness is correctly attributed to each zone.
1763
1767
  """
1764
1768
  result = {}
1765
1769
 
@@ -1768,8 +1772,12 @@ class Property(PropertyOperationsMixin):
1768
1772
  top = float(interval['top'])
1769
1773
  base = float(interval['base'])
1770
1774
 
1771
- # Create mask for this interval (top <= depth < base)
1772
- interval_mask = (self.depth >= top) & (self.depth < base)
1775
+ # Compute zone-aware intervals truncated at zone boundaries
1776
+ zone_intervals = compute_zone_intervals(self.depth, top, base)
1777
+
1778
+ # Create mask based on zone intervals - includes any sample that
1779
+ # contributes to this zone (even boundary samples with partial intervals)
1780
+ interval_mask = zone_intervals > 0
1773
1781
 
1774
1782
  # If there are secondary properties, group within this interval
1775
1783
  if self.secondary_properties:
@@ -1779,7 +1787,8 @@ class Property(PropertyOperationsMixin):
1779
1787
  weighted=weighted,
1780
1788
  arithmetic=arithmetic,
1781
1789
  gross_thickness=gross_thickness,
1782
- precision=precision
1790
+ precision=precision,
1791
+ zone_intervals=zone_intervals
1783
1792
  )
1784
1793
  else:
1785
1794
  # No secondary properties, compute stats directly for interval
@@ -1788,7 +1797,8 @@ class Property(PropertyOperationsMixin):
1788
1797
  weighted=weighted,
1789
1798
  arithmetic=arithmetic,
1790
1799
  gross_thickness=gross_thickness,
1791
- precision=precision
1800
+ precision=precision,
1801
+ zone_intervals=zone_intervals
1792
1802
  )
1793
1803
 
1794
1804
  return result
@@ -1905,6 +1915,9 @@ class Property(PropertyOperationsMixin):
1905
1915
  This allows overlapping intervals where the same depths can
1906
1916
  contribute to multiple zones. Zone-level metadata (depth_range, thickness)
1907
1917
  is shown at the interval level, and fractions are relative to zone thickness.
1918
+
1919
+ Uses zone-aware intervals that are truncated at zone boundaries to ensure
1920
+ thickness is correctly attributed to each zone.
1908
1921
  """
1909
1922
  result = {}
1910
1923
 
@@ -1913,13 +1926,16 @@ class Property(PropertyOperationsMixin):
1913
1926
  top = float(interval['top'])
1914
1927
  base = float(interval['base'])
1915
1928
 
1916
- # Create mask for this interval (top <= depth < base)
1917
- interval_mask = (self.depth >= top) & (self.depth < base)
1929
+ # Compute zone-aware intervals truncated at zone boundaries
1930
+ zone_intervals = compute_zone_intervals(self.depth, top, base)
1931
+
1932
+ # Create mask based on zone intervals - includes any sample that
1933
+ # contributes to this zone (even boundary samples with partial intervals)
1934
+ interval_mask = zone_intervals > 0
1918
1935
 
1919
- # Calculate zone thickness for fraction calculation
1920
- full_intervals = compute_intervals(self.depth)
1936
+ # Calculate zone thickness using zone-aware intervals
1921
1937
  valid_mask = ~np.isnan(self.values) & interval_mask
1922
- zone_thickness = float(np.sum(full_intervals[valid_mask]))
1938
+ zone_thickness = float(np.sum(zone_intervals[valid_mask]))
1923
1939
 
1924
1940
  # Get actual depth range within the interval (where we have data)
1925
1941
  if np.any(valid_mask):
@@ -1944,7 +1960,8 @@ class Property(PropertyOperationsMixin):
1944
1960
  interval_mask,
1945
1961
  gross_thickness=zone_thickness, # Use zone thickness for fractions
1946
1962
  precision=precision,
1947
- include_depth_range=False # Don't include depth_range per facies
1963
+ include_depth_range=False, # Don't include depth_range per facies
1964
+ zone_intervals=zone_intervals # Pass zone-aware intervals
1948
1965
  )
1949
1966
  else:
1950
1967
  # No secondary properties, compute stats directly for interval
@@ -1952,7 +1969,8 @@ class Property(PropertyOperationsMixin):
1952
1969
  interval_mask,
1953
1970
  gross_thickness=zone_thickness, # Use zone thickness for fractions
1954
1971
  precision=precision,
1955
- include_depth_range=False # Don't include depth_range per facies
1972
+ include_depth_range=False, # Don't include depth_range per facies
1973
+ zone_intervals=zone_intervals # Pass zone-aware intervals
1956
1974
  )
1957
1975
 
1958
1976
  # Nest facies stats under 'facies' key for cleaner structure
@@ -1967,7 +1985,8 @@ class Property(PropertyOperationsMixin):
1967
1985
  mask: np.ndarray,
1968
1986
  gross_thickness: float,
1969
1987
  precision: int = 6,
1970
- include_depth_range: bool = True
1988
+ include_depth_range: bool = True,
1989
+ zone_intervals: Optional[np.ndarray] = None
1971
1990
  ) -> dict:
1972
1991
  """
1973
1992
  Recursively group discrete statistics by secondary properties.
@@ -1984,6 +2003,9 @@ class Property(PropertyOperationsMixin):
1984
2003
  Number of decimal places for rounding
1985
2004
  include_depth_range : bool, default True
1986
2005
  Whether to include depth_range in per-facies stats
2006
+ zone_intervals : np.ndarray, optional
2007
+ Pre-computed zone-aware intervals truncated at zone boundaries.
2008
+ If None, computes intervals using standard midpoint method.
1987
2009
 
1988
2010
  Returns
1989
2011
  -------
@@ -1992,7 +2014,7 @@ class Property(PropertyOperationsMixin):
1992
2014
  """
1993
2015
  if filter_idx >= len(self.secondary_properties):
1994
2016
  # Base case: compute discrete statistics for this group
1995
- return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
2017
+ return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range, zone_intervals)
1996
2018
 
1997
2019
  # Get unique values for current filter
1998
2020
  current_filter = self.secondary_properties[filter_idx]
@@ -2002,12 +2024,16 @@ class Property(PropertyOperationsMixin):
2002
2024
 
2003
2025
  if len(unique_vals) == 0:
2004
2026
  # No valid values, return stats for current mask
2005
- return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
2027
+ return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range, zone_intervals)
2006
2028
 
2007
2029
  # Group by each unique value
2008
2030
  depth_array = self.depth
2009
2031
  values_array = self.values
2010
- full_intervals = compute_intervals(depth_array)
2032
+ # Use zone intervals if provided, otherwise compute on full array
2033
+ if zone_intervals is not None:
2034
+ full_intervals = zone_intervals
2035
+ else:
2036
+ full_intervals = compute_intervals(depth_array)
2011
2037
 
2012
2038
  result = {}
2013
2039
  for val in unique_vals:
@@ -2038,7 +2064,7 @@ class Property(PropertyOperationsMixin):
2038
2064
  key = f"{current_filter.name}_{val:.2f}"
2039
2065
 
2040
2066
  result[key] = self._recursive_discrete_group(
2041
- filter_idx + 1, sub_mask, group_thickness, precision, include_depth_range
2067
+ filter_idx + 1, sub_mask, group_thickness, precision, include_depth_range, zone_intervals
2042
2068
  )
2043
2069
 
2044
2070
  return result
@@ -2048,7 +2074,8 @@ class Property(PropertyOperationsMixin):
2048
2074
  mask: np.ndarray,
2049
2075
  gross_thickness: float,
2050
2076
  precision: int = 6,
2051
- include_depth_range: bool = True
2077
+ include_depth_range: bool = True,
2078
+ zone_intervals: Optional[np.ndarray] = None
2052
2079
  ) -> dict:
2053
2080
  """
2054
2081
  Compute categorical statistics for discrete property values.
@@ -2064,6 +2091,9 @@ class Property(PropertyOperationsMixin):
2064
2091
  include_depth_range : bool, default True
2065
2092
  Whether to include depth_range in per-facies stats.
2066
2093
  Set to False when using filter_intervals (depth_range shown at zone level).
2094
+ zone_intervals : np.ndarray, optional
2095
+ Pre-computed zone-aware intervals truncated at zone boundaries.
2096
+ If None, computes intervals using standard midpoint method.
2067
2097
 
2068
2098
  Returns
2069
2099
  -------
@@ -2077,9 +2107,12 @@ class Property(PropertyOperationsMixin):
2077
2107
  values = values_array[mask]
2078
2108
  depths = depth_array[mask]
2079
2109
 
2080
- # Compute intervals on full array then mask
2081
- full_intervals = compute_intervals(depth_array)
2082
- intervals = full_intervals[mask]
2110
+ # Use zone intervals if provided, otherwise compute on full array
2111
+ if zone_intervals is not None:
2112
+ intervals = zone_intervals[mask]
2113
+ else:
2114
+ full_intervals = compute_intervals(depth_array)
2115
+ intervals = full_intervals[mask]
2083
2116
 
2084
2117
  # Find unique discrete values
2085
2118
  valid_mask_local = ~np.isnan(values)
@@ -2132,7 +2165,8 @@ class Property(PropertyOperationsMixin):
2132
2165
  weighted: bool,
2133
2166
  arithmetic: bool,
2134
2167
  gross_thickness: float,
2135
- precision: int = 6
2168
+ precision: int = 6,
2169
+ zone_intervals: Optional[np.ndarray] = None
2136
2170
  ) -> dict:
2137
2171
  """
2138
2172
  Recursively group by secondary properties.
@@ -2151,6 +2185,9 @@ class Property(PropertyOperationsMixin):
2151
2185
  Total gross thickness for fraction calculation
2152
2186
  precision : int, default 6
2153
2187
  Number of decimal places for rounding
2188
+ zone_intervals : np.ndarray, optional
2189
+ Pre-computed zone-aware intervals truncated at zone boundaries.
2190
+ If None, computes intervals using standard midpoint method.
2154
2191
 
2155
2192
  Returns
2156
2193
  -------
@@ -2159,7 +2196,7 @@ class Property(PropertyOperationsMixin):
2159
2196
  """
2160
2197
  if filter_idx >= len(self.secondary_properties):
2161
2198
  # Base case: compute statistics for this group
2162
- return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision)
2199
+ return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision, zone_intervals)
2163
2200
 
2164
2201
  # Get unique values for current filter
2165
2202
  current_filter = self.secondary_properties[filter_idx]
@@ -2170,14 +2207,18 @@ class Property(PropertyOperationsMixin):
2170
2207
 
2171
2208
  if len(unique_vals) == 0:
2172
2209
  # No valid values, return stats for current mask
2173
- return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision)
2210
+ return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision, zone_intervals)
2174
2211
 
2175
2212
  # Calculate parent thickness BEFORE subdividing
2176
2213
  # This becomes the gross_thickness for all child groups
2177
2214
  # Cache property access to avoid overhead
2178
2215
  depth_array = self.depth
2179
2216
  values_array = self.values
2180
- parent_intervals = compute_intervals(depth_array)
2217
+ # Use zone intervals if provided, otherwise compute on full array
2218
+ if zone_intervals is not None:
2219
+ parent_intervals = zone_intervals
2220
+ else:
2221
+ parent_intervals = compute_intervals(depth_array)
2181
2222
  parent_valid = mask & ~np.isnan(values_array)
2182
2223
  parent_thickness = float(np.sum(parent_intervals[parent_valid]))
2183
2224
 
@@ -2212,7 +2253,7 @@ class Property(PropertyOperationsMixin):
2212
2253
  key = f"{current_filter.name}_{val:.2f}"
2213
2254
 
2214
2255
  result[key] = self._recursive_group(
2215
- filter_idx + 1, sub_mask, weighted, arithmetic, parent_thickness, precision
2256
+ filter_idx + 1, sub_mask, weighted, arithmetic, parent_thickness, precision, zone_intervals
2216
2257
  )
2217
2258
 
2218
2259
  return result
@@ -2223,7 +2264,8 @@ class Property(PropertyOperationsMixin):
2223
2264
  weighted: bool = True,
2224
2265
  arithmetic: bool = False,
2225
2266
  gross_thickness: float = 0.0,
2226
- precision: int = 6
2267
+ precision: int = 6,
2268
+ zone_intervals: Optional[np.ndarray] = None
2227
2269
  ) -> dict:
2228
2270
  """
2229
2271
  Compute statistics for values selected by mask.
@@ -2244,6 +2286,9 @@ class Property(PropertyOperationsMixin):
2244
2286
  Total gross thickness for fraction calculation
2245
2287
  precision : int, default 6
2246
2288
  Number of decimal places for rounding
2289
+ zone_intervals : np.ndarray, optional
2290
+ Pre-computed zone-aware intervals truncated at zone boundaries.
2291
+ If None, computes intervals using standard midpoint method.
2247
2292
 
2248
2293
  Returns
2249
2294
  -------
@@ -2263,12 +2308,17 @@ class Property(PropertyOperationsMixin):
2263
2308
  values = values_array[mask]
2264
2309
  valid = values[~np.isnan(values)]
2265
2310
 
2266
- # Compute depth intervals on FULL depth array first, then mask
2267
- # This is critical! Intervals must be computed on full grid so that
2268
- # zone boundary samples get correct weights based on their neighbors
2269
- # in the full sequence, not just within their zone.
2270
- full_intervals = compute_intervals(depth_array)
2271
- intervals = full_intervals[mask]
2311
+ # Use zone intervals if provided, otherwise compute on full array
2312
+ # Zone intervals are truncated at zone boundaries for accurate thickness
2313
+ if zone_intervals is not None:
2314
+ intervals = zone_intervals[mask]
2315
+ else:
2316
+ # Compute depth intervals on FULL depth array first, then mask
2317
+ # This is critical! Intervals must be computed on full grid so that
2318
+ # zone boundary samples get correct weights based on their neighbors
2319
+ # in the full sequence, not just within their zone.
2320
+ full_intervals = compute_intervals(depth_array)
2321
+ intervals = full_intervals[mask]
2272
2322
  valid_mask_local = ~np.isnan(values)
2273
2323
  valid_intervals = intervals[valid_mask_local]
2274
2324
 
@@ -61,6 +61,83 @@ def compute_intervals(depth: np.ndarray) -> np.ndarray:
61
61
  return intervals
62
62
 
63
63
 
64
+ def compute_zone_intervals(
65
+ depth: np.ndarray,
66
+ top: float,
67
+ base: float
68
+ ) -> np.ndarray:
69
+ """
70
+ Compute depth intervals truncated to zone boundaries.
71
+
72
+ Uses the midpoint method but truncates intervals at zone boundaries
73
+ to ensure thickness is correctly attributed to each zone.
74
+
75
+ Parameters
76
+ ----------
77
+ depth : np.ndarray
78
+ Depth values (must be sorted ascending)
79
+ top : float
80
+ Zone top depth (inclusive)
81
+ base : float
82
+ Zone base depth (exclusive)
83
+
84
+ Returns
85
+ -------
86
+ np.ndarray
87
+ Interval thickness for each depth point, truncated to zone boundaries.
88
+ Points outside the zone have zero interval.
89
+
90
+ Examples
91
+ --------
92
+ >>> depth = np.array([2708.0, 2708.3, 2708.4, 2708.6])
93
+ >>> # Zone from 2708.0 to 2708.4
94
+ >>> compute_zone_intervals(depth, 2708.0, 2708.4)
95
+ array([0.15, 0.2, 0.05, 0.0])
96
+
97
+ The intervals are truncated at zone boundary 2708.4:
98
+ - 2708.0: from 2708.0 to midpoint(2708.0, 2708.3)=2708.15 = 0.15m
99
+ - 2708.3: from 2708.15 to midpoint(2708.3, 2708.4)=2708.35 = 0.2m
100
+ - 2708.4: from 2708.35 to 2708.4 (zone boundary) = 0.05m (truncated)
101
+ - 2708.6: outside zone = 0.0m
102
+ """
103
+ if len(depth) == 0:
104
+ return np.array([])
105
+
106
+ if len(depth) == 1:
107
+ # Single point - check if it's in the zone
108
+ if top <= depth[0] < base:
109
+ return np.array([base - top])
110
+ return np.array([0.0])
111
+
112
+ zone_intervals = np.zeros(len(depth))
113
+
114
+ for i in range(len(depth)):
115
+ d = depth[i]
116
+
117
+ # Calculate midpoint bounds for this sample
118
+ if i == 0:
119
+ lower_bound = d - (depth[1] - d) / 2.0 # Mirror first interval
120
+ else:
121
+ lower_bound = (depth[i - 1] + d) / 2.0
122
+
123
+ if i == len(depth) - 1:
124
+ upper_bound = d + (d - depth[-2]) / 2.0 # Mirror last interval
125
+ else:
126
+ upper_bound = (d + depth[i + 1]) / 2.0
127
+
128
+ # Truncate to zone boundaries
129
+ effective_lower = max(lower_bound, top)
130
+ effective_upper = min(upper_bound, base)
131
+
132
+ # Only count if there's overlap with the zone
133
+ if effective_upper > effective_lower:
134
+ zone_intervals[i] = effective_upper - effective_lower
135
+ else:
136
+ zone_intervals[i] = 0.0
137
+
138
+ return zone_intervals
139
+
140
+
64
141
  def mean(
65
142
  values: np.ndarray,
66
143
  weights: Optional[np.ndarray] = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.147
3
+ Version: 0.1.149
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
@@ -1,15 +1,15 @@
1
1
  well_log_toolkit/__init__.py,sha256=ilJAIIhh68pYfD9I3V53juTEJpoMN8oHpcpEFNpuXAQ,3793
2
2
  well_log_toolkit/exceptions.py,sha256=X_fzC7d4yaBFO9Vx74dEIB6xmI9Agi6_bTU3MPxn6ko,985
3
3
  well_log_toolkit/las_file.py,sha256=Tj0mRfX1aX2s6uug7BBlY1m_mu3G50EGxHGzD0eEedE,53876
4
- well_log_toolkit/manager.py,sha256=WQHzNOXj7jVLeQlWG_uqfd93qemi5Mbe__z9NQRqk_Y,132563
4
+ well_log_toolkit/manager.py,sha256=Jx0w1KpxRB2Oex6bC0FQFZOlJkoTKNexW3HMXxndL68,136713
5
5
  well_log_toolkit/operations.py,sha256=z8j8fGBOwoJGUQFy-Vawjq9nm3OD_dUt0oaNh8yuG7o,18515
6
- well_log_toolkit/property.py,sha256=GsiD9c4SfBw8ar7ZJXS0NNejPlpvFRHKck_eBR2lLmo,100965
6
+ well_log_toolkit/property.py,sha256=TkBuqgunIGIGdsLtWtQkwwXPdh0yqY37PgO_150FilA,103782
7
7
  well_log_toolkit/regression.py,sha256=JDcRxaODJnFikAdPJyTq8eUV7iY0vCDmvnGufqlojxs,31625
8
- well_log_toolkit/statistics.py,sha256=_huPMbv2H3o9ezunjEM94mJknX5wPK8V4nDv2lIZZRw,16814
8
+ well_log_toolkit/statistics.py,sha256=cpUbaRGlqyqpGWKtETk9XpXWrMJIIjVacdqEqIBkvqQ,19118
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.147.dist-info/METADATA,sha256=U89SvoBEewtJiCSN0ZNvevh1UGV__w7-8C3YGGIQZUM,63473
13
- well_log_toolkit-0.1.147.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
- well_log_toolkit-0.1.147.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.147.dist-info/RECORD,,
12
+ well_log_toolkit-0.1.149.dist-info/METADATA,sha256=g__8aYidDAd_oKi-MQ7PLGX0Ss63NHRbxjudCfdGtF8,63473
13
+ well_log_toolkit-0.1.149.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
+ well_log_toolkit-0.1.149.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.149.dist-info/RECORD,,