well-log-toolkit 0.1.145__tar.gz → 0.1.147__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/PKG-INFO +1 -1
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/pyproject.toml +1 -1
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit/manager.py +599 -6
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit/property.py +4 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit.egg-info/PKG-INFO +1 -1
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/README.md +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/setup.cfg +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit/__init__.py +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit/exceptions.py +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit/las_file.py +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit/operations.py +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit/regression.py +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit/statistics.py +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit/utils.py +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit/visualization.py +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit/well.py +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit.egg-info/requires.txt +0 -0
- {well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "well-log-toolkit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.147"
|
|
8
8
|
description = "Fast LAS file processing with lazy loading and filtering for well log analysis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -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(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.145 → well_log_toolkit-0.1.147}/well_log_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|