well-log-toolkit 0.1.142__py3-none-any.whl → 0.1.144__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- well_log_toolkit/property.py +361 -9
- well_log_toolkit/well.py +54 -1
- {well_log_toolkit-0.1.142.dist-info → well_log_toolkit-0.1.144.dist-info}/METADATA +69 -1
- {well_log_toolkit-0.1.142.dist-info → well_log_toolkit-0.1.144.dist-info}/RECORD +6 -6
- {well_log_toolkit-0.1.142.dist-info → well_log_toolkit-0.1.144.dist-info}/WHEEL +0 -0
- {well_log_toolkit-0.1.142.dist-info → well_log_toolkit-0.1.144.dist-info}/top_level.txt +0 -0
well_log_toolkit/property.py
CHANGED
|
@@ -1167,6 +1167,220 @@ class Property(PropertyOperationsMixin):
|
|
|
1167
1167
|
|
|
1168
1168
|
return new_prop
|
|
1169
1169
|
|
|
1170
|
+
def filter_intervals(
|
|
1171
|
+
self,
|
|
1172
|
+
intervals: Union[list[dict], dict[str, list[dict]], str],
|
|
1173
|
+
name: str = "Custom_Intervals",
|
|
1174
|
+
insert_boundaries: Optional[bool] = None,
|
|
1175
|
+
save: Optional[str] = None
|
|
1176
|
+
) -> 'Property':
|
|
1177
|
+
"""
|
|
1178
|
+
Filter by custom depth intervals defined as top/base pairs.
|
|
1179
|
+
|
|
1180
|
+
Each interval is processed independently, allowing overlapping intervals
|
|
1181
|
+
where the same depths can be counted in multiple zones.
|
|
1182
|
+
|
|
1183
|
+
Parameters
|
|
1184
|
+
----------
|
|
1185
|
+
intervals : list[dict] | dict[str, list[dict]] | str
|
|
1186
|
+
Interval definitions. Can be:
|
|
1187
|
+
- list[dict]: Direct list of intervals for the current well
|
|
1188
|
+
- dict[str, list[dict]]: Well-specific intervals keyed by well name.
|
|
1189
|
+
Current well must be included or raises error.
|
|
1190
|
+
- str: Name of a previously saved filter to use
|
|
1191
|
+
name : str, default "Custom_Intervals"
|
|
1192
|
+
Name for the filter property (used in output labels)
|
|
1193
|
+
insert_boundaries : bool, optional
|
|
1194
|
+
If True, insert synthetic samples at interval boundaries.
|
|
1195
|
+
Default is True for continuous properties, False for sampled properties.
|
|
1196
|
+
save : str, optional
|
|
1197
|
+
If provided, save the intervals to the well(s) under this name.
|
|
1198
|
+
Overwrites any existing filter with the same name.
|
|
1199
|
+
|
|
1200
|
+
Returns
|
|
1201
|
+
-------
|
|
1202
|
+
Property
|
|
1203
|
+
New property instance with custom intervals as filter dimension
|
|
1204
|
+
|
|
1205
|
+
Examples
|
|
1206
|
+
--------
|
|
1207
|
+
>>> # Filter by custom zones
|
|
1208
|
+
>>> intervals = [
|
|
1209
|
+
... {"name": "Zone_A", "top": 2500, "base": 2600},
|
|
1210
|
+
... {"name": "Zone_B", "top": 2600, "base": 2750},
|
|
1211
|
+
... ]
|
|
1212
|
+
>>> filtered = well.PHIE.filter_intervals(intervals)
|
|
1213
|
+
>>> filtered.sums_avg()
|
|
1214
|
+
|
|
1215
|
+
>>> # Save intervals for reuse
|
|
1216
|
+
>>> well.PHIE.filter_intervals(intervals, save="Reservoir_Zones")
|
|
1217
|
+
>>> # Later, use saved filter by name
|
|
1218
|
+
>>> well.PHIE.filter_intervals("Reservoir_Zones").sums_avg()
|
|
1219
|
+
|
|
1220
|
+
>>> # Save different intervals for multiple wells
|
|
1221
|
+
>>> manager.well_A.PHIE.filter_intervals({
|
|
1222
|
+
... "well_A": intervals_a,
|
|
1223
|
+
... "well_B": intervals_b
|
|
1224
|
+
... }, save="My_Zones")
|
|
1225
|
+
>>> # Now both wells have "My_Zones" saved
|
|
1226
|
+
|
|
1227
|
+
>>> # Overlapping intervals - each calculated independently
|
|
1228
|
+
>>> intervals = [
|
|
1229
|
+
... {"name": "Full_Reservoir", "top": 2500, "base": 2800},
|
|
1230
|
+
... {"name": "Upper_Only", "top": 2500, "base": 2650}
|
|
1231
|
+
... ]
|
|
1232
|
+
>>> # Depths 2500-2650 will be counted in BOTH zones
|
|
1233
|
+
|
|
1234
|
+
Notes
|
|
1235
|
+
-----
|
|
1236
|
+
Intervals can overlap or have gaps. Depths outside all intervals
|
|
1237
|
+
are excluded from statistics. Overlapping intervals are calculated
|
|
1238
|
+
independently - the same depths can contribute to multiple zones.
|
|
1239
|
+
"""
|
|
1240
|
+
# Handle string input (saved filter name)
|
|
1241
|
+
if isinstance(intervals, str):
|
|
1242
|
+
filter_name = intervals
|
|
1243
|
+
if self.parent_well is None:
|
|
1244
|
+
raise PropertyNotFoundError(
|
|
1245
|
+
f"Cannot use saved filter '{filter_name}': no parent well reference."
|
|
1246
|
+
)
|
|
1247
|
+
if filter_name not in self.parent_well._saved_filter_intervals:
|
|
1248
|
+
available = list(self.parent_well._saved_filter_intervals.keys())
|
|
1249
|
+
raise PropertyNotFoundError(
|
|
1250
|
+
f"Saved filter '{filter_name}' not found in well '{self.parent_well.name}'. "
|
|
1251
|
+
f"Available filters: {available if available else 'none'}"
|
|
1252
|
+
)
|
|
1253
|
+
intervals = self.parent_well._saved_filter_intervals[filter_name]
|
|
1254
|
+
# Use filter name as the output name if not overridden
|
|
1255
|
+
if name == "Custom_Intervals":
|
|
1256
|
+
name = filter_name
|
|
1257
|
+
|
|
1258
|
+
# Handle dict input (well-specific intervals)
|
|
1259
|
+
elif isinstance(intervals, dict):
|
|
1260
|
+
if self.parent_well is None:
|
|
1261
|
+
raise PropertyNotFoundError(
|
|
1262
|
+
"Cannot use well-specific intervals: no parent well reference."
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
well_name = self.parent_well.name
|
|
1266
|
+
sanitized_name = self.parent_well.sanitized_name
|
|
1267
|
+
|
|
1268
|
+
# Check if current well is in the dict (by name or sanitized name)
|
|
1269
|
+
current_well_intervals = [] # Default to empty if not found
|
|
1270
|
+
if well_name in intervals:
|
|
1271
|
+
current_well_intervals = intervals[well_name]
|
|
1272
|
+
elif sanitized_name in intervals:
|
|
1273
|
+
current_well_intervals = intervals[sanitized_name]
|
|
1274
|
+
|
|
1275
|
+
# Save to all specified wells if save parameter provided
|
|
1276
|
+
if save and self.parent_well.parent_manager:
|
|
1277
|
+
manager = self.parent_well.parent_manager
|
|
1278
|
+
for key, well_intervals in intervals.items():
|
|
1279
|
+
# Find well by name or sanitized name
|
|
1280
|
+
target_well = None
|
|
1281
|
+
for w in manager._wells.values():
|
|
1282
|
+
if w.name == key or w.sanitized_name == key:
|
|
1283
|
+
target_well = w
|
|
1284
|
+
break
|
|
1285
|
+
if target_well:
|
|
1286
|
+
self._validate_intervals(well_intervals)
|
|
1287
|
+
target_well._saved_filter_intervals[save] = well_intervals
|
|
1288
|
+
|
|
1289
|
+
intervals = current_well_intervals
|
|
1290
|
+
|
|
1291
|
+
# Validate and save if we have intervals
|
|
1292
|
+
if intervals:
|
|
1293
|
+
# Validate interval structure
|
|
1294
|
+
self._validate_intervals(intervals)
|
|
1295
|
+
|
|
1296
|
+
# Save to current well if save parameter provided
|
|
1297
|
+
if save and self.parent_well:
|
|
1298
|
+
self.parent_well._saved_filter_intervals[save] = intervals
|
|
1299
|
+
|
|
1300
|
+
# Determine if we should insert boundaries
|
|
1301
|
+
if insert_boundaries is None:
|
|
1302
|
+
insert_boundaries = self.type != 'sampled'
|
|
1303
|
+
|
|
1304
|
+
# Collect all boundary depths from intervals for boundary insertion
|
|
1305
|
+
if insert_boundaries and intervals:
|
|
1306
|
+
boundary_depths = []
|
|
1307
|
+
for interval in intervals:
|
|
1308
|
+
boundary_depths.append(float(interval['top']))
|
|
1309
|
+
boundary_depths.append(float(interval['base']))
|
|
1310
|
+
boundary_depths = np.unique(boundary_depths)
|
|
1311
|
+
|
|
1312
|
+
# Create a temporary discrete property just for boundary insertion
|
|
1313
|
+
# Values don't matter here, only the depths
|
|
1314
|
+
temp_discrete = Property(
|
|
1315
|
+
name=name,
|
|
1316
|
+
depth=boundary_depths,
|
|
1317
|
+
values=np.arange(len(boundary_depths), dtype=float),
|
|
1318
|
+
parent_well=self.parent_well,
|
|
1319
|
+
prop_type='discrete'
|
|
1320
|
+
)
|
|
1321
|
+
new_depth, new_values, new_secondaries = self._insert_boundary_samples(temp_discrete)
|
|
1322
|
+
else:
|
|
1323
|
+
new_depth = self.depth.copy()
|
|
1324
|
+
new_values = self.values.copy()
|
|
1325
|
+
new_secondaries = [sp for sp in self.secondary_properties]
|
|
1326
|
+
|
|
1327
|
+
# Create new Property instance
|
|
1328
|
+
new_prop = Property(
|
|
1329
|
+
name=self.name,
|
|
1330
|
+
depth=new_depth,
|
|
1331
|
+
values=new_values,
|
|
1332
|
+
parent_well=self.parent_well,
|
|
1333
|
+
unit=self.unit,
|
|
1334
|
+
prop_type=self.type,
|
|
1335
|
+
description=self.description,
|
|
1336
|
+
null_value=-999.25,
|
|
1337
|
+
labels=self.labels,
|
|
1338
|
+
colors=self.colors,
|
|
1339
|
+
styles=self.styles,
|
|
1340
|
+
thicknesses=self.thicknesses,
|
|
1341
|
+
source_las=self.source_las,
|
|
1342
|
+
source_name=self.source_name,
|
|
1343
|
+
original_name=self.original_name
|
|
1344
|
+
)
|
|
1345
|
+
new_prop.secondary_properties = new_secondaries
|
|
1346
|
+
|
|
1347
|
+
# Store custom intervals for independent processing in sums_avg/discrete_summary
|
|
1348
|
+
new_prop._custom_intervals = intervals
|
|
1349
|
+
new_prop._custom_intervals_name = name
|
|
1350
|
+
|
|
1351
|
+
# Track filtering metadata
|
|
1352
|
+
new_prop._is_filtered = True
|
|
1353
|
+
new_prop._original_sample_count = len(self.depth)
|
|
1354
|
+
new_prop._boundary_samples_inserted = len(new_depth) - len(self.depth)
|
|
1355
|
+
|
|
1356
|
+
return new_prop
|
|
1357
|
+
|
|
1358
|
+
def _validate_intervals(self, intervals: list[dict]) -> None:
|
|
1359
|
+
"""
|
|
1360
|
+
Validate interval structure.
|
|
1361
|
+
|
|
1362
|
+
Parameters
|
|
1363
|
+
----------
|
|
1364
|
+
intervals : list[dict]
|
|
1365
|
+
List of interval definitions to validate
|
|
1366
|
+
|
|
1367
|
+
Raises
|
|
1368
|
+
------
|
|
1369
|
+
ValueError
|
|
1370
|
+
If any interval is invalid
|
|
1371
|
+
"""
|
|
1372
|
+
for i, interval in enumerate(intervals):
|
|
1373
|
+
if not isinstance(interval, dict):
|
|
1374
|
+
raise ValueError(f"Interval {i} must be a dict, got {type(interval)}")
|
|
1375
|
+
for key in ('name', 'top', 'base'):
|
|
1376
|
+
if key not in interval:
|
|
1377
|
+
raise ValueError(f"Interval {i} missing required key '{key}'")
|
|
1378
|
+
if interval['top'] >= interval['base']:
|
|
1379
|
+
raise ValueError(
|
|
1380
|
+
f"Interval '{interval['name']}': top ({interval['top']}) must be "
|
|
1381
|
+
f"less than base ({interval['base']})"
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1170
1384
|
def _insert_boundary_samples(
|
|
1171
1385
|
self,
|
|
1172
1386
|
discrete_prop: 'Property'
|
|
@@ -1500,6 +1714,16 @@ class Property(PropertyOperationsMixin):
|
|
|
1500
1714
|
valid_mask = ~np.isnan(self.values)
|
|
1501
1715
|
gross_thickness = float(np.sum(full_intervals[valid_mask]))
|
|
1502
1716
|
|
|
1717
|
+
# Check for custom intervals (from filter_intervals)
|
|
1718
|
+
# These are processed independently, allowing overlaps
|
|
1719
|
+
if hasattr(self, '_custom_intervals') and self._custom_intervals:
|
|
1720
|
+
return self._compute_stats_by_intervals(
|
|
1721
|
+
weighted=weighted,
|
|
1722
|
+
arithmetic=arithmetic,
|
|
1723
|
+
gross_thickness=gross_thickness,
|
|
1724
|
+
precision=precision
|
|
1725
|
+
)
|
|
1726
|
+
|
|
1503
1727
|
if not self.secondary_properties:
|
|
1504
1728
|
# No filters, simple statistics
|
|
1505
1729
|
return self._compute_stats(
|
|
@@ -1520,6 +1744,51 @@ class Property(PropertyOperationsMixin):
|
|
|
1520
1744
|
precision=precision
|
|
1521
1745
|
)
|
|
1522
1746
|
|
|
1747
|
+
def _compute_stats_by_intervals(
|
|
1748
|
+
self,
|
|
1749
|
+
weighted: bool,
|
|
1750
|
+
arithmetic: bool,
|
|
1751
|
+
gross_thickness: float,
|
|
1752
|
+
precision: int
|
|
1753
|
+
) -> dict:
|
|
1754
|
+
"""
|
|
1755
|
+
Compute statistics for each custom interval independently.
|
|
1756
|
+
|
|
1757
|
+
This allows overlapping intervals where the same depths can
|
|
1758
|
+
contribute to multiple zones.
|
|
1759
|
+
"""
|
|
1760
|
+
result = {}
|
|
1761
|
+
|
|
1762
|
+
for interval in self._custom_intervals:
|
|
1763
|
+
interval_name = interval['name']
|
|
1764
|
+
top = float(interval['top'])
|
|
1765
|
+
base = float(interval['base'])
|
|
1766
|
+
|
|
1767
|
+
# Create mask for this interval (top <= depth < base)
|
|
1768
|
+
interval_mask = (self.depth >= top) & (self.depth < base)
|
|
1769
|
+
|
|
1770
|
+
# If there are secondary properties, group within this interval
|
|
1771
|
+
if self.secondary_properties:
|
|
1772
|
+
result[interval_name] = self._recursive_group(
|
|
1773
|
+
0,
|
|
1774
|
+
interval_mask,
|
|
1775
|
+
weighted=weighted,
|
|
1776
|
+
arithmetic=arithmetic,
|
|
1777
|
+
gross_thickness=gross_thickness,
|
|
1778
|
+
precision=precision
|
|
1779
|
+
)
|
|
1780
|
+
else:
|
|
1781
|
+
# No secondary properties, compute stats directly for interval
|
|
1782
|
+
result[interval_name] = self._compute_stats(
|
|
1783
|
+
interval_mask,
|
|
1784
|
+
weighted=weighted,
|
|
1785
|
+
arithmetic=arithmetic,
|
|
1786
|
+
gross_thickness=gross_thickness,
|
|
1787
|
+
precision=precision
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1790
|
+
return result
|
|
1791
|
+
|
|
1523
1792
|
def discrete_summary(self, precision: int = 6) -> dict:
|
|
1524
1793
|
"""
|
|
1525
1794
|
Compute summary statistics for discrete/categorical properties.
|
|
@@ -1570,6 +1839,14 @@ class Property(PropertyOperationsMixin):
|
|
|
1570
1839
|
valid_mask = ~np.isnan(self.values)
|
|
1571
1840
|
gross_thickness = float(np.sum(full_intervals[valid_mask]))
|
|
1572
1841
|
|
|
1842
|
+
# Check for custom intervals (from filter_intervals)
|
|
1843
|
+
# These are processed independently, allowing overlaps
|
|
1844
|
+
if hasattr(self, '_custom_intervals') and self._custom_intervals:
|
|
1845
|
+
return self._compute_discrete_stats_by_intervals(
|
|
1846
|
+
gross_thickness=gross_thickness,
|
|
1847
|
+
precision=precision
|
|
1848
|
+
)
|
|
1849
|
+
|
|
1573
1850
|
if not self.secondary_properties:
|
|
1574
1851
|
# No filters, compute stats for all discrete values
|
|
1575
1852
|
return self._compute_discrete_stats(
|
|
@@ -1586,12 +1863,80 @@ class Property(PropertyOperationsMixin):
|
|
|
1586
1863
|
precision=precision
|
|
1587
1864
|
)
|
|
1588
1865
|
|
|
1866
|
+
def _compute_discrete_stats_by_intervals(
|
|
1867
|
+
self,
|
|
1868
|
+
gross_thickness: float,
|
|
1869
|
+
precision: int
|
|
1870
|
+
) -> dict:
|
|
1871
|
+
"""
|
|
1872
|
+
Compute discrete statistics for each custom interval independently.
|
|
1873
|
+
|
|
1874
|
+
This allows overlapping intervals where the same depths can
|
|
1875
|
+
contribute to multiple zones. Zone-level metadata (depth_range, thickness)
|
|
1876
|
+
is shown at the interval level, and fractions are relative to zone thickness.
|
|
1877
|
+
"""
|
|
1878
|
+
result = {}
|
|
1879
|
+
|
|
1880
|
+
for interval in self._custom_intervals:
|
|
1881
|
+
interval_name = interval['name']
|
|
1882
|
+
top = float(interval['top'])
|
|
1883
|
+
base = float(interval['base'])
|
|
1884
|
+
|
|
1885
|
+
# Create mask for this interval (top <= depth < base)
|
|
1886
|
+
interval_mask = (self.depth >= top) & (self.depth < base)
|
|
1887
|
+
|
|
1888
|
+
# Calculate zone thickness for fraction calculation
|
|
1889
|
+
full_intervals = compute_intervals(self.depth)
|
|
1890
|
+
valid_mask = ~np.isnan(self.values) & interval_mask
|
|
1891
|
+
zone_thickness = float(np.sum(full_intervals[valid_mask]))
|
|
1892
|
+
|
|
1893
|
+
# Get actual depth range within the interval (where we have data)
|
|
1894
|
+
if np.any(valid_mask):
|
|
1895
|
+
zone_depths = self.depth[valid_mask]
|
|
1896
|
+
zone_depth_range = {
|
|
1897
|
+
'min': round(float(np.min(zone_depths)), precision),
|
|
1898
|
+
'max': round(float(np.max(zone_depths)), precision)
|
|
1899
|
+
}
|
|
1900
|
+
else:
|
|
1901
|
+
zone_depth_range = {'min': top, 'max': base}
|
|
1902
|
+
|
|
1903
|
+
# Build interval result with zone-level metadata
|
|
1904
|
+
interval_result = {
|
|
1905
|
+
'depth_range': zone_depth_range,
|
|
1906
|
+
'thickness': round(zone_thickness, precision)
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
# If there are secondary properties, group within this interval
|
|
1910
|
+
if self.secondary_properties:
|
|
1911
|
+
facies_stats = self._recursive_discrete_group(
|
|
1912
|
+
0,
|
|
1913
|
+
interval_mask,
|
|
1914
|
+
gross_thickness=zone_thickness, # Use zone thickness for fractions
|
|
1915
|
+
precision=precision,
|
|
1916
|
+
include_depth_range=False # Don't include depth_range per facies
|
|
1917
|
+
)
|
|
1918
|
+
else:
|
|
1919
|
+
# No secondary properties, compute stats directly for interval
|
|
1920
|
+
facies_stats = self._compute_discrete_stats(
|
|
1921
|
+
interval_mask,
|
|
1922
|
+
gross_thickness=zone_thickness, # Use zone thickness for fractions
|
|
1923
|
+
precision=precision,
|
|
1924
|
+
include_depth_range=False # Don't include depth_range per facies
|
|
1925
|
+
)
|
|
1926
|
+
|
|
1927
|
+
# Nest facies stats under 'facies' key for cleaner structure
|
|
1928
|
+
interval_result['facies'] = facies_stats
|
|
1929
|
+
result[interval_name] = interval_result
|
|
1930
|
+
|
|
1931
|
+
return result
|
|
1932
|
+
|
|
1589
1933
|
def _recursive_discrete_group(
|
|
1590
1934
|
self,
|
|
1591
1935
|
filter_idx: int,
|
|
1592
1936
|
mask: np.ndarray,
|
|
1593
1937
|
gross_thickness: float,
|
|
1594
|
-
precision: int = 6
|
|
1938
|
+
precision: int = 6,
|
|
1939
|
+
include_depth_range: bool = True
|
|
1595
1940
|
) -> dict:
|
|
1596
1941
|
"""
|
|
1597
1942
|
Recursively group discrete statistics by secondary properties.
|
|
@@ -1606,6 +1951,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1606
1951
|
Total gross thickness for fraction calculation
|
|
1607
1952
|
precision : int, default 6
|
|
1608
1953
|
Number of decimal places for rounding
|
|
1954
|
+
include_depth_range : bool, default True
|
|
1955
|
+
Whether to include depth_range in per-facies stats
|
|
1609
1956
|
|
|
1610
1957
|
Returns
|
|
1611
1958
|
-------
|
|
@@ -1614,7 +1961,7 @@ class Property(PropertyOperationsMixin):
|
|
|
1614
1961
|
"""
|
|
1615
1962
|
if filter_idx >= len(self.secondary_properties):
|
|
1616
1963
|
# Base case: compute discrete statistics for this group
|
|
1617
|
-
return self._compute_discrete_stats(mask, gross_thickness, precision)
|
|
1964
|
+
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
|
|
1618
1965
|
|
|
1619
1966
|
# Get unique values for current filter
|
|
1620
1967
|
current_filter = self.secondary_properties[filter_idx]
|
|
@@ -1624,7 +1971,7 @@ class Property(PropertyOperationsMixin):
|
|
|
1624
1971
|
|
|
1625
1972
|
if len(unique_vals) == 0:
|
|
1626
1973
|
# No valid values, return stats for current mask
|
|
1627
|
-
return self._compute_discrete_stats(mask, gross_thickness, precision)
|
|
1974
|
+
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
|
|
1628
1975
|
|
|
1629
1976
|
# Group by each unique value
|
|
1630
1977
|
depth_array = self.depth
|
|
@@ -1660,7 +2007,7 @@ class Property(PropertyOperationsMixin):
|
|
|
1660
2007
|
key = f"{current_filter.name}_{val:.2f}"
|
|
1661
2008
|
|
|
1662
2009
|
result[key] = self._recursive_discrete_group(
|
|
1663
|
-
filter_idx + 1, sub_mask, group_thickness, precision
|
|
2010
|
+
filter_idx + 1, sub_mask, group_thickness, precision, include_depth_range
|
|
1664
2011
|
)
|
|
1665
2012
|
|
|
1666
2013
|
return result
|
|
@@ -1669,7 +2016,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1669
2016
|
self,
|
|
1670
2017
|
mask: np.ndarray,
|
|
1671
2018
|
gross_thickness: float,
|
|
1672
|
-
precision: int = 6
|
|
2019
|
+
precision: int = 6,
|
|
2020
|
+
include_depth_range: bool = True
|
|
1673
2021
|
) -> dict:
|
|
1674
2022
|
"""
|
|
1675
2023
|
Compute categorical statistics for discrete property values.
|
|
@@ -1682,12 +2030,15 @@ class Property(PropertyOperationsMixin):
|
|
|
1682
2030
|
Total gross thickness for fraction calculation
|
|
1683
2031
|
precision : int, default 6
|
|
1684
2032
|
Number of decimal places for rounding
|
|
2033
|
+
include_depth_range : bool, default True
|
|
2034
|
+
Whether to include depth_range in per-facies stats.
|
|
2035
|
+
Set to False when using filter_intervals (depth_range shown at zone level).
|
|
1685
2036
|
|
|
1686
2037
|
Returns
|
|
1687
2038
|
-------
|
|
1688
2039
|
dict
|
|
1689
2040
|
Dictionary with stats for each discrete value:
|
|
1690
|
-
{value_label: {code, count, thickness, fraction, depth_range}}
|
|
2041
|
+
{value_label: {code, count, thickness, fraction, [depth_range]}}
|
|
1691
2042
|
"""
|
|
1692
2043
|
values_array = self.values
|
|
1693
2044
|
depth_array = self.depth
|
|
@@ -1733,12 +2084,13 @@ class Property(PropertyOperationsMixin):
|
|
|
1733
2084
|
'code': int_val,
|
|
1734
2085
|
'count': count,
|
|
1735
2086
|
'thickness': round(thickness, precision),
|
|
1736
|
-
'fraction': round(fraction, precision)
|
|
1737
|
-
|
|
2087
|
+
'fraction': round(fraction, precision)
|
|
2088
|
+
}
|
|
2089
|
+
if include_depth_range:
|
|
2090
|
+
stats['depth_range'] = {
|
|
1738
2091
|
'min': round(float(np.min(val_depths)), precision),
|
|
1739
2092
|
'max': round(float(np.max(val_depths)), precision)
|
|
1740
2093
|
}
|
|
1741
|
-
}
|
|
1742
2094
|
if label is not None:
|
|
1743
2095
|
stats['label'] = label
|
|
1744
2096
|
|
well_log_toolkit/well.py
CHANGED
|
@@ -220,6 +220,8 @@ class Well:
|
|
|
220
220
|
self._deleted_sources: list[str] = [] # List of source names to delete
|
|
221
221
|
# Track sources marked for rename (to rename files on save)
|
|
222
222
|
self._renamed_sources: dict[str, str] = {} # {old_name: new_name}
|
|
223
|
+
# Saved filter intervals for use with filter_intervals()
|
|
224
|
+
self._saved_filter_intervals: dict[str, list[dict]] = {} # {filter_name: [intervals]}
|
|
223
225
|
|
|
224
226
|
def __setattr__(self, name: str, value):
|
|
225
227
|
"""
|
|
@@ -1226,7 +1228,58 @@ class Well:
|
|
|
1226
1228
|
f"Property '{name}' not found in well '{self.name}'. "
|
|
1227
1229
|
f"Available properties: {available or 'none'}"
|
|
1228
1230
|
)
|
|
1229
|
-
|
|
1231
|
+
|
|
1232
|
+
def get_intervals(self, name: str) -> list[dict]:
|
|
1233
|
+
"""
|
|
1234
|
+
Get saved filter intervals by name.
|
|
1235
|
+
|
|
1236
|
+
Parameters
|
|
1237
|
+
----------
|
|
1238
|
+
name : str
|
|
1239
|
+
Name of the saved filter intervals
|
|
1240
|
+
|
|
1241
|
+
Returns
|
|
1242
|
+
-------
|
|
1243
|
+
list[dict]
|
|
1244
|
+
List of interval definitions, each with keys 'name', 'top', 'base'
|
|
1245
|
+
|
|
1246
|
+
Raises
|
|
1247
|
+
------
|
|
1248
|
+
KeyError
|
|
1249
|
+
If no intervals with this name exist
|
|
1250
|
+
|
|
1251
|
+
Examples
|
|
1252
|
+
--------
|
|
1253
|
+
>>> # Save intervals
|
|
1254
|
+
>>> well.PHIE.filter_intervals([
|
|
1255
|
+
... {"name": "Zone_A", "top": 2500, "base": 2650}
|
|
1256
|
+
... ], save="My_Zones")
|
|
1257
|
+
>>>
|
|
1258
|
+
>>> # Retrieve them later
|
|
1259
|
+
>>> intervals = well.get_intervals("My_Zones")
|
|
1260
|
+
>>> print(intervals)
|
|
1261
|
+
[{'name': 'Zone_A', 'top': 2500, 'base': 2650}]
|
|
1262
|
+
"""
|
|
1263
|
+
if name not in self._saved_filter_intervals:
|
|
1264
|
+
available = list(self._saved_filter_intervals.keys())
|
|
1265
|
+
raise KeyError(
|
|
1266
|
+
f"Filter intervals '{name}' not found in well '{self.name}'. "
|
|
1267
|
+
f"Available: {available if available else 'none'}"
|
|
1268
|
+
)
|
|
1269
|
+
return self._saved_filter_intervals[name]
|
|
1270
|
+
|
|
1271
|
+
@property
|
|
1272
|
+
def saved_intervals(self) -> list[str]:
|
|
1273
|
+
"""
|
|
1274
|
+
List of saved filter interval names.
|
|
1275
|
+
|
|
1276
|
+
Returns
|
|
1277
|
+
-------
|
|
1278
|
+
list[str]
|
|
1279
|
+
Names of all saved filter intervals
|
|
1280
|
+
"""
|
|
1281
|
+
return list(self._saved_filter_intervals.keys())
|
|
1282
|
+
|
|
1230
1283
|
@staticmethod
|
|
1231
1284
|
def _is_regular_grid(depth: np.ndarray, tolerance: float = 1e-6) -> tuple[bool, Optional[float]]:
|
|
1232
1285
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: well-log-toolkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.144
|
|
4
4
|
Summary: Fast LAS file processing with lazy loading and filtering for well log analysis
|
|
5
5
|
Author-email: Kristian dF Kollsgård <kkollsg@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -288,6 +288,74 @@ stats = well.PHIE.filter('Zone').filter('Facies').sums_avg()
|
|
|
288
288
|
- `samples` - Number of valid measurements
|
|
289
289
|
- `range`, `depth_range` - Min/max values and depths
|
|
290
290
|
|
|
291
|
+
### Custom Interval Filtering
|
|
292
|
+
|
|
293
|
+
Define custom depth intervals without needing a discrete property in the well:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
# Define intervals with name, top, and base
|
|
297
|
+
intervals = [
|
|
298
|
+
{"name": "Zone_A", "top": 2500, "base": 2650},
|
|
299
|
+
{"name": "Zone_B", "top": 2650, "base": 2800}
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
# Use with sums_avg or discrete_summary
|
|
303
|
+
stats = well.PHIE.filter_intervals(intervals).sums_avg()
|
|
304
|
+
# → {'Zone_A': {'mean': 0.18, ...}, 'Zone_B': {'mean': 0.21, ...}}
|
|
305
|
+
|
|
306
|
+
facies_stats = well.Facies.filter_intervals(intervals).discrete_summary()
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Overlapping intervals** are supported - each interval is calculated independently:
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
# These intervals overlap at 2600-2700m
|
|
313
|
+
intervals = [
|
|
314
|
+
{"name": "Full_Reservoir", "top": 2500, "base": 2800},
|
|
315
|
+
{"name": "Upper_Section", "top": 2500, "base": 2700}
|
|
316
|
+
]
|
|
317
|
+
# Depths 2500-2700 are counted in BOTH zones
|
|
318
|
+
stats = well.PHIE.filter_intervals(intervals).sums_avg()
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Save intervals for reuse:**
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
# Save intervals to the well
|
|
325
|
+
well.PHIE.filter_intervals(intervals, save="Reservoir_Zones")
|
|
326
|
+
|
|
327
|
+
# Use saved intervals by name
|
|
328
|
+
stats = well.PHIE.filter_intervals("Reservoir_Zones").sums_avg()
|
|
329
|
+
|
|
330
|
+
# List saved intervals
|
|
331
|
+
print(well.saved_intervals) # ['Reservoir_Zones']
|
|
332
|
+
|
|
333
|
+
# Retrieve intervals
|
|
334
|
+
intervals = well.get_intervals("Reservoir_Zones")
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Save different intervals for multiple wells:**
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
# Define well-specific intervals
|
|
341
|
+
manager.well_A.PHIE.filter_intervals({
|
|
342
|
+
"Well_A": [{"name": "Zone_A", "top": 2500, "base": 2700}],
|
|
343
|
+
"Well_B": [{"name": "Zone_A", "top": 2600, "base": 2800}]
|
|
344
|
+
}, save="My_Zones")
|
|
345
|
+
|
|
346
|
+
# Both wells now have "My_Zones" saved with their respective intervals
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Chain with other filters:**
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
# Combine custom intervals with property filters
|
|
353
|
+
stats = well.PHIE.filter_intervals(intervals).filter("NetFlag").sums_avg()
|
|
354
|
+
# → {'Zone_A': {'Net': {...}, 'NonNet': {...}}, 'Zone_B': {...}}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
> **💡 Key Difference:** Unlike `.filter('Well_Tops')` where each depth belongs to exactly one zone, `filter_intervals()` allows overlapping intervals where the same depths can contribute to multiple zones.
|
|
358
|
+
|
|
291
359
|
### Property Operations
|
|
292
360
|
|
|
293
361
|
Create computed properties using natural mathematical syntax:
|
|
@@ -3,13 +3,13 @@ well_log_toolkit/exceptions.py,sha256=X_fzC7d4yaBFO9Vx74dEIB6xmI9Agi6_bTU3MPxn6k
|
|
|
3
3
|
well_log_toolkit/las_file.py,sha256=Tj0mRfX1aX2s6uug7BBlY1m_mu3G50EGxHGzD0eEedE,53876
|
|
4
4
|
well_log_toolkit/manager.py,sha256=VIARJLkYhxqxgTqfVfAAZU6AVsAPkQWPOUE6RNGnIdY,110558
|
|
5
5
|
well_log_toolkit/operations.py,sha256=z8j8fGBOwoJGUQFy-Vawjq9nm3OD_dUt0oaNh8yuG7o,18515
|
|
6
|
-
well_log_toolkit/property.py,sha256=
|
|
6
|
+
well_log_toolkit/property.py,sha256=4g5-_WRdJ9HwDKkufU4s_oOnCh6Deg58ZX5jE9Uwx2c,99833
|
|
7
7
|
well_log_toolkit/regression.py,sha256=JDcRxaODJnFikAdPJyTq8eUV7iY0vCDmvnGufqlojxs,31625
|
|
8
8
|
well_log_toolkit/statistics.py,sha256=_huPMbv2H3o9ezunjEM94mJknX5wPK8V4nDv2lIZZRw,16814
|
|
9
9
|
well_log_toolkit/utils.py,sha256=O2KPq4htIoUlL74V2zKftdqqTjRfezU9M-568zPLme0,6866
|
|
10
10
|
well_log_toolkit/visualization.py,sha256=nnpmFmbj44TbP0fsnLMR1GaKRkqKCEpI6Fd8Cp0oqBc,204716
|
|
11
|
-
well_log_toolkit/well.py,sha256=
|
|
12
|
-
well_log_toolkit-0.1.
|
|
13
|
-
well_log_toolkit-0.1.
|
|
14
|
-
well_log_toolkit-0.1.
|
|
15
|
-
well_log_toolkit-0.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|