well-log-toolkit 0.1.143__py3-none-any.whl → 0.1.145__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 +94 -35
- {well_log_toolkit-0.1.143.dist-info → well_log_toolkit-0.1.145.dist-info}/METADATA +69 -1
- {well_log_toolkit-0.1.143.dist-info → well_log_toolkit-0.1.145.dist-info}/RECORD +5 -5
- {well_log_toolkit-0.1.143.dist-info → well_log_toolkit-0.1.145.dist-info}/WHEEL +0 -0
- {well_log_toolkit-0.1.143.dist-info → well_log_toolkit-0.1.145.dist-info}/top_level.txt +0 -0
well_log_toolkit/property.py
CHANGED
|
@@ -1789,7 +1789,11 @@ class Property(PropertyOperationsMixin):
|
|
|
1789
1789
|
|
|
1790
1790
|
return result
|
|
1791
1791
|
|
|
1792
|
-
def discrete_summary(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
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,
|
|
@@ -1872,7 +1899,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1872
1899
|
Compute discrete statistics for each custom interval independently.
|
|
1873
1900
|
|
|
1874
1901
|
This allows overlapping intervals where the same depths can
|
|
1875
|
-
contribute to multiple zones.
|
|
1902
|
+
contribute to multiple zones. Zone-level metadata (depth_range, thickness)
|
|
1903
|
+
is shown at the interval level, and fractions are relative to zone thickness.
|
|
1876
1904
|
"""
|
|
1877
1905
|
result = {}
|
|
1878
1906
|
|
|
@@ -1884,22 +1912,49 @@ class Property(PropertyOperationsMixin):
|
|
|
1884
1912
|
# Create mask for this interval (top <= depth < base)
|
|
1885
1913
|
interval_mask = (self.depth >= top) & (self.depth < base)
|
|
1886
1914
|
|
|
1915
|
+
# Calculate zone thickness for fraction calculation
|
|
1916
|
+
full_intervals = compute_intervals(self.depth)
|
|
1917
|
+
valid_mask = ~np.isnan(self.values) & interval_mask
|
|
1918
|
+
zone_thickness = float(np.sum(full_intervals[valid_mask]))
|
|
1919
|
+
|
|
1920
|
+
# Get actual depth range within the interval (where we have data)
|
|
1921
|
+
if np.any(valid_mask):
|
|
1922
|
+
zone_depths = self.depth[valid_mask]
|
|
1923
|
+
zone_depth_range = {
|
|
1924
|
+
'min': round(float(np.min(zone_depths)), precision),
|
|
1925
|
+
'max': round(float(np.max(zone_depths)), precision)
|
|
1926
|
+
}
|
|
1927
|
+
else:
|
|
1928
|
+
zone_depth_range = {'min': top, 'max': base}
|
|
1929
|
+
|
|
1930
|
+
# Build interval result with zone-level metadata
|
|
1931
|
+
interval_result = {
|
|
1932
|
+
'depth_range': zone_depth_range,
|
|
1933
|
+
'thickness': round(zone_thickness, precision)
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1887
1936
|
# If there are secondary properties, group within this interval
|
|
1888
1937
|
if self.secondary_properties:
|
|
1889
|
-
|
|
1938
|
+
facies_stats = self._recursive_discrete_group(
|
|
1890
1939
|
0,
|
|
1891
1940
|
interval_mask,
|
|
1892
|
-
gross_thickness=
|
|
1893
|
-
precision=precision
|
|
1941
|
+
gross_thickness=zone_thickness, # Use zone thickness for fractions
|
|
1942
|
+
precision=precision,
|
|
1943
|
+
include_depth_range=False # Don't include depth_range per facies
|
|
1894
1944
|
)
|
|
1895
1945
|
else:
|
|
1896
1946
|
# No secondary properties, compute stats directly for interval
|
|
1897
|
-
|
|
1947
|
+
facies_stats = self._compute_discrete_stats(
|
|
1898
1948
|
interval_mask,
|
|
1899
|
-
gross_thickness=
|
|
1900
|
-
precision=precision
|
|
1949
|
+
gross_thickness=zone_thickness, # Use zone thickness for fractions
|
|
1950
|
+
precision=precision,
|
|
1951
|
+
include_depth_range=False # Don't include depth_range per facies
|
|
1901
1952
|
)
|
|
1902
1953
|
|
|
1954
|
+
# Nest facies stats under 'facies' key for cleaner structure
|
|
1955
|
+
interval_result['facies'] = facies_stats
|
|
1956
|
+
result[interval_name] = interval_result
|
|
1957
|
+
|
|
1903
1958
|
return result
|
|
1904
1959
|
|
|
1905
1960
|
def _recursive_discrete_group(
|
|
@@ -1907,7 +1962,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1907
1962
|
filter_idx: int,
|
|
1908
1963
|
mask: np.ndarray,
|
|
1909
1964
|
gross_thickness: float,
|
|
1910
|
-
precision: int = 6
|
|
1965
|
+
precision: int = 6,
|
|
1966
|
+
include_depth_range: bool = True
|
|
1911
1967
|
) -> dict:
|
|
1912
1968
|
"""
|
|
1913
1969
|
Recursively group discrete statistics by secondary properties.
|
|
@@ -1922,6 +1978,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1922
1978
|
Total gross thickness for fraction calculation
|
|
1923
1979
|
precision : int, default 6
|
|
1924
1980
|
Number of decimal places for rounding
|
|
1981
|
+
include_depth_range : bool, default True
|
|
1982
|
+
Whether to include depth_range in per-facies stats
|
|
1925
1983
|
|
|
1926
1984
|
Returns
|
|
1927
1985
|
-------
|
|
@@ -1930,7 +1988,7 @@ class Property(PropertyOperationsMixin):
|
|
|
1930
1988
|
"""
|
|
1931
1989
|
if filter_idx >= len(self.secondary_properties):
|
|
1932
1990
|
# Base case: compute discrete statistics for this group
|
|
1933
|
-
return self._compute_discrete_stats(mask, gross_thickness, precision)
|
|
1991
|
+
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
|
|
1934
1992
|
|
|
1935
1993
|
# Get unique values for current filter
|
|
1936
1994
|
current_filter = self.secondary_properties[filter_idx]
|
|
@@ -1940,7 +1998,7 @@ class Property(PropertyOperationsMixin):
|
|
|
1940
1998
|
|
|
1941
1999
|
if len(unique_vals) == 0:
|
|
1942
2000
|
# No valid values, return stats for current mask
|
|
1943
|
-
return self._compute_discrete_stats(mask, gross_thickness, precision)
|
|
2001
|
+
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
|
|
1944
2002
|
|
|
1945
2003
|
# Group by each unique value
|
|
1946
2004
|
depth_array = self.depth
|
|
@@ -1976,7 +2034,7 @@ class Property(PropertyOperationsMixin):
|
|
|
1976
2034
|
key = f"{current_filter.name}_{val:.2f}"
|
|
1977
2035
|
|
|
1978
2036
|
result[key] = self._recursive_discrete_group(
|
|
1979
|
-
filter_idx + 1, sub_mask, group_thickness, precision
|
|
2037
|
+
filter_idx + 1, sub_mask, group_thickness, precision, include_depth_range
|
|
1980
2038
|
)
|
|
1981
2039
|
|
|
1982
2040
|
return result
|
|
@@ -1985,7 +2043,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1985
2043
|
self,
|
|
1986
2044
|
mask: np.ndarray,
|
|
1987
2045
|
gross_thickness: float,
|
|
1988
|
-
precision: int = 6
|
|
2046
|
+
precision: int = 6,
|
|
2047
|
+
include_depth_range: bool = True
|
|
1989
2048
|
) -> dict:
|
|
1990
2049
|
"""
|
|
1991
2050
|
Compute categorical statistics for discrete property values.
|
|
@@ -1998,12 +2057,15 @@ class Property(PropertyOperationsMixin):
|
|
|
1998
2057
|
Total gross thickness for fraction calculation
|
|
1999
2058
|
precision : int, default 6
|
|
2000
2059
|
Number of decimal places for rounding
|
|
2060
|
+
include_depth_range : bool, default True
|
|
2061
|
+
Whether to include depth_range in per-facies stats.
|
|
2062
|
+
Set to False when using filter_intervals (depth_range shown at zone level).
|
|
2001
2063
|
|
|
2002
2064
|
Returns
|
|
2003
2065
|
-------
|
|
2004
2066
|
dict
|
|
2005
2067
|
Dictionary with stats for each discrete value:
|
|
2006
|
-
{value_label: {code, count, thickness, fraction, depth_range}}
|
|
2068
|
+
{value_label: {code, count, thickness, fraction, [depth_range]}}
|
|
2007
2069
|
"""
|
|
2008
2070
|
values_array = self.values
|
|
2009
2071
|
depth_array = self.depth
|
|
@@ -2036,27 +2098,24 @@ class Property(PropertyOperationsMixin):
|
|
|
2036
2098
|
count = int(np.sum(val_mask))
|
|
2037
2099
|
fraction = thickness / gross_thickness if gross_thickness > 0 else 0.0
|
|
2038
2100
|
|
|
2039
|
-
# Determine the key
|
|
2101
|
+
# Determine the key (use label if available, otherwise name_code)
|
|
2040
2102
|
int_val = int(val)
|
|
2041
2103
|
if self.labels is not None and int_val in self.labels:
|
|
2042
2104
|
key = self.labels[int_val]
|
|
2043
|
-
label = self.labels[int_val]
|
|
2044
2105
|
else:
|
|
2045
2106
|
key = f"{self.name}_{int_val}"
|
|
2046
|
-
label = None
|
|
2047
2107
|
|
|
2048
2108
|
stats = {
|
|
2049
2109
|
'code': int_val,
|
|
2050
2110
|
'count': count,
|
|
2051
2111
|
'thickness': round(thickness, precision),
|
|
2052
|
-
'fraction': round(fraction, precision)
|
|
2053
|
-
|
|
2112
|
+
'fraction': round(fraction, precision)
|
|
2113
|
+
}
|
|
2114
|
+
if include_depth_range:
|
|
2115
|
+
stats['depth_range'] = {
|
|
2054
2116
|
'min': round(float(np.min(val_depths)), precision),
|
|
2055
2117
|
'max': round(float(np.max(val_depths)), precision)
|
|
2056
2118
|
}
|
|
2057
|
-
}
|
|
2058
|
-
if label is not None:
|
|
2059
|
-
stats['label'] = label
|
|
2060
2119
|
|
|
2061
2120
|
result[key] = stats
|
|
2062
2121
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: well-log-toolkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.145
|
|
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=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.
|
|
13
|
-
well_log_toolkit-0.1.
|
|
14
|
-
well_log_toolkit-0.1.
|
|
15
|
-
well_log_toolkit-0.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|