well-log-toolkit 0.1.148__py3-none-any.whl → 0.1.150__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/manager.py +110 -5
- well_log_toolkit/property.py +94 -38
- well_log_toolkit/statistics.py +77 -0
- {well_log_toolkit-0.1.148.dist-info → well_log_toolkit-0.1.150.dist-info}/METADATA +1 -1
- {well_log_toolkit-0.1.148.dist-info → well_log_toolkit-0.1.150.dist-info}/RECORD +7 -7
- {well_log_toolkit-0.1.148.dist-info → well_log_toolkit-0.1.150.dist-info}/WHEEL +0 -0
- {well_log_toolkit-0.1.148.dist-info → well_log_toolkit-0.1.150.dist-info}/top_level.txt +0 -0
well_log_toolkit/manager.py
CHANGED
|
@@ -1445,12 +1445,23 @@ class _ManagerPropertyProxy:
|
|
|
1445
1445
|
prop = prop.filter(filter_name, source=source_name)
|
|
1446
1446
|
|
|
1447
1447
|
# Compute sums_avg
|
|
1448
|
-
|
|
1448
|
+
result = prop.sums_avg(
|
|
1449
1449
|
weighted=weighted,
|
|
1450
1450
|
arithmetic=arithmetic,
|
|
1451
1451
|
precision=precision
|
|
1452
1452
|
)
|
|
1453
1453
|
|
|
1454
|
+
# Add well-level thickness for this source if using filter_intervals
|
|
1455
|
+
if self._custom_intervals and result:
|
|
1456
|
+
well_thickness = 0.0
|
|
1457
|
+
for key, value in result.items():
|
|
1458
|
+
if isinstance(value, dict) and 'thickness' in value:
|
|
1459
|
+
well_thickness += value['thickness']
|
|
1460
|
+
if well_thickness > 0:
|
|
1461
|
+
result['thickness'] = round(well_thickness, precision)
|
|
1462
|
+
|
|
1463
|
+
source_results[source_name] = result
|
|
1464
|
+
|
|
1454
1465
|
except (PropertyNotFoundError, PropertyTypeError, AttributeError, KeyError, ValueError):
|
|
1455
1466
|
# Property or filter doesn't exist in this source, or filter isn't discrete - skip it
|
|
1456
1467
|
pass
|
|
@@ -1476,12 +1487,23 @@ class _ManagerPropertyProxy:
|
|
|
1476
1487
|
prop = prop.filter(filter_name)
|
|
1477
1488
|
|
|
1478
1489
|
# Compute sums_avg
|
|
1479
|
-
|
|
1490
|
+
result = prop.sums_avg(
|
|
1480
1491
|
weighted=weighted,
|
|
1481
1492
|
arithmetic=arithmetic,
|
|
1482
1493
|
precision=precision
|
|
1483
1494
|
)
|
|
1484
1495
|
|
|
1496
|
+
# Add well-level thickness (sum of all zone thicknesses) if using filter_intervals
|
|
1497
|
+
if self._custom_intervals and result:
|
|
1498
|
+
well_thickness = 0.0
|
|
1499
|
+
for key, value in result.items():
|
|
1500
|
+
if isinstance(value, dict) and 'thickness' in value:
|
|
1501
|
+
well_thickness += value['thickness']
|
|
1502
|
+
if well_thickness > 0:
|
|
1503
|
+
result['thickness'] = round(well_thickness, precision)
|
|
1504
|
+
|
|
1505
|
+
return result
|
|
1506
|
+
|
|
1485
1507
|
except PropertyNotFoundError as e:
|
|
1486
1508
|
# Check if it's ambiguous (exists in multiple sources)
|
|
1487
1509
|
if "ambiguous" in str(e).lower():
|
|
@@ -1512,6 +1534,16 @@ class _ManagerPropertyProxy:
|
|
|
1512
1534
|
arithmetic=arithmetic,
|
|
1513
1535
|
precision=precision
|
|
1514
1536
|
)
|
|
1537
|
+
|
|
1538
|
+
# Add well-level thickness for this source if using filter_intervals
|
|
1539
|
+
if self._custom_intervals and result:
|
|
1540
|
+
well_thickness = 0.0
|
|
1541
|
+
for key, value in result.items():
|
|
1542
|
+
if isinstance(value, dict) and 'thickness' in value:
|
|
1543
|
+
well_thickness += value['thickness']
|
|
1544
|
+
if well_thickness > 0:
|
|
1545
|
+
result['thickness'] = round(well_thickness, precision)
|
|
1546
|
+
|
|
1515
1547
|
source_results[source_name] = result
|
|
1516
1548
|
|
|
1517
1549
|
except (PropertyNotFoundError, PropertyTypeError, AttributeError, KeyError, ValueError):
|
|
@@ -1632,7 +1664,7 @@ class _ManagerMultiPropertyProxy:
|
|
|
1632
1664
|
PROPERTY_STATS = {'mean', 'median', 'mode', 'sum', 'std_dev', 'percentile', 'range'}
|
|
1633
1665
|
|
|
1634
1666
|
# Stats that are common across properties (stay at group level)
|
|
1635
|
-
COMMON_STATS = {'depth_range', 'samples', 'thickness', '
|
|
1667
|
+
COMMON_STATS = {'depth_range', 'samples', 'thickness', 'thickness_fraction', 'calculation'}
|
|
1636
1668
|
|
|
1637
1669
|
def __init__(
|
|
1638
1670
|
self,
|
|
@@ -1849,7 +1881,17 @@ class _ManagerMultiPropertyProxy:
|
|
|
1849
1881
|
return self._merge_flat_results(property_results)
|
|
1850
1882
|
|
|
1851
1883
|
# Merge results: nest property-specific stats, keep common stats at group level
|
|
1852
|
-
|
|
1884
|
+
merged = self._merge_property_results(property_results)
|
|
1885
|
+
|
|
1886
|
+
# Add well-level thickness (sum of all zone thicknesses)
|
|
1887
|
+
if self._custom_intervals and merged:
|
|
1888
|
+
well_thickness = 0.0
|
|
1889
|
+
for key, value in merged.items():
|
|
1890
|
+
if isinstance(value, dict) and 'thickness' in value:
|
|
1891
|
+
well_thickness += value['thickness']
|
|
1892
|
+
merged['thickness'] = round(well_thickness, 6)
|
|
1893
|
+
|
|
1894
|
+
return merged
|
|
1853
1895
|
|
|
1854
1896
|
def _apply_filter_intervals(self, prop, well):
|
|
1855
1897
|
"""
|
|
@@ -3108,7 +3150,70 @@ class WellDataManager:
|
|
|
3108
3150
|
['well_12_3_2_B', 'well_12_3_2_A']
|
|
3109
3151
|
"""
|
|
3110
3152
|
return list(self._wells.keys())
|
|
3111
|
-
|
|
3153
|
+
|
|
3154
|
+
@property
|
|
3155
|
+
def saved_intervals(self) -> dict[str, list[str]]:
|
|
3156
|
+
"""
|
|
3157
|
+
List saved interval names for all wells.
|
|
3158
|
+
|
|
3159
|
+
Returns
|
|
3160
|
+
-------
|
|
3161
|
+
dict[str, list[str]]
|
|
3162
|
+
Dictionary mapping well names to their saved interval names
|
|
3163
|
+
|
|
3164
|
+
Examples
|
|
3165
|
+
--------
|
|
3166
|
+
>>> manager.saved_intervals
|
|
3167
|
+
{'well_A': ['Reservoir_Zones', 'Slump_Zones'], 'well_B': ['Reservoir_Zones']}
|
|
3168
|
+
"""
|
|
3169
|
+
result = {}
|
|
3170
|
+
for well_name, well in self._wells.items():
|
|
3171
|
+
if well.saved_intervals:
|
|
3172
|
+
result[well_name] = well.saved_intervals
|
|
3173
|
+
return result
|
|
3174
|
+
|
|
3175
|
+
def get_intervals(self, name: str) -> dict[str, list[dict]]:
|
|
3176
|
+
"""
|
|
3177
|
+
Get saved filter intervals by name from all wells that have them.
|
|
3178
|
+
|
|
3179
|
+
Parameters
|
|
3180
|
+
----------
|
|
3181
|
+
name : str
|
|
3182
|
+
Name of the saved filter intervals
|
|
3183
|
+
|
|
3184
|
+
Returns
|
|
3185
|
+
-------
|
|
3186
|
+
dict[str, list[dict]]
|
|
3187
|
+
Dictionary mapping well names to their interval definitions
|
|
3188
|
+
|
|
3189
|
+
Raises
|
|
3190
|
+
------
|
|
3191
|
+
KeyError
|
|
3192
|
+
If no wells have intervals with the given name
|
|
3193
|
+
|
|
3194
|
+
Examples
|
|
3195
|
+
--------
|
|
3196
|
+
>>> manager.get_intervals("Slump_Zones")
|
|
3197
|
+
{'well_A': [{'name': 'Zone_A', 'top': 2500, 'base': 2650}],
|
|
3198
|
+
'well_B': [{'name': 'Zone_A', 'top': 2600, 'base': 2750}]}
|
|
3199
|
+
"""
|
|
3200
|
+
result = {}
|
|
3201
|
+
for well_name, well in self._wells.items():
|
|
3202
|
+
if name in well.saved_intervals:
|
|
3203
|
+
result[well_name] = well.get_intervals(name)
|
|
3204
|
+
|
|
3205
|
+
if not result:
|
|
3206
|
+
# Collect all available interval names for error message
|
|
3207
|
+
all_names = set()
|
|
3208
|
+
for well in self._wells.values():
|
|
3209
|
+
all_names.update(well.saved_intervals)
|
|
3210
|
+
raise KeyError(
|
|
3211
|
+
f"No wells have saved intervals named '{name}'. "
|
|
3212
|
+
f"Available: {sorted(all_names) if all_names else 'none'}"
|
|
3213
|
+
)
|
|
3214
|
+
|
|
3215
|
+
return result
|
|
3216
|
+
|
|
3112
3217
|
def get_well(self, name: str) -> Well:
|
|
3113
3218
|
"""
|
|
3114
3219
|
Get well by original or sanitized name.
|
well_log_toolkit/property.py
CHANGED
|
@@ -11,6 +11,7 @@ from scipy.interpolate import interp1d
|
|
|
11
11
|
from .exceptions import PropertyError, PropertyNotFoundError, PropertyTypeError, DepthAlignmentError
|
|
12
12
|
from .statistics import (
|
|
13
13
|
compute_intervals,
|
|
14
|
+
compute_zone_intervals,
|
|
14
15
|
mean as stat_mean, sum as stat_sum, std as stat_std, percentile as stat_percentile
|
|
15
16
|
)
|
|
16
17
|
from .utils import filter_names
|
|
@@ -1429,8 +1430,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1429
1430
|
max_valid_depth = valid_depths.max()
|
|
1430
1431
|
|
|
1431
1432
|
# Find boundaries that fall within the valid data range
|
|
1432
|
-
# Filter to boundaries within valid range
|
|
1433
|
-
mask = (boundary_depths
|
|
1433
|
+
# Filter to boundaries within valid range (inclusive on both ends)
|
|
1434
|
+
mask = (boundary_depths >= min_valid_depth) & (boundary_depths <= max_valid_depth)
|
|
1434
1435
|
potential_boundaries = boundary_depths[mask]
|
|
1435
1436
|
|
|
1436
1437
|
if len(potential_boundaries) == 0:
|
|
@@ -1760,6 +1761,9 @@ class Property(PropertyOperationsMixin):
|
|
|
1760
1761
|
|
|
1761
1762
|
This allows overlapping intervals where the same depths can
|
|
1762
1763
|
contribute to multiple zones.
|
|
1764
|
+
|
|
1765
|
+
Uses zone-aware intervals that are truncated at zone boundaries to ensure
|
|
1766
|
+
thickness is correctly attributed to each zone.
|
|
1763
1767
|
"""
|
|
1764
1768
|
result = {}
|
|
1765
1769
|
|
|
@@ -1768,27 +1772,40 @@ class Property(PropertyOperationsMixin):
|
|
|
1768
1772
|
top = float(interval['top'])
|
|
1769
1773
|
base = float(interval['base'])
|
|
1770
1774
|
|
|
1771
|
-
#
|
|
1772
|
-
|
|
1775
|
+
# Compute zone-aware intervals truncated at zone boundaries
|
|
1776
|
+
zone_intervals = compute_zone_intervals(self.depth, top, base)
|
|
1777
|
+
|
|
1778
|
+
# Create mask based on zone intervals - includes any sample that
|
|
1779
|
+
# contributes to this zone (even boundary samples with partial intervals)
|
|
1780
|
+
interval_mask = zone_intervals > 0
|
|
1781
|
+
|
|
1782
|
+
# Calculate zone thickness (sum of valid intervals within zone)
|
|
1783
|
+
valid_mask = interval_mask & ~np.isnan(self.values)
|
|
1784
|
+
zone_thickness = float(np.sum(zone_intervals[valid_mask]))
|
|
1773
1785
|
|
|
1774
1786
|
# If there are secondary properties, group within this interval
|
|
1775
1787
|
if self.secondary_properties:
|
|
1776
|
-
|
|
1788
|
+
interval_result = self._recursive_group(
|
|
1777
1789
|
0,
|
|
1778
1790
|
interval_mask,
|
|
1779
1791
|
weighted=weighted,
|
|
1780
1792
|
arithmetic=arithmetic,
|
|
1781
|
-
gross_thickness=
|
|
1782
|
-
precision=precision
|
|
1793
|
+
gross_thickness=zone_thickness, # Pass zone thickness as gross for children
|
|
1794
|
+
precision=precision,
|
|
1795
|
+
zone_intervals=zone_intervals
|
|
1783
1796
|
)
|
|
1797
|
+
# Add zone-level thickness
|
|
1798
|
+
interval_result['thickness'] = round(zone_thickness, precision)
|
|
1799
|
+
result[interval_name] = interval_result
|
|
1784
1800
|
else:
|
|
1785
1801
|
# No secondary properties, compute stats directly for interval
|
|
1786
1802
|
result[interval_name] = self._compute_stats(
|
|
1787
1803
|
interval_mask,
|
|
1788
1804
|
weighted=weighted,
|
|
1789
1805
|
arithmetic=arithmetic,
|
|
1790
|
-
gross_thickness=
|
|
1791
|
-
precision=precision
|
|
1806
|
+
gross_thickness=zone_thickness, # Use zone thickness for fraction calc
|
|
1807
|
+
precision=precision,
|
|
1808
|
+
zone_intervals=zone_intervals
|
|
1792
1809
|
)
|
|
1793
1810
|
|
|
1794
1811
|
return result
|
|
@@ -1905,6 +1922,9 @@ class Property(PropertyOperationsMixin):
|
|
|
1905
1922
|
This allows overlapping intervals where the same depths can
|
|
1906
1923
|
contribute to multiple zones. Zone-level metadata (depth_range, thickness)
|
|
1907
1924
|
is shown at the interval level, and fractions are relative to zone thickness.
|
|
1925
|
+
|
|
1926
|
+
Uses zone-aware intervals that are truncated at zone boundaries to ensure
|
|
1927
|
+
thickness is correctly attributed to each zone.
|
|
1908
1928
|
"""
|
|
1909
1929
|
result = {}
|
|
1910
1930
|
|
|
@@ -1913,13 +1933,16 @@ class Property(PropertyOperationsMixin):
|
|
|
1913
1933
|
top = float(interval['top'])
|
|
1914
1934
|
base = float(interval['base'])
|
|
1915
1935
|
|
|
1916
|
-
#
|
|
1917
|
-
|
|
1936
|
+
# Compute zone-aware intervals truncated at zone boundaries
|
|
1937
|
+
zone_intervals = compute_zone_intervals(self.depth, top, base)
|
|
1938
|
+
|
|
1939
|
+
# Create mask based on zone intervals - includes any sample that
|
|
1940
|
+
# contributes to this zone (even boundary samples with partial intervals)
|
|
1941
|
+
interval_mask = zone_intervals > 0
|
|
1918
1942
|
|
|
1919
|
-
# Calculate zone thickness
|
|
1920
|
-
full_intervals = compute_intervals(self.depth)
|
|
1943
|
+
# Calculate zone thickness using zone-aware intervals
|
|
1921
1944
|
valid_mask = ~np.isnan(self.values) & interval_mask
|
|
1922
|
-
zone_thickness = float(np.sum(
|
|
1945
|
+
zone_thickness = float(np.sum(zone_intervals[valid_mask]))
|
|
1923
1946
|
|
|
1924
1947
|
# Get actual depth range within the interval (where we have data)
|
|
1925
1948
|
if np.any(valid_mask):
|
|
@@ -1944,7 +1967,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1944
1967
|
interval_mask,
|
|
1945
1968
|
gross_thickness=zone_thickness, # Use zone thickness for fractions
|
|
1946
1969
|
precision=precision,
|
|
1947
|
-
include_depth_range=False # Don't include depth_range per facies
|
|
1970
|
+
include_depth_range=False, # Don't include depth_range per facies
|
|
1971
|
+
zone_intervals=zone_intervals # Pass zone-aware intervals
|
|
1948
1972
|
)
|
|
1949
1973
|
else:
|
|
1950
1974
|
# No secondary properties, compute stats directly for interval
|
|
@@ -1952,7 +1976,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1952
1976
|
interval_mask,
|
|
1953
1977
|
gross_thickness=zone_thickness, # Use zone thickness for fractions
|
|
1954
1978
|
precision=precision,
|
|
1955
|
-
include_depth_range=False # Don't include depth_range per facies
|
|
1979
|
+
include_depth_range=False, # Don't include depth_range per facies
|
|
1980
|
+
zone_intervals=zone_intervals # Pass zone-aware intervals
|
|
1956
1981
|
)
|
|
1957
1982
|
|
|
1958
1983
|
# Nest facies stats under 'facies' key for cleaner structure
|
|
@@ -1967,7 +1992,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1967
1992
|
mask: np.ndarray,
|
|
1968
1993
|
gross_thickness: float,
|
|
1969
1994
|
precision: int = 6,
|
|
1970
|
-
include_depth_range: bool = True
|
|
1995
|
+
include_depth_range: bool = True,
|
|
1996
|
+
zone_intervals: Optional[np.ndarray] = None
|
|
1971
1997
|
) -> dict:
|
|
1972
1998
|
"""
|
|
1973
1999
|
Recursively group discrete statistics by secondary properties.
|
|
@@ -1984,6 +2010,9 @@ class Property(PropertyOperationsMixin):
|
|
|
1984
2010
|
Number of decimal places for rounding
|
|
1985
2011
|
include_depth_range : bool, default True
|
|
1986
2012
|
Whether to include depth_range in per-facies stats
|
|
2013
|
+
zone_intervals : np.ndarray, optional
|
|
2014
|
+
Pre-computed zone-aware intervals truncated at zone boundaries.
|
|
2015
|
+
If None, computes intervals using standard midpoint method.
|
|
1987
2016
|
|
|
1988
2017
|
Returns
|
|
1989
2018
|
-------
|
|
@@ -1992,7 +2021,7 @@ class Property(PropertyOperationsMixin):
|
|
|
1992
2021
|
"""
|
|
1993
2022
|
if filter_idx >= len(self.secondary_properties):
|
|
1994
2023
|
# Base case: compute discrete statistics for this group
|
|
1995
|
-
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
|
|
2024
|
+
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range, zone_intervals)
|
|
1996
2025
|
|
|
1997
2026
|
# Get unique values for current filter
|
|
1998
2027
|
current_filter = self.secondary_properties[filter_idx]
|
|
@@ -2002,12 +2031,16 @@ class Property(PropertyOperationsMixin):
|
|
|
2002
2031
|
|
|
2003
2032
|
if len(unique_vals) == 0:
|
|
2004
2033
|
# No valid values, return stats for current mask
|
|
2005
|
-
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
|
|
2034
|
+
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range, zone_intervals)
|
|
2006
2035
|
|
|
2007
2036
|
# Group by each unique value
|
|
2008
2037
|
depth_array = self.depth
|
|
2009
2038
|
values_array = self.values
|
|
2010
|
-
|
|
2039
|
+
# Use zone intervals if provided, otherwise compute on full array
|
|
2040
|
+
if zone_intervals is not None:
|
|
2041
|
+
full_intervals = zone_intervals
|
|
2042
|
+
else:
|
|
2043
|
+
full_intervals = compute_intervals(depth_array)
|
|
2011
2044
|
|
|
2012
2045
|
result = {}
|
|
2013
2046
|
for val in unique_vals:
|
|
@@ -2038,7 +2071,7 @@ class Property(PropertyOperationsMixin):
|
|
|
2038
2071
|
key = f"{current_filter.name}_{val:.2f}"
|
|
2039
2072
|
|
|
2040
2073
|
result[key] = self._recursive_discrete_group(
|
|
2041
|
-
filter_idx + 1, sub_mask, group_thickness, precision, include_depth_range
|
|
2074
|
+
filter_idx + 1, sub_mask, group_thickness, precision, include_depth_range, zone_intervals
|
|
2042
2075
|
)
|
|
2043
2076
|
|
|
2044
2077
|
return result
|
|
@@ -2048,7 +2081,8 @@ class Property(PropertyOperationsMixin):
|
|
|
2048
2081
|
mask: np.ndarray,
|
|
2049
2082
|
gross_thickness: float,
|
|
2050
2083
|
precision: int = 6,
|
|
2051
|
-
include_depth_range: bool = True
|
|
2084
|
+
include_depth_range: bool = True,
|
|
2085
|
+
zone_intervals: Optional[np.ndarray] = None
|
|
2052
2086
|
) -> dict:
|
|
2053
2087
|
"""
|
|
2054
2088
|
Compute categorical statistics for discrete property values.
|
|
@@ -2064,6 +2098,9 @@ class Property(PropertyOperationsMixin):
|
|
|
2064
2098
|
include_depth_range : bool, default True
|
|
2065
2099
|
Whether to include depth_range in per-facies stats.
|
|
2066
2100
|
Set to False when using filter_intervals (depth_range shown at zone level).
|
|
2101
|
+
zone_intervals : np.ndarray, optional
|
|
2102
|
+
Pre-computed zone-aware intervals truncated at zone boundaries.
|
|
2103
|
+
If None, computes intervals using standard midpoint method.
|
|
2067
2104
|
|
|
2068
2105
|
Returns
|
|
2069
2106
|
-------
|
|
@@ -2077,9 +2114,12 @@ class Property(PropertyOperationsMixin):
|
|
|
2077
2114
|
values = values_array[mask]
|
|
2078
2115
|
depths = depth_array[mask]
|
|
2079
2116
|
|
|
2080
|
-
#
|
|
2081
|
-
|
|
2082
|
-
|
|
2117
|
+
# Use zone intervals if provided, otherwise compute on full array
|
|
2118
|
+
if zone_intervals is not None:
|
|
2119
|
+
intervals = zone_intervals[mask]
|
|
2120
|
+
else:
|
|
2121
|
+
full_intervals = compute_intervals(depth_array)
|
|
2122
|
+
intervals = full_intervals[mask]
|
|
2083
2123
|
|
|
2084
2124
|
# Find unique discrete values
|
|
2085
2125
|
valid_mask_local = ~np.isnan(values)
|
|
@@ -2132,7 +2172,8 @@ class Property(PropertyOperationsMixin):
|
|
|
2132
2172
|
weighted: bool,
|
|
2133
2173
|
arithmetic: bool,
|
|
2134
2174
|
gross_thickness: float,
|
|
2135
|
-
precision: int = 6
|
|
2175
|
+
precision: int = 6,
|
|
2176
|
+
zone_intervals: Optional[np.ndarray] = None
|
|
2136
2177
|
) -> dict:
|
|
2137
2178
|
"""
|
|
2138
2179
|
Recursively group by secondary properties.
|
|
@@ -2151,6 +2192,9 @@ class Property(PropertyOperationsMixin):
|
|
|
2151
2192
|
Total gross thickness for fraction calculation
|
|
2152
2193
|
precision : int, default 6
|
|
2153
2194
|
Number of decimal places for rounding
|
|
2195
|
+
zone_intervals : np.ndarray, optional
|
|
2196
|
+
Pre-computed zone-aware intervals truncated at zone boundaries.
|
|
2197
|
+
If None, computes intervals using standard midpoint method.
|
|
2154
2198
|
|
|
2155
2199
|
Returns
|
|
2156
2200
|
-------
|
|
@@ -2159,7 +2203,7 @@ class Property(PropertyOperationsMixin):
|
|
|
2159
2203
|
"""
|
|
2160
2204
|
if filter_idx >= len(self.secondary_properties):
|
|
2161
2205
|
# Base case: compute statistics for this group
|
|
2162
|
-
return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision)
|
|
2206
|
+
return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision, zone_intervals)
|
|
2163
2207
|
|
|
2164
2208
|
# Get unique values for current filter
|
|
2165
2209
|
current_filter = self.secondary_properties[filter_idx]
|
|
@@ -2170,14 +2214,18 @@ class Property(PropertyOperationsMixin):
|
|
|
2170
2214
|
|
|
2171
2215
|
if len(unique_vals) == 0:
|
|
2172
2216
|
# No valid values, return stats for current mask
|
|
2173
|
-
return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision)
|
|
2217
|
+
return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision, zone_intervals)
|
|
2174
2218
|
|
|
2175
2219
|
# Calculate parent thickness BEFORE subdividing
|
|
2176
2220
|
# This becomes the gross_thickness for all child groups
|
|
2177
2221
|
# Cache property access to avoid overhead
|
|
2178
2222
|
depth_array = self.depth
|
|
2179
2223
|
values_array = self.values
|
|
2180
|
-
|
|
2224
|
+
# Use zone intervals if provided, otherwise compute on full array
|
|
2225
|
+
if zone_intervals is not None:
|
|
2226
|
+
parent_intervals = zone_intervals
|
|
2227
|
+
else:
|
|
2228
|
+
parent_intervals = compute_intervals(depth_array)
|
|
2181
2229
|
parent_valid = mask & ~np.isnan(values_array)
|
|
2182
2230
|
parent_thickness = float(np.sum(parent_intervals[parent_valid]))
|
|
2183
2231
|
|
|
@@ -2212,7 +2260,7 @@ class Property(PropertyOperationsMixin):
|
|
|
2212
2260
|
key = f"{current_filter.name}_{val:.2f}"
|
|
2213
2261
|
|
|
2214
2262
|
result[key] = self._recursive_group(
|
|
2215
|
-
filter_idx + 1, sub_mask, weighted, arithmetic, parent_thickness, precision
|
|
2263
|
+
filter_idx + 1, sub_mask, weighted, arithmetic, parent_thickness, precision, zone_intervals
|
|
2216
2264
|
)
|
|
2217
2265
|
|
|
2218
2266
|
return result
|
|
@@ -2223,7 +2271,8 @@ class Property(PropertyOperationsMixin):
|
|
|
2223
2271
|
weighted: bool = True,
|
|
2224
2272
|
arithmetic: bool = False,
|
|
2225
2273
|
gross_thickness: float = 0.0,
|
|
2226
|
-
precision: int = 6
|
|
2274
|
+
precision: int = 6,
|
|
2275
|
+
zone_intervals: Optional[np.ndarray] = None
|
|
2227
2276
|
) -> dict:
|
|
2228
2277
|
"""
|
|
2229
2278
|
Compute statistics for values selected by mask.
|
|
@@ -2244,6 +2293,9 @@ class Property(PropertyOperationsMixin):
|
|
|
2244
2293
|
Total gross thickness for fraction calculation
|
|
2245
2294
|
precision : int, default 6
|
|
2246
2295
|
Number of decimal places for rounding
|
|
2296
|
+
zone_intervals : np.ndarray, optional
|
|
2297
|
+
Pre-computed zone-aware intervals truncated at zone boundaries.
|
|
2298
|
+
If None, computes intervals using standard midpoint method.
|
|
2247
2299
|
|
|
2248
2300
|
Returns
|
|
2249
2301
|
-------
|
|
@@ -2263,12 +2315,17 @@ class Property(PropertyOperationsMixin):
|
|
|
2263
2315
|
values = values_array[mask]
|
|
2264
2316
|
valid = values[~np.isnan(values)]
|
|
2265
2317
|
|
|
2266
|
-
#
|
|
2267
|
-
#
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2318
|
+
# Use zone intervals if provided, otherwise compute on full array
|
|
2319
|
+
# Zone intervals are truncated at zone boundaries for accurate thickness
|
|
2320
|
+
if zone_intervals is not None:
|
|
2321
|
+
intervals = zone_intervals[mask]
|
|
2322
|
+
else:
|
|
2323
|
+
# Compute depth intervals on FULL depth array first, then mask
|
|
2324
|
+
# This is critical! Intervals must be computed on full grid so that
|
|
2325
|
+
# zone boundary samples get correct weights based on their neighbors
|
|
2326
|
+
# in the full sequence, not just within their zone.
|
|
2327
|
+
full_intervals = compute_intervals(depth_array)
|
|
2328
|
+
intervals = full_intervals[mask]
|
|
2272
2329
|
valid_mask_local = ~np.isnan(values)
|
|
2273
2330
|
valid_intervals = intervals[valid_mask_local]
|
|
2274
2331
|
|
|
@@ -2366,7 +2423,6 @@ class Property(PropertyOperationsMixin):
|
|
|
2366
2423
|
'depth_range': _round_value(depth_range_dict),
|
|
2367
2424
|
'samples': int(len(valid)),
|
|
2368
2425
|
'thickness': round(thickness, precision),
|
|
2369
|
-
'gross_thickness': round(gross_thickness, precision),
|
|
2370
2426
|
'thickness_fraction': round(fraction, precision),
|
|
2371
2427
|
'calculation': calc_method,
|
|
2372
2428
|
}
|
well_log_toolkit/statistics.py
CHANGED
|
@@ -61,6 +61,83 @@ def compute_intervals(depth: np.ndarray) -> np.ndarray:
|
|
|
61
61
|
return intervals
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def compute_zone_intervals(
|
|
65
|
+
depth: np.ndarray,
|
|
66
|
+
top: float,
|
|
67
|
+
base: float
|
|
68
|
+
) -> np.ndarray:
|
|
69
|
+
"""
|
|
70
|
+
Compute depth intervals truncated to zone boundaries.
|
|
71
|
+
|
|
72
|
+
Uses the midpoint method but truncates intervals at zone boundaries
|
|
73
|
+
to ensure thickness is correctly attributed to each zone.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
depth : np.ndarray
|
|
78
|
+
Depth values (must be sorted ascending)
|
|
79
|
+
top : float
|
|
80
|
+
Zone top depth (inclusive)
|
|
81
|
+
base : float
|
|
82
|
+
Zone base depth (exclusive)
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
np.ndarray
|
|
87
|
+
Interval thickness for each depth point, truncated to zone boundaries.
|
|
88
|
+
Points outside the zone have zero interval.
|
|
89
|
+
|
|
90
|
+
Examples
|
|
91
|
+
--------
|
|
92
|
+
>>> depth = np.array([2708.0, 2708.3, 2708.4, 2708.6])
|
|
93
|
+
>>> # Zone from 2708.0 to 2708.4
|
|
94
|
+
>>> compute_zone_intervals(depth, 2708.0, 2708.4)
|
|
95
|
+
array([0.15, 0.2, 0.05, 0.0])
|
|
96
|
+
|
|
97
|
+
The intervals are truncated at zone boundary 2708.4:
|
|
98
|
+
- 2708.0: from 2708.0 to midpoint(2708.0, 2708.3)=2708.15 = 0.15m
|
|
99
|
+
- 2708.3: from 2708.15 to midpoint(2708.3, 2708.4)=2708.35 = 0.2m
|
|
100
|
+
- 2708.4: from 2708.35 to 2708.4 (zone boundary) = 0.05m (truncated)
|
|
101
|
+
- 2708.6: outside zone = 0.0m
|
|
102
|
+
"""
|
|
103
|
+
if len(depth) == 0:
|
|
104
|
+
return np.array([])
|
|
105
|
+
|
|
106
|
+
if len(depth) == 1:
|
|
107
|
+
# Single point - check if it's in the zone
|
|
108
|
+
if top <= depth[0] < base:
|
|
109
|
+
return np.array([base - top])
|
|
110
|
+
return np.array([0.0])
|
|
111
|
+
|
|
112
|
+
zone_intervals = np.zeros(len(depth))
|
|
113
|
+
|
|
114
|
+
for i in range(len(depth)):
|
|
115
|
+
d = depth[i]
|
|
116
|
+
|
|
117
|
+
# Calculate midpoint bounds for this sample
|
|
118
|
+
if i == 0:
|
|
119
|
+
lower_bound = d - (depth[1] - d) / 2.0 # Mirror first interval
|
|
120
|
+
else:
|
|
121
|
+
lower_bound = (depth[i - 1] + d) / 2.0
|
|
122
|
+
|
|
123
|
+
if i == len(depth) - 1:
|
|
124
|
+
upper_bound = d + (d - depth[-2]) / 2.0 # Mirror last interval
|
|
125
|
+
else:
|
|
126
|
+
upper_bound = (d + depth[i + 1]) / 2.0
|
|
127
|
+
|
|
128
|
+
# Truncate to zone boundaries
|
|
129
|
+
effective_lower = max(lower_bound, top)
|
|
130
|
+
effective_upper = min(upper_bound, base)
|
|
131
|
+
|
|
132
|
+
# Only count if there's overlap with the zone
|
|
133
|
+
if effective_upper > effective_lower:
|
|
134
|
+
zone_intervals[i] = effective_upper - effective_lower
|
|
135
|
+
else:
|
|
136
|
+
zone_intervals[i] = 0.0
|
|
137
|
+
|
|
138
|
+
return zone_intervals
|
|
139
|
+
|
|
140
|
+
|
|
64
141
|
def mean(
|
|
65
142
|
values: np.ndarray,
|
|
66
143
|
weights: Optional[np.ndarray] = None,
|
|
@@ -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=
|
|
4
|
+
well_log_toolkit/manager.py,sha256=F6C8SXSD53FtG16-u3z298ViAj-5cMeNLSAoU77MyXY,140694
|
|
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=XY3BAN76CY6KY8na4iyoz6P-inhDyb821o3gN7ZC3q4,104184
|
|
7
7
|
well_log_toolkit/regression.py,sha256=JDcRxaODJnFikAdPJyTq8eUV7iY0vCDmvnGufqlojxs,31625
|
|
8
|
-
well_log_toolkit/statistics.py,sha256=
|
|
8
|
+
well_log_toolkit/statistics.py,sha256=cpUbaRGlqyqpGWKtETk9XpXWrMJIIjVacdqEqIBkvqQ,19118
|
|
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.150.dist-info/METADATA,sha256=I5J-CdMD7XIL0uhVhHnergVpoFxblRISMCT5KumaiU8,63473
|
|
13
|
+
well_log_toolkit-0.1.150.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
14
|
+
well_log_toolkit-0.1.150.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
|
|
15
|
+
well_log_toolkit-0.1.150.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|