well-log-toolkit 0.1.144__py3-none-any.whl → 0.1.146__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:
@@ -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,
@@ -2071,14 +2098,12 @@ class Property(PropertyOperationsMixin):
2071
2098
  count = int(np.sum(val_mask))
2072
2099
  fraction = thickness / gross_thickness if gross_thickness > 0 else 0.0
2073
2100
 
2074
- # Determine the key and label
2101
+ # Determine the key (use label if available, otherwise name_code)
2075
2102
  int_val = int(val)
2076
2103
  if self.labels is not None and int_val in self.labels:
2077
2104
  key = self.labels[int_val]
2078
- label = self.labels[int_val]
2079
2105
  else:
2080
2106
  key = f"{self.name}_{int_val}"
2081
- label = None
2082
2107
 
2083
2108
  stats = {
2084
2109
  'code': int_val,
@@ -2091,8 +2116,6 @@ class Property(PropertyOperationsMixin):
2091
2116
  'min': round(float(np.min(val_depths)), precision),
2092
2117
  'max': round(float(np.max(val_depths)), precision)
2093
2118
  }
2094
- if label is not None:
2095
- stats['label'] = label
2096
2119
 
2097
2120
  result[key] = stats
2098
2121
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.144
3
+ Version: 0.1.146
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=vT5-W21PS0XU5M4dSaueqpo2p8pK_tYe47qEVOgxWsQ,117930
5
5
  well_log_toolkit/operations.py,sha256=z8j8fGBOwoJGUQFy-Vawjq9nm3OD_dUt0oaNh8yuG7o,18515
6
- well_log_toolkit/property.py,sha256=4g5-_WRdJ9HwDKkufU4s_oOnCh6Deg58ZX5jE9Uwx2c,99833
6
+ well_log_toolkit/property.py,sha256=O5Ti5ahWV3CTlBLGZ-ntEIed6GGyzsxnyO_EbYrNLP0,100752
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.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,,
12
+ well_log_toolkit-0.1.146.dist-info/METADATA,sha256=s_S72extRI5wIXZOZ4IIRFkmQPo-Gw5i2udBow2fRG4,63473
13
+ well_log_toolkit-0.1.146.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
+ well_log_toolkit-0.1.146.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.146.dist-info/RECORD,,