well-log-toolkit 0.1.145__py3-none-any.whl → 0.1.147__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.
@@ -132,11 +132,12 @@ class _ManagerPropertyProxy:
132
132
  When assigned to a manager attribute, the operation is broadcast to all wells.
133
133
  """
134
134
 
135
- def __init__(self, manager: 'WellDataManager', property_name: str, operation=None, filters=None):
135
+ def __init__(self, manager: 'WellDataManager', property_name: str, operation=None, filters=None, custom_intervals=None):
136
136
  self._manager = manager
137
137
  self._property_name = property_name
138
138
  self._operation = operation # Function to apply to each property
139
139
  self._filters = filters or [] # List of (filter_name, insert_boundaries) tuples
140
+ self._custom_intervals = custom_intervals # For filter_intervals: str (saved name) or dict (well-specific)
140
141
 
141
142
  def _apply_operation(self, prop: Property):
142
143
  """Apply stored operation to a property."""
@@ -147,9 +148,50 @@ class _ManagerPropertyProxy:
147
148
  # Apply the operation
148
149
  return self._operation(prop)
149
150
 
151
+ def _apply_filter_intervals(self, prop: Property, well):
152
+ """
153
+ Apply filter_intervals to a property if custom_intervals is set.
154
+
155
+ Returns None if the well doesn't have the required saved intervals.
156
+ """
157
+ if not self._custom_intervals:
158
+ return prop
159
+
160
+ intervals_config = self._custom_intervals
161
+ intervals = intervals_config['intervals']
162
+ name = intervals_config['name']
163
+ insert_boundaries = intervals_config['insert_boundaries']
164
+ save = intervals_config['save']
165
+
166
+ # Resolve intervals for this well
167
+ if isinstance(intervals, str):
168
+ # Saved filter name - check if this well has it
169
+ if intervals not in well._saved_filter_intervals:
170
+ return None # Skip wells that don't have this saved filter
171
+ well_intervals = intervals
172
+ elif isinstance(intervals, dict):
173
+ # Well-specific intervals
174
+ well_intervals = None
175
+ if well.name in intervals:
176
+ well_intervals = intervals[well.name]
177
+ elif well.sanitized_name in intervals:
178
+ well_intervals = intervals[well.sanitized_name]
179
+ if well_intervals is None:
180
+ return None # Skip wells not in the dict
181
+ else:
182
+ return None
183
+
184
+ # Apply filter_intervals
185
+ return prop.filter_intervals(
186
+ well_intervals,
187
+ name=name,
188
+ insert_boundaries=insert_boundaries,
189
+ save=save
190
+ )
191
+
150
192
  def _create_proxy_with_operation(self, operation):
151
193
  """Create a new proxy with an operation."""
152
- return _ManagerPropertyProxy(self._manager, self._property_name, operation, self._filters)
194
+ return _ManagerPropertyProxy(self._manager, self._property_name, operation, self._filters, self._custom_intervals)
153
195
 
154
196
  def _extract_statistic_from_grouped(self, grouped_result: dict, stat_name: str, **kwargs) -> dict:
155
197
  """
@@ -1115,7 +1157,141 @@ class _ManagerPropertyProxy:
1115
1157
  new_filters = self._filters + [(property_name, insert_boundaries)]
1116
1158
 
1117
1159
  # Return new proxy with filter added
1118
- return _ManagerPropertyProxy(self._manager, self._property_name, self._operation, new_filters)
1160
+ return _ManagerPropertyProxy(self._manager, self._property_name, self._operation, new_filters, self._custom_intervals)
1161
+
1162
+ def filter_intervals(
1163
+ self,
1164
+ intervals: Union[str, dict],
1165
+ name: str = "Custom_Intervals",
1166
+ insert_boundaries: Optional[bool] = None,
1167
+ save: Optional[str] = None
1168
+ ) -> '_ManagerPropertyProxy':
1169
+ """
1170
+ Filter by custom depth intervals across all wells.
1171
+
1172
+ Parameters
1173
+ ----------
1174
+ intervals : str | dict
1175
+ - str: Name of saved filter intervals (looks up per-well)
1176
+ - dict: Well-specific intervals {well_name: [intervals]}
1177
+ name : str, default "Custom_Intervals"
1178
+ Name for the filter property (used in output labels)
1179
+ insert_boundaries : bool, optional
1180
+ If True, insert synthetic samples at interval boundaries.
1181
+ save : str, optional
1182
+ If provided, save the intervals to the well(s) under this name.
1183
+
1184
+ Returns
1185
+ -------
1186
+ _ManagerPropertyProxy
1187
+ New proxy with intervals filter added
1188
+
1189
+ Examples
1190
+ --------
1191
+ >>> # Use saved intervals (only wells with saved intervals are included)
1192
+ >>> manager.Facies.filter_intervals("Reservoir_Zones").discrete_summary()
1193
+
1194
+ >>> # Well-specific intervals
1195
+ >>> manager.Facies.filter_intervals({
1196
+ ... "well_A": [{"name": "Zone1", "top": 2500, "base": 2700}],
1197
+ ... "well_B": [{"name": "Zone1", "top": 2600, "base": 2800}]
1198
+ ... }).discrete_summary()
1199
+ """
1200
+ # Store intervals config for use when computing stats
1201
+ intervals_config = {
1202
+ 'intervals': intervals,
1203
+ 'name': name,
1204
+ 'insert_boundaries': insert_boundaries,
1205
+ 'save': save
1206
+ }
1207
+
1208
+ return _ManagerPropertyProxy(
1209
+ self._manager, self._property_name, self._operation,
1210
+ self._filters, intervals_config
1211
+ )
1212
+
1213
+ def discrete_summary(
1214
+ self,
1215
+ precision: int = 6,
1216
+ skip: Optional[list] = None
1217
+ ) -> dict:
1218
+ """
1219
+ Compute discrete summary statistics across all wells.
1220
+
1221
+ Parameters
1222
+ ----------
1223
+ precision : int, default 6
1224
+ Number of decimal places for rounding numeric results
1225
+ skip : list[str], optional
1226
+ List of field names to exclude from the output.
1227
+ Valid fields: 'code', 'count', 'thickness', 'fraction', 'depth_range'
1228
+
1229
+ Returns
1230
+ -------
1231
+ dict
1232
+ Nested dictionary with structure:
1233
+ {
1234
+ "well_name": {
1235
+ "zone_name": {
1236
+ "depth_range": {...},
1237
+ "thickness": ...,
1238
+ "facies": {...}
1239
+ }
1240
+ }
1241
+ }
1242
+
1243
+ Examples
1244
+ --------
1245
+ >>> # Use saved intervals
1246
+ >>> manager.Facies.filter_intervals("Reservoir_Zones").discrete_summary()
1247
+
1248
+ >>> # Skip certain fields
1249
+ >>> manager.Facies.filter_intervals("Zones").discrete_summary(skip=["code", "count"])
1250
+ """
1251
+ if not self._custom_intervals:
1252
+ raise ValueError(
1253
+ "discrete_summary() requires filter_intervals(). "
1254
+ "Use .filter_intervals('saved_name') or .filter_intervals({...}) first."
1255
+ )
1256
+
1257
+ result = {}
1258
+
1259
+ for well_name, well in self._manager._wells.items():
1260
+ well_result = self._compute_discrete_summary_for_well(well, precision, skip)
1261
+ if well_result is not None:
1262
+ result[well_name] = well_result
1263
+
1264
+ return _sanitize_for_json(result)
1265
+
1266
+ def _compute_discrete_summary_for_well(
1267
+ self,
1268
+ well,
1269
+ precision: int,
1270
+ skip: Optional[list]
1271
+ ):
1272
+ """
1273
+ Helper to compute discrete_summary for a property in a well.
1274
+ """
1275
+ try:
1276
+ prop = well.get_property(self._property_name)
1277
+ prop = self._apply_operation(prop)
1278
+
1279
+ # Apply filter_intervals
1280
+ prop = self._apply_filter_intervals(prop, well)
1281
+ if prop is None:
1282
+ return None # Well doesn't have the saved intervals
1283
+
1284
+ # Apply any additional filters
1285
+ for filter_name, filter_insert_boundaries in self._filters:
1286
+ if filter_insert_boundaries is not None:
1287
+ prop = prop.filter(filter_name, insert_boundaries=filter_insert_boundaries)
1288
+ else:
1289
+ prop = prop.filter(filter_name)
1290
+
1291
+ return prop.discrete_summary(precision=precision, skip=skip)
1292
+
1293
+ except (PropertyNotFoundError, PropertyTypeError, AttributeError, KeyError, ValueError):
1294
+ return None
1119
1295
 
1120
1296
  def sums_avg(
1121
1297
  self,
@@ -1206,11 +1382,19 @@ class _ManagerPropertyProxy:
1206
1382
  >>> # "core": {"Zone_1": {...}}
1207
1383
  >>> # }
1208
1384
  >>> # }
1385
+
1386
+ >>> # With custom intervals
1387
+ >>> manager.PHIE.filter_intervals("Reservoir_Zones").sums_avg()
1388
+ >>> # Returns:
1389
+ >>> # {
1390
+ >>> # "well_A": {"Zone_1": {"mean": 0.18, ...}},
1391
+ >>> # "well_B": {"Zone_1": {"mean": 0.21, ...}}
1392
+ >>> # }
1209
1393
  """
1210
- if not self._filters:
1394
+ if not self._filters and not self._custom_intervals:
1211
1395
  raise ValueError(
1212
- "sums_avg() requires at least one filter. "
1213
- "Use .filter('property_name') before calling sums_avg()"
1396
+ "sums_avg() requires at least one filter or filter_intervals(). "
1397
+ "Use .filter('property_name') or .filter_intervals(...) before calling sums_avg()"
1214
1398
  )
1215
1399
 
1216
1400
  result = {}
@@ -1247,6 +1431,11 @@ class _ManagerPropertyProxy:
1247
1431
  prop = well.get_property(self._property_name, source=source_name)
1248
1432
  prop = self._apply_operation(prop)
1249
1433
 
1434
+ # Apply filter_intervals if set
1435
+ prop = self._apply_filter_intervals(prop, well)
1436
+ if prop is None:
1437
+ continue # Well doesn't have the saved intervals
1438
+
1250
1439
  # Apply all filters (specify source to avoid ambiguity)
1251
1440
  # If a filter doesn't exist, PropertyNotFoundError will be raised and caught below
1252
1441
  for filter_name, insert_boundaries in self._filters:
@@ -1274,6 +1463,11 @@ class _ManagerPropertyProxy:
1274
1463
  prop = well.get_property(self._property_name)
1275
1464
  prop = self._apply_operation(prop)
1276
1465
 
1466
+ # Apply filter_intervals if set
1467
+ prop = self._apply_filter_intervals(prop, well)
1468
+ if prop is None:
1469
+ return None # Well doesn't have the saved intervals
1470
+
1277
1471
  # Apply all filters
1278
1472
  for filter_name, insert_boundaries in self._filters:
1279
1473
  if insert_boundaries is not None:
@@ -1299,6 +1493,11 @@ class _ManagerPropertyProxy:
1299
1493
  prop = well.get_property(self._property_name, source=source_name)
1300
1494
  prop = self._apply_operation(prop)
1301
1495
 
1496
+ # Apply filter_intervals if set
1497
+ prop = self._apply_filter_intervals(prop, well)
1498
+ if prop is None:
1499
+ continue # Well doesn't have the saved intervals
1500
+
1302
1501
  # Apply all filters (specify source to avoid ambiguity)
1303
1502
  # If a filter doesn't exist, PropertyNotFoundError will be raised and caught below
1304
1503
  for filter_name, insert_boundaries in self._filters:
@@ -1419,6 +1618,346 @@ class _ManagerPropertyProxy:
1419
1618
  )
1420
1619
 
1421
1620
 
1621
+ class _ManagerMultiPropertyProxy:
1622
+ """
1623
+ Proxy for computing statistics across multiple properties on all wells.
1624
+
1625
+ Supports filter(), filter_intervals(), and sums_avg() methods.
1626
+ Multi-property results nest property-specific stats under property names
1627
+ while keeping common stats (depth_range, samples, thickness, etc.) at
1628
+ the group level.
1629
+ """
1630
+
1631
+ # Stats that are specific to each property (nested under property name)
1632
+ PROPERTY_STATS = {'mean', 'median', 'mode', 'sum', 'std_dev', 'percentile', 'range'}
1633
+
1634
+ # Stats that are common across properties (stay at group level)
1635
+ COMMON_STATS = {'depth_range', 'samples', 'thickness', 'gross_thickness', 'thickness_fraction', 'calculation'}
1636
+
1637
+ def __init__(
1638
+ self,
1639
+ manager: 'WellDataManager',
1640
+ property_names: list[str],
1641
+ filters: Optional[list[tuple]] = None,
1642
+ custom_intervals: Optional[dict] = None
1643
+ ):
1644
+ self._manager = manager
1645
+ self._property_names = property_names
1646
+ self._filters = filters or []
1647
+ self._custom_intervals = custom_intervals
1648
+
1649
+ def filter(
1650
+ self,
1651
+ property_name: str,
1652
+ insert_boundaries: Optional[bool] = None
1653
+ ) -> '_ManagerMultiPropertyProxy':
1654
+ """
1655
+ Add a filter (discrete property) to group statistics by.
1656
+
1657
+ Parameters
1658
+ ----------
1659
+ property_name : str
1660
+ Name of discrete property to group by
1661
+ insert_boundaries : bool, optional
1662
+ Whether to insert boundary values at filter transitions
1663
+
1664
+ Returns
1665
+ -------
1666
+ _ManagerMultiPropertyProxy
1667
+ New proxy with filter added
1668
+ """
1669
+ new_filters = self._filters + [(property_name, insert_boundaries)]
1670
+ return _ManagerMultiPropertyProxy(
1671
+ self._manager, self._property_names, new_filters, self._custom_intervals
1672
+ )
1673
+
1674
+ def filter_intervals(
1675
+ self,
1676
+ intervals: Union[str, list, dict],
1677
+ name: str = "Custom_Intervals",
1678
+ insert_boundaries: Optional[bool] = None,
1679
+ save: Optional[str] = None
1680
+ ) -> '_ManagerMultiPropertyProxy':
1681
+ """
1682
+ Filter by custom depth intervals.
1683
+
1684
+ Parameters
1685
+ ----------
1686
+ intervals : str, list, or dict
1687
+ - str: Name of saved intervals to retrieve from each well
1688
+ - list: List of interval dicts [{"name": "Zone_A", "top": 2500, "base": 2700}, ...]
1689
+ - dict: Well-specific intervals {"well_name": [...], ...}
1690
+ name : str, default "Custom_Intervals"
1691
+ Name for the interval filter in results
1692
+ insert_boundaries : bool, optional
1693
+ Whether to insert boundary values at interval edges
1694
+ save : str, optional
1695
+ If provided, save intervals to wells with this name
1696
+
1697
+ Returns
1698
+ -------
1699
+ _ManagerMultiPropertyProxy
1700
+ New proxy with custom intervals set
1701
+ """
1702
+ intervals_config = {
1703
+ 'intervals': intervals,
1704
+ 'name': name,
1705
+ 'insert_boundaries': insert_boundaries,
1706
+ 'save': save
1707
+ }
1708
+ return _ManagerMultiPropertyProxy(
1709
+ self._manager, self._property_names, self._filters, intervals_config
1710
+ )
1711
+
1712
+ def sums_avg(
1713
+ self,
1714
+ weighted: Optional[bool] = None,
1715
+ arithmetic: Optional[bool] = None,
1716
+ precision: int = 6
1717
+ ) -> dict:
1718
+ """
1719
+ Compute statistics for multiple properties across all wells.
1720
+
1721
+ Multi-property results nest property-specific stats (mean, median, etc.)
1722
+ under each property name, while common stats (depth_range, samples,
1723
+ thickness, etc.) remain at the group level.
1724
+
1725
+ Parameters
1726
+ ----------
1727
+ weighted : bool, optional
1728
+ Include depth-weighted statistics.
1729
+ Default: True for continuous/discrete, False for sampled
1730
+ arithmetic : bool, optional
1731
+ Include arithmetic (unweighted) statistics.
1732
+ Default: False for continuous/discrete, True for sampled
1733
+ precision : int, default 6
1734
+ Number of decimal places for rounding numeric results
1735
+
1736
+ Returns
1737
+ -------
1738
+ dict
1739
+ Nested dictionary with structure:
1740
+ {
1741
+ "well_name": {
1742
+ "interval_name": { # if using filter_intervals
1743
+ "filter_value": {
1744
+ "PropertyA": {"mean": ..., "median": ..., ...},
1745
+ "PropertyB": {"mean": ..., "median": ..., ...},
1746
+ "depth_range": {...},
1747
+ "samples": ...,
1748
+ "thickness": ...,
1749
+ ...
1750
+ }
1751
+ }
1752
+ }
1753
+ }
1754
+
1755
+ Examples
1756
+ --------
1757
+ >>> manager.properties(['PHIE', 'PERM']).filter('Facies').sums_avg()
1758
+ >>> # Returns stats for both properties grouped by facies
1759
+
1760
+ >>> manager.properties(['PHIE', 'PERM']).filter_intervals("Zones").sums_avg()
1761
+ >>> # 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
+
1769
+ result = {}
1770
+
1771
+ for well_name, well in self._manager._wells.items():
1772
+ well_result = self._compute_sums_avg_for_well(
1773
+ well, weighted, arithmetic, precision
1774
+ )
1775
+ if well_result is not None:
1776
+ result[well_name] = well_result
1777
+
1778
+ return _sanitize_for_json(result)
1779
+
1780
+ def _compute_sums_avg_for_well(
1781
+ self,
1782
+ well,
1783
+ weighted: Optional[bool],
1784
+ arithmetic: Optional[bool],
1785
+ precision: int
1786
+ ):
1787
+ """
1788
+ Compute multi-property sums_avg for a single well.
1789
+ """
1790
+ # Collect results for each property
1791
+ property_results = {}
1792
+
1793
+ for prop_name in self._property_names:
1794
+ try:
1795
+ prop = well.get_property(prop_name)
1796
+
1797
+ # Apply filter_intervals if set
1798
+ if self._custom_intervals:
1799
+ prop = self._apply_filter_intervals(prop, well)
1800
+ if prop is None:
1801
+ return None # Well doesn't have the saved intervals
1802
+
1803
+ # Apply all filters
1804
+ for filter_name, insert_boundaries in self._filters:
1805
+ if insert_boundaries is not None:
1806
+ prop = prop.filter(filter_name, insert_boundaries=insert_boundaries)
1807
+ else:
1808
+ prop = prop.filter(filter_name)
1809
+
1810
+ # Compute sums_avg
1811
+ result = prop.sums_avg(
1812
+ weighted=weighted,
1813
+ arithmetic=arithmetic,
1814
+ precision=precision
1815
+ )
1816
+ property_results[prop_name] = result
1817
+
1818
+ except (PropertyNotFoundError, PropertyTypeError, AttributeError, KeyError, ValueError):
1819
+ # Property doesn't exist in this well, skip it
1820
+ pass
1821
+
1822
+ if not property_results:
1823
+ return None
1824
+
1825
+ # Merge results: nest property-specific stats, keep common stats at group level
1826
+ return self._merge_property_results(property_results)
1827
+
1828
+ def _apply_filter_intervals(self, prop, well):
1829
+ """
1830
+ Apply filter_intervals to a property if custom_intervals is set.
1831
+
1832
+ Returns None if the well doesn't have the required saved intervals.
1833
+ """
1834
+ if not self._custom_intervals:
1835
+ return prop
1836
+
1837
+ intervals_config = self._custom_intervals
1838
+ intervals = intervals_config['intervals']
1839
+ name = intervals_config['name']
1840
+ insert_boundaries = intervals_config['insert_boundaries']
1841
+ save = intervals_config['save']
1842
+
1843
+ # Resolve intervals for this well
1844
+ if isinstance(intervals, str):
1845
+ # Saved filter name - check if this well has it
1846
+ if intervals not in well._saved_filter_intervals:
1847
+ return None # Skip wells that don't have this saved filter
1848
+ well_intervals = intervals
1849
+ elif isinstance(intervals, dict):
1850
+ # Well-specific intervals
1851
+ well_intervals = None
1852
+ if well.name in intervals:
1853
+ well_intervals = intervals[well.name]
1854
+ elif well.sanitized_name in intervals:
1855
+ well_intervals = intervals[well.sanitized_name]
1856
+ if well_intervals is None:
1857
+ return None # Skip wells not in the dict
1858
+ elif isinstance(intervals, list):
1859
+ # Direct list of intervals
1860
+ well_intervals = intervals
1861
+ else:
1862
+ return None
1863
+
1864
+ # Apply filter_intervals
1865
+ return prop.filter_intervals(
1866
+ well_intervals,
1867
+ name=name,
1868
+ insert_boundaries=insert_boundaries,
1869
+ save=save
1870
+ )
1871
+
1872
+ def _merge_property_results(self, property_results: dict) -> dict:
1873
+ """
1874
+ Merge results from multiple properties.
1875
+
1876
+ Nests property-specific stats under property names while keeping
1877
+ common stats at the group level.
1878
+
1879
+ Parameters
1880
+ ----------
1881
+ property_results : dict
1882
+ {property_name: sums_avg_result}
1883
+
1884
+ Returns
1885
+ -------
1886
+ dict
1887
+ Merged result with structure:
1888
+ {
1889
+ "group_value": {
1890
+ "PropertyA": {"mean": ..., ...},
1891
+ "PropertyB": {"mean": ..., ...},
1892
+ "depth_range": {...},
1893
+ "samples": ...,
1894
+ ...
1895
+ }
1896
+ }
1897
+ """
1898
+ if not property_results:
1899
+ return {}
1900
+
1901
+ # Use first property result as the structure template
1902
+ first_prop = next(iter(property_results.keys()))
1903
+ first_result = property_results[first_prop]
1904
+
1905
+ return self._merge_recursive(property_results, first_result)
1906
+
1907
+ def _merge_recursive(self, property_results: dict, template: dict) -> dict:
1908
+ """
1909
+ Recursively merge property results following the template structure.
1910
+ """
1911
+ result = {}
1912
+
1913
+ for key, value in template.items():
1914
+ if isinstance(value, dict):
1915
+ # Check if this is a stats dict (has property-specific keys)
1916
+ if any(k in value for k in self.PROPERTY_STATS):
1917
+ # This is a leaf stats dict - merge property stats here
1918
+ merged = {}
1919
+
1920
+ # Add property-specific stats for each property
1921
+ for prop_name, prop_result in property_results.items():
1922
+ # Navigate to the same key in this property's result
1923
+ prop_value = self._get_nested_value(prop_result, key)
1924
+ if prop_value and isinstance(prop_value, dict):
1925
+ # Extract property-specific stats
1926
+ prop_stats = {
1927
+ k: v for k, v in prop_value.items()
1928
+ if k in self.PROPERTY_STATS
1929
+ }
1930
+ if prop_stats:
1931
+ merged[prop_name] = prop_stats
1932
+
1933
+ # Add common stats from the first property
1934
+ for k, v in value.items():
1935
+ if k in self.COMMON_STATS:
1936
+ merged[k] = v
1937
+
1938
+ result[key] = merged
1939
+ else:
1940
+ # This is an intermediate nesting level - recurse
1941
+ # Collect corresponding sub-dicts from all properties
1942
+ sub_property_results = {}
1943
+ for prop_name, prop_result in property_results.items():
1944
+ prop_value = self._get_nested_value(prop_result, key)
1945
+ if prop_value and isinstance(prop_value, dict):
1946
+ sub_property_results[prop_name] = prop_value
1947
+
1948
+ if sub_property_results:
1949
+ result[key] = self._merge_recursive(sub_property_results, value)
1950
+ else:
1951
+ # Non-dict value, just copy from template
1952
+ result[key] = value
1953
+
1954
+ return result
1955
+
1956
+ def _get_nested_value(self, d: dict, key: str):
1957
+ """Get value from dict, returning None if key doesn't exist."""
1958
+ return d.get(key) if isinstance(d, dict) else None
1959
+
1960
+
1422
1961
  class WellDataManager:
1423
1962
  """
1424
1963
  Global orchestrator for multi-well analysis.
@@ -1511,6 +2050,60 @@ class WellDataManager:
1511
2050
  # Return a proxy that can be used for operations across all wells
1512
2051
  return _ManagerPropertyProxy(self, name)
1513
2052
 
2053
+ def properties(self, property_names: list[str]) -> _ManagerMultiPropertyProxy:
2054
+ """
2055
+ Create a multi-property proxy for computing statistics across multiple properties.
2056
+
2057
+ This allows computing statistics for multiple properties at once, with
2058
+ property-specific stats (mean, median, etc.) nested under property names
2059
+ and common stats (depth_range, samples, thickness, etc.) at the group level.
2060
+
2061
+ Parameters
2062
+ ----------
2063
+ property_names : list[str]
2064
+ List of property names to include in statistics
2065
+
2066
+ Returns
2067
+ -------
2068
+ _ManagerMultiPropertyProxy
2069
+ Proxy that supports filter(), filter_intervals(), and sums_avg()
2070
+
2071
+ Examples
2072
+ --------
2073
+ >>> # Compute stats for multiple properties grouped by facies
2074
+ >>> manager.properties(['PHIE', 'PERM']).filter('Facies').sums_avg()
2075
+ >>> # Returns:
2076
+ >>> # {
2077
+ >>> # "well_A": {
2078
+ >>> # "Sand": {
2079
+ >>> # "PHIE": {"mean": 0.18, "median": 0.17, ...},
2080
+ >>> # "PERM": {"mean": 150, "median": 120, ...},
2081
+ >>> # "depth_range": {...},
2082
+ >>> # "samples": 387,
2083
+ >>> # "thickness": 29.4,
2084
+ >>> # ...
2085
+ >>> # }
2086
+ >>> # }
2087
+ >>> # }
2088
+
2089
+ >>> # With custom intervals
2090
+ >>> manager.properties(['PHIE', 'PERM']).filter('Facies').filter_intervals("Zones").sums_avg()
2091
+ >>> # Returns:
2092
+ >>> # {
2093
+ >>> # "well_A": {
2094
+ >>> # "Zone_1": {
2095
+ >>> # "Sand": {
2096
+ >>> # "PHIE": {"mean": 0.18, ...},
2097
+ >>> # "PERM": {"mean": 150, ...},
2098
+ >>> # "depth_range": {...},
2099
+ >>> # ...
2100
+ >>> # }
2101
+ >>> # }
2102
+ >>> # }
2103
+ >>> # }
2104
+ """
2105
+ return _ManagerMultiPropertyProxy(self, property_names)
2106
+
1514
2107
  def load_las(
1515
2108
  self,
1516
2109
  filepath: Union[str, Path, list[Union[str, Path]]],
@@ -1165,6 +1165,10 @@ class Property(PropertyOperationsMixin):
1165
1165
  new_prop._original_sample_count = len(self.depth)
1166
1166
  new_prop._boundary_samples_inserted = len(new_depth) - len(self.depth)
1167
1167
 
1168
+ # Preserve custom intervals if they exist (from filter_intervals)
1169
+ if hasattr(self, '_custom_intervals') and self._custom_intervals:
1170
+ new_prop._custom_intervals = self._custom_intervals
1171
+
1168
1172
  return new_prop
1169
1173
 
1170
1174
  def filter_intervals(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.145
3
+ Version: 0.1.147
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=VIARJLkYhxqxgTqfVfAAZU6AVsAPkQWPOUE6RNGnIdY,110558
4
+ well_log_toolkit/manager.py,sha256=WQHzNOXj7jVLeQlWG_uqfd93qemi5Mbe__z9NQRqk_Y,132563
5
5
  well_log_toolkit/operations.py,sha256=z8j8fGBOwoJGUQFy-Vawjq9nm3OD_dUt0oaNh8yuG7o,18515
6
- well_log_toolkit/property.py,sha256=O5Ti5ahWV3CTlBLGZ-ntEIed6GGyzsxnyO_EbYrNLP0,100752
6
+ well_log_toolkit/property.py,sha256=GsiD9c4SfBw8ar7ZJXS0NNejPlpvFRHKck_eBR2lLmo,100965
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.145.dist-info/METADATA,sha256=5HaBS3lhnirT8Uu8SEvjFn60HRdG8Ft5sa-DUhl94yU,63473
13
- well_log_toolkit-0.1.145.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
- well_log_toolkit-0.1.145.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.145.dist-info/RECORD,,
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,,