well-log-toolkit 0.1.147__py3-none-any.whl → 0.1.149__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 +109 -9
- well_log_toolkit/property.py +84 -34
- well_log_toolkit/statistics.py +77 -0
- {well_log_toolkit-0.1.147.dist-info → well_log_toolkit-0.1.149.dist-info}/METADATA +1 -1
- {well_log_toolkit-0.1.147.dist-info → well_log_toolkit-0.1.149.dist-info}/RECORD +7 -7
- {well_log_toolkit-0.1.147.dist-info → well_log_toolkit-0.1.149.dist-info}/WHEEL +0 -0
- {well_log_toolkit-0.1.147.dist-info → well_log_toolkit-0.1.149.dist-info}/top_level.txt +0 -0
well_log_toolkit/manager.py
CHANGED
|
@@ -1646,6 +1646,19 @@ class _ManagerMultiPropertyProxy:
|
|
|
1646
1646
|
self._filters = filters or []
|
|
1647
1647
|
self._custom_intervals = custom_intervals
|
|
1648
1648
|
|
|
1649
|
+
def __getattr__(self, name: str) -> '_ManagerMultiPropertyProxy':
|
|
1650
|
+
"""
|
|
1651
|
+
Attribute access as shorthand for filter().
|
|
1652
|
+
|
|
1653
|
+
Allows: manager.properties(['A', 'B']).Facies.sums_avg()
|
|
1654
|
+
Same as: manager.properties(['A', 'B']).filter('Facies').sums_avg()
|
|
1655
|
+
"""
|
|
1656
|
+
# Avoid recursion for private attributes
|
|
1657
|
+
if name.startswith('_'):
|
|
1658
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
1659
|
+
# Treat as filter
|
|
1660
|
+
return self.filter(name)
|
|
1661
|
+
|
|
1649
1662
|
def filter(
|
|
1650
1663
|
self,
|
|
1651
1664
|
property_name: str,
|
|
@@ -1759,13 +1772,10 @@ class _ManagerMultiPropertyProxy:
|
|
|
1759
1772
|
|
|
1760
1773
|
>>> manager.properties(['PHIE', 'PERM']).filter_intervals("Zones").sums_avg()
|
|
1761
1774
|
>>> # 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
1775
|
|
|
1776
|
+
>>> # No filters - compute stats for full well
|
|
1777
|
+
>>> manager.properties(['PHIE', 'PERM']).sums_avg()
|
|
1778
|
+
"""
|
|
1769
1779
|
result = {}
|
|
1770
1780
|
|
|
1771
1781
|
for well_name, well in self._manager._wells.items():
|
|
@@ -1787,6 +1797,18 @@ class _ManagerMultiPropertyProxy:
|
|
|
1787
1797
|
"""
|
|
1788
1798
|
Compute multi-property sums_avg for a single well.
|
|
1789
1799
|
"""
|
|
1800
|
+
# Check if this well has the required saved intervals (if using saved name)
|
|
1801
|
+
if self._custom_intervals:
|
|
1802
|
+
intervals = self._custom_intervals.get('intervals')
|
|
1803
|
+
if isinstance(intervals, str):
|
|
1804
|
+
# Saved filter name - check if this well has it
|
|
1805
|
+
if intervals not in well._saved_filter_intervals:
|
|
1806
|
+
return None # Skip wells that don't have this saved filter
|
|
1807
|
+
elif isinstance(intervals, dict):
|
|
1808
|
+
# Well-specific intervals - check if this well is in the dict
|
|
1809
|
+
if well.name not in intervals and well.sanitized_name not in intervals:
|
|
1810
|
+
return None # Skip wells not in the dict
|
|
1811
|
+
|
|
1790
1812
|
# Collect results for each property
|
|
1791
1813
|
property_results = {}
|
|
1792
1814
|
|
|
@@ -1798,7 +1820,7 @@ class _ManagerMultiPropertyProxy:
|
|
|
1798
1820
|
if self._custom_intervals:
|
|
1799
1821
|
prop = self._apply_filter_intervals(prop, well)
|
|
1800
1822
|
if prop is None:
|
|
1801
|
-
|
|
1823
|
+
continue # Skip this property if intervals can't be applied
|
|
1802
1824
|
|
|
1803
1825
|
# Apply all filters
|
|
1804
1826
|
for filter_name, insert_boundaries in self._filters:
|
|
@@ -1815,13 +1837,17 @@ class _ManagerMultiPropertyProxy:
|
|
|
1815
1837
|
)
|
|
1816
1838
|
property_results[prop_name] = result
|
|
1817
1839
|
|
|
1818
|
-
except (PropertyNotFoundError, PropertyTypeError, AttributeError, KeyError
|
|
1819
|
-
# Property doesn't exist in this well, skip it
|
|
1840
|
+
except (PropertyNotFoundError, PropertyTypeError, AttributeError, KeyError):
|
|
1841
|
+
# Property doesn't exist in this well or filter error, skip it
|
|
1820
1842
|
pass
|
|
1821
1843
|
|
|
1822
1844
|
if not property_results:
|
|
1823
1845
|
return None
|
|
1824
1846
|
|
|
1847
|
+
# If no filters/intervals, return simple merged result (no grouping)
|
|
1848
|
+
if not self._filters and not self._custom_intervals:
|
|
1849
|
+
return self._merge_flat_results(property_results)
|
|
1850
|
+
|
|
1825
1851
|
# Merge results: nest property-specific stats, keep common stats at group level
|
|
1826
1852
|
return self._merge_property_results(property_results)
|
|
1827
1853
|
|
|
@@ -1869,6 +1895,54 @@ class _ManagerMultiPropertyProxy:
|
|
|
1869
1895
|
save=save
|
|
1870
1896
|
)
|
|
1871
1897
|
|
|
1898
|
+
def _merge_flat_results(self, property_results: dict) -> dict:
|
|
1899
|
+
"""
|
|
1900
|
+
Merge results when no filters are applied (flat structure).
|
|
1901
|
+
|
|
1902
|
+
Returns a single dict with property-specific stats nested under property
|
|
1903
|
+
names and common stats at the top level.
|
|
1904
|
+
|
|
1905
|
+
Parameters
|
|
1906
|
+
----------
|
|
1907
|
+
property_results : dict
|
|
1908
|
+
{property_name: sums_avg_result}
|
|
1909
|
+
|
|
1910
|
+
Returns
|
|
1911
|
+
-------
|
|
1912
|
+
dict
|
|
1913
|
+
{
|
|
1914
|
+
"PropertyA": {"mean": ..., "median": ..., ...},
|
|
1915
|
+
"PropertyB": {"mean": ..., ...},
|
|
1916
|
+
"depth_range": {...},
|
|
1917
|
+
"samples": ...,
|
|
1918
|
+
...
|
|
1919
|
+
}
|
|
1920
|
+
"""
|
|
1921
|
+
if not property_results:
|
|
1922
|
+
return {}
|
|
1923
|
+
|
|
1924
|
+
result = {}
|
|
1925
|
+
|
|
1926
|
+
# Add property-specific stats for each property
|
|
1927
|
+
for prop_name, prop_result in property_results.items():
|
|
1928
|
+
if isinstance(prop_result, dict):
|
|
1929
|
+
# Extract property-specific stats
|
|
1930
|
+
prop_stats = {
|
|
1931
|
+
k: v for k, v in prop_result.items()
|
|
1932
|
+
if k in self.PROPERTY_STATS
|
|
1933
|
+
}
|
|
1934
|
+
if prop_stats:
|
|
1935
|
+
result[prop_name] = prop_stats
|
|
1936
|
+
|
|
1937
|
+
# Add common stats from first property
|
|
1938
|
+
first_result = next(iter(property_results.values()))
|
|
1939
|
+
if isinstance(first_result, dict):
|
|
1940
|
+
for k, v in first_result.items():
|
|
1941
|
+
if k in self.COMMON_STATS:
|
|
1942
|
+
result[k] = v
|
|
1943
|
+
|
|
1944
|
+
return result
|
|
1945
|
+
|
|
1872
1946
|
def _merge_property_results(self, property_results: dict) -> dict:
|
|
1873
1947
|
"""
|
|
1874
1948
|
Merge results from multiple properties.
|
|
@@ -2867,6 +2941,18 @@ class WellDataManager:
|
|
|
2867
2941
|
# Delete sources marked for deletion
|
|
2868
2942
|
well.delete_marked_sources(well_folder)
|
|
2869
2943
|
|
|
2944
|
+
# Save filter intervals if any exist
|
|
2945
|
+
if hasattr(well, '_saved_filter_intervals') and well._saved_filter_intervals:
|
|
2946
|
+
import json
|
|
2947
|
+
intervals_file = well_folder / "intervals.json"
|
|
2948
|
+
with open(intervals_file, 'w') as f:
|
|
2949
|
+
json.dump(well._saved_filter_intervals, f, indent=2)
|
|
2950
|
+
else:
|
|
2951
|
+
# Remove intervals file if no intervals (in case they were deleted)
|
|
2952
|
+
intervals_file = well_folder / "intervals.json"
|
|
2953
|
+
if intervals_file.exists():
|
|
2954
|
+
intervals_file.unlink()
|
|
2955
|
+
|
|
2870
2956
|
# Save templates
|
|
2871
2957
|
if self._templates:
|
|
2872
2958
|
templates_folder = save_path / "templates"
|
|
@@ -2957,6 +3043,20 @@ class WellDataManager:
|
|
|
2957
3043
|
for las_file in las_files:
|
|
2958
3044
|
self.load_las(las_file, silent=True)
|
|
2959
3045
|
|
|
3046
|
+
# Load saved filter intervals if they exist
|
|
3047
|
+
intervals_file = well_folder / "intervals.json"
|
|
3048
|
+
if intervals_file.exists():
|
|
3049
|
+
import json
|
|
3050
|
+
try:
|
|
3051
|
+
with open(intervals_file, 'r') as f:
|
|
3052
|
+
saved_intervals = json.load(f)
|
|
3053
|
+
# Find the well for this folder and set its intervals
|
|
3054
|
+
well_key = well_folder.name # e.g., "well_35_9_16_A"
|
|
3055
|
+
if well_key in self._wells:
|
|
3056
|
+
self._wells[well_key]._saved_filter_intervals = saved_intervals
|
|
3057
|
+
except Exception as e:
|
|
3058
|
+
warnings.warn(f"Could not load intervals from {intervals_file}: {e}")
|
|
3059
|
+
|
|
2960
3060
|
return self
|
|
2961
3061
|
|
|
2962
3062
|
def add_well(self, well_name: str) -> Well:
|
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,8 +1772,12 @@ 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
|
|
1773
1781
|
|
|
1774
1782
|
# If there are secondary properties, group within this interval
|
|
1775
1783
|
if self.secondary_properties:
|
|
@@ -1779,7 +1787,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1779
1787
|
weighted=weighted,
|
|
1780
1788
|
arithmetic=arithmetic,
|
|
1781
1789
|
gross_thickness=gross_thickness,
|
|
1782
|
-
precision=precision
|
|
1790
|
+
precision=precision,
|
|
1791
|
+
zone_intervals=zone_intervals
|
|
1783
1792
|
)
|
|
1784
1793
|
else:
|
|
1785
1794
|
# No secondary properties, compute stats directly for interval
|
|
@@ -1788,7 +1797,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1788
1797
|
weighted=weighted,
|
|
1789
1798
|
arithmetic=arithmetic,
|
|
1790
1799
|
gross_thickness=gross_thickness,
|
|
1791
|
-
precision=precision
|
|
1800
|
+
precision=precision,
|
|
1801
|
+
zone_intervals=zone_intervals
|
|
1792
1802
|
)
|
|
1793
1803
|
|
|
1794
1804
|
return result
|
|
@@ -1905,6 +1915,9 @@ class Property(PropertyOperationsMixin):
|
|
|
1905
1915
|
This allows overlapping intervals where the same depths can
|
|
1906
1916
|
contribute to multiple zones. Zone-level metadata (depth_range, thickness)
|
|
1907
1917
|
is shown at the interval level, and fractions are relative to zone thickness.
|
|
1918
|
+
|
|
1919
|
+
Uses zone-aware intervals that are truncated at zone boundaries to ensure
|
|
1920
|
+
thickness is correctly attributed to each zone.
|
|
1908
1921
|
"""
|
|
1909
1922
|
result = {}
|
|
1910
1923
|
|
|
@@ -1913,13 +1926,16 @@ class Property(PropertyOperationsMixin):
|
|
|
1913
1926
|
top = float(interval['top'])
|
|
1914
1927
|
base = float(interval['base'])
|
|
1915
1928
|
|
|
1916
|
-
#
|
|
1917
|
-
|
|
1929
|
+
# Compute zone-aware intervals truncated at zone boundaries
|
|
1930
|
+
zone_intervals = compute_zone_intervals(self.depth, top, base)
|
|
1931
|
+
|
|
1932
|
+
# Create mask based on zone intervals - includes any sample that
|
|
1933
|
+
# contributes to this zone (even boundary samples with partial intervals)
|
|
1934
|
+
interval_mask = zone_intervals > 0
|
|
1918
1935
|
|
|
1919
|
-
# Calculate zone thickness
|
|
1920
|
-
full_intervals = compute_intervals(self.depth)
|
|
1936
|
+
# Calculate zone thickness using zone-aware intervals
|
|
1921
1937
|
valid_mask = ~np.isnan(self.values) & interval_mask
|
|
1922
|
-
zone_thickness = float(np.sum(
|
|
1938
|
+
zone_thickness = float(np.sum(zone_intervals[valid_mask]))
|
|
1923
1939
|
|
|
1924
1940
|
# Get actual depth range within the interval (where we have data)
|
|
1925
1941
|
if np.any(valid_mask):
|
|
@@ -1944,7 +1960,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1944
1960
|
interval_mask,
|
|
1945
1961
|
gross_thickness=zone_thickness, # Use zone thickness for fractions
|
|
1946
1962
|
precision=precision,
|
|
1947
|
-
include_depth_range=False # Don't include depth_range per facies
|
|
1963
|
+
include_depth_range=False, # Don't include depth_range per facies
|
|
1964
|
+
zone_intervals=zone_intervals # Pass zone-aware intervals
|
|
1948
1965
|
)
|
|
1949
1966
|
else:
|
|
1950
1967
|
# No secondary properties, compute stats directly for interval
|
|
@@ -1952,7 +1969,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1952
1969
|
interval_mask,
|
|
1953
1970
|
gross_thickness=zone_thickness, # Use zone thickness for fractions
|
|
1954
1971
|
precision=precision,
|
|
1955
|
-
include_depth_range=False # Don't include depth_range per facies
|
|
1972
|
+
include_depth_range=False, # Don't include depth_range per facies
|
|
1973
|
+
zone_intervals=zone_intervals # Pass zone-aware intervals
|
|
1956
1974
|
)
|
|
1957
1975
|
|
|
1958
1976
|
# Nest facies stats under 'facies' key for cleaner structure
|
|
@@ -1967,7 +1985,8 @@ class Property(PropertyOperationsMixin):
|
|
|
1967
1985
|
mask: np.ndarray,
|
|
1968
1986
|
gross_thickness: float,
|
|
1969
1987
|
precision: int = 6,
|
|
1970
|
-
include_depth_range: bool = True
|
|
1988
|
+
include_depth_range: bool = True,
|
|
1989
|
+
zone_intervals: Optional[np.ndarray] = None
|
|
1971
1990
|
) -> dict:
|
|
1972
1991
|
"""
|
|
1973
1992
|
Recursively group discrete statistics by secondary properties.
|
|
@@ -1984,6 +2003,9 @@ class Property(PropertyOperationsMixin):
|
|
|
1984
2003
|
Number of decimal places for rounding
|
|
1985
2004
|
include_depth_range : bool, default True
|
|
1986
2005
|
Whether to include depth_range in per-facies stats
|
|
2006
|
+
zone_intervals : np.ndarray, optional
|
|
2007
|
+
Pre-computed zone-aware intervals truncated at zone boundaries.
|
|
2008
|
+
If None, computes intervals using standard midpoint method.
|
|
1987
2009
|
|
|
1988
2010
|
Returns
|
|
1989
2011
|
-------
|
|
@@ -1992,7 +2014,7 @@ class Property(PropertyOperationsMixin):
|
|
|
1992
2014
|
"""
|
|
1993
2015
|
if filter_idx >= len(self.secondary_properties):
|
|
1994
2016
|
# Base case: compute discrete statistics for this group
|
|
1995
|
-
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
|
|
2017
|
+
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range, zone_intervals)
|
|
1996
2018
|
|
|
1997
2019
|
# Get unique values for current filter
|
|
1998
2020
|
current_filter = self.secondary_properties[filter_idx]
|
|
@@ -2002,12 +2024,16 @@ class Property(PropertyOperationsMixin):
|
|
|
2002
2024
|
|
|
2003
2025
|
if len(unique_vals) == 0:
|
|
2004
2026
|
# No valid values, return stats for current mask
|
|
2005
|
-
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range)
|
|
2027
|
+
return self._compute_discrete_stats(mask, gross_thickness, precision, include_depth_range, zone_intervals)
|
|
2006
2028
|
|
|
2007
2029
|
# Group by each unique value
|
|
2008
2030
|
depth_array = self.depth
|
|
2009
2031
|
values_array = self.values
|
|
2010
|
-
|
|
2032
|
+
# Use zone intervals if provided, otherwise compute on full array
|
|
2033
|
+
if zone_intervals is not None:
|
|
2034
|
+
full_intervals = zone_intervals
|
|
2035
|
+
else:
|
|
2036
|
+
full_intervals = compute_intervals(depth_array)
|
|
2011
2037
|
|
|
2012
2038
|
result = {}
|
|
2013
2039
|
for val in unique_vals:
|
|
@@ -2038,7 +2064,7 @@ class Property(PropertyOperationsMixin):
|
|
|
2038
2064
|
key = f"{current_filter.name}_{val:.2f}"
|
|
2039
2065
|
|
|
2040
2066
|
result[key] = self._recursive_discrete_group(
|
|
2041
|
-
filter_idx + 1, sub_mask, group_thickness, precision, include_depth_range
|
|
2067
|
+
filter_idx + 1, sub_mask, group_thickness, precision, include_depth_range, zone_intervals
|
|
2042
2068
|
)
|
|
2043
2069
|
|
|
2044
2070
|
return result
|
|
@@ -2048,7 +2074,8 @@ class Property(PropertyOperationsMixin):
|
|
|
2048
2074
|
mask: np.ndarray,
|
|
2049
2075
|
gross_thickness: float,
|
|
2050
2076
|
precision: int = 6,
|
|
2051
|
-
include_depth_range: bool = True
|
|
2077
|
+
include_depth_range: bool = True,
|
|
2078
|
+
zone_intervals: Optional[np.ndarray] = None
|
|
2052
2079
|
) -> dict:
|
|
2053
2080
|
"""
|
|
2054
2081
|
Compute categorical statistics for discrete property values.
|
|
@@ -2064,6 +2091,9 @@ class Property(PropertyOperationsMixin):
|
|
|
2064
2091
|
include_depth_range : bool, default True
|
|
2065
2092
|
Whether to include depth_range in per-facies stats.
|
|
2066
2093
|
Set to False when using filter_intervals (depth_range shown at zone level).
|
|
2094
|
+
zone_intervals : np.ndarray, optional
|
|
2095
|
+
Pre-computed zone-aware intervals truncated at zone boundaries.
|
|
2096
|
+
If None, computes intervals using standard midpoint method.
|
|
2067
2097
|
|
|
2068
2098
|
Returns
|
|
2069
2099
|
-------
|
|
@@ -2077,9 +2107,12 @@ class Property(PropertyOperationsMixin):
|
|
|
2077
2107
|
values = values_array[mask]
|
|
2078
2108
|
depths = depth_array[mask]
|
|
2079
2109
|
|
|
2080
|
-
#
|
|
2081
|
-
|
|
2082
|
-
|
|
2110
|
+
# Use zone intervals if provided, otherwise compute on full array
|
|
2111
|
+
if zone_intervals is not None:
|
|
2112
|
+
intervals = zone_intervals[mask]
|
|
2113
|
+
else:
|
|
2114
|
+
full_intervals = compute_intervals(depth_array)
|
|
2115
|
+
intervals = full_intervals[mask]
|
|
2083
2116
|
|
|
2084
2117
|
# Find unique discrete values
|
|
2085
2118
|
valid_mask_local = ~np.isnan(values)
|
|
@@ -2132,7 +2165,8 @@ class Property(PropertyOperationsMixin):
|
|
|
2132
2165
|
weighted: bool,
|
|
2133
2166
|
arithmetic: bool,
|
|
2134
2167
|
gross_thickness: float,
|
|
2135
|
-
precision: int = 6
|
|
2168
|
+
precision: int = 6,
|
|
2169
|
+
zone_intervals: Optional[np.ndarray] = None
|
|
2136
2170
|
) -> dict:
|
|
2137
2171
|
"""
|
|
2138
2172
|
Recursively group by secondary properties.
|
|
@@ -2151,6 +2185,9 @@ class Property(PropertyOperationsMixin):
|
|
|
2151
2185
|
Total gross thickness for fraction calculation
|
|
2152
2186
|
precision : int, default 6
|
|
2153
2187
|
Number of decimal places for rounding
|
|
2188
|
+
zone_intervals : np.ndarray, optional
|
|
2189
|
+
Pre-computed zone-aware intervals truncated at zone boundaries.
|
|
2190
|
+
If None, computes intervals using standard midpoint method.
|
|
2154
2191
|
|
|
2155
2192
|
Returns
|
|
2156
2193
|
-------
|
|
@@ -2159,7 +2196,7 @@ class Property(PropertyOperationsMixin):
|
|
|
2159
2196
|
"""
|
|
2160
2197
|
if filter_idx >= len(self.secondary_properties):
|
|
2161
2198
|
# Base case: compute statistics for this group
|
|
2162
|
-
return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision)
|
|
2199
|
+
return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision, zone_intervals)
|
|
2163
2200
|
|
|
2164
2201
|
# Get unique values for current filter
|
|
2165
2202
|
current_filter = self.secondary_properties[filter_idx]
|
|
@@ -2170,14 +2207,18 @@ class Property(PropertyOperationsMixin):
|
|
|
2170
2207
|
|
|
2171
2208
|
if len(unique_vals) == 0:
|
|
2172
2209
|
# No valid values, return stats for current mask
|
|
2173
|
-
return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision)
|
|
2210
|
+
return self._compute_stats(mask, weighted, arithmetic, gross_thickness, precision, zone_intervals)
|
|
2174
2211
|
|
|
2175
2212
|
# Calculate parent thickness BEFORE subdividing
|
|
2176
2213
|
# This becomes the gross_thickness for all child groups
|
|
2177
2214
|
# Cache property access to avoid overhead
|
|
2178
2215
|
depth_array = self.depth
|
|
2179
2216
|
values_array = self.values
|
|
2180
|
-
|
|
2217
|
+
# Use zone intervals if provided, otherwise compute on full array
|
|
2218
|
+
if zone_intervals is not None:
|
|
2219
|
+
parent_intervals = zone_intervals
|
|
2220
|
+
else:
|
|
2221
|
+
parent_intervals = compute_intervals(depth_array)
|
|
2181
2222
|
parent_valid = mask & ~np.isnan(values_array)
|
|
2182
2223
|
parent_thickness = float(np.sum(parent_intervals[parent_valid]))
|
|
2183
2224
|
|
|
@@ -2212,7 +2253,7 @@ class Property(PropertyOperationsMixin):
|
|
|
2212
2253
|
key = f"{current_filter.name}_{val:.2f}"
|
|
2213
2254
|
|
|
2214
2255
|
result[key] = self._recursive_group(
|
|
2215
|
-
filter_idx + 1, sub_mask, weighted, arithmetic, parent_thickness, precision
|
|
2256
|
+
filter_idx + 1, sub_mask, weighted, arithmetic, parent_thickness, precision, zone_intervals
|
|
2216
2257
|
)
|
|
2217
2258
|
|
|
2218
2259
|
return result
|
|
@@ -2223,7 +2264,8 @@ class Property(PropertyOperationsMixin):
|
|
|
2223
2264
|
weighted: bool = True,
|
|
2224
2265
|
arithmetic: bool = False,
|
|
2225
2266
|
gross_thickness: float = 0.0,
|
|
2226
|
-
precision: int = 6
|
|
2267
|
+
precision: int = 6,
|
|
2268
|
+
zone_intervals: Optional[np.ndarray] = None
|
|
2227
2269
|
) -> dict:
|
|
2228
2270
|
"""
|
|
2229
2271
|
Compute statistics for values selected by mask.
|
|
@@ -2244,6 +2286,9 @@ class Property(PropertyOperationsMixin):
|
|
|
2244
2286
|
Total gross thickness for fraction calculation
|
|
2245
2287
|
precision : int, default 6
|
|
2246
2288
|
Number of decimal places for rounding
|
|
2289
|
+
zone_intervals : np.ndarray, optional
|
|
2290
|
+
Pre-computed zone-aware intervals truncated at zone boundaries.
|
|
2291
|
+
If None, computes intervals using standard midpoint method.
|
|
2247
2292
|
|
|
2248
2293
|
Returns
|
|
2249
2294
|
-------
|
|
@@ -2263,12 +2308,17 @@ class Property(PropertyOperationsMixin):
|
|
|
2263
2308
|
values = values_array[mask]
|
|
2264
2309
|
valid = values[~np.isnan(values)]
|
|
2265
2310
|
|
|
2266
|
-
#
|
|
2267
|
-
#
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2311
|
+
# Use zone intervals if provided, otherwise compute on full array
|
|
2312
|
+
# Zone intervals are truncated at zone boundaries for accurate thickness
|
|
2313
|
+
if zone_intervals is not None:
|
|
2314
|
+
intervals = zone_intervals[mask]
|
|
2315
|
+
else:
|
|
2316
|
+
# Compute depth intervals on FULL depth array first, then mask
|
|
2317
|
+
# This is critical! Intervals must be computed on full grid so that
|
|
2318
|
+
# zone boundary samples get correct weights based on their neighbors
|
|
2319
|
+
# in the full sequence, not just within their zone.
|
|
2320
|
+
full_intervals = compute_intervals(depth_array)
|
|
2321
|
+
intervals = full_intervals[mask]
|
|
2272
2322
|
valid_mask_local = ~np.isnan(values)
|
|
2273
2323
|
valid_intervals = intervals[valid_mask_local]
|
|
2274
2324
|
|
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=Jx0w1KpxRB2Oex6bC0FQFZOlJkoTKNexW3HMXxndL68,136713
|
|
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=TkBuqgunIGIGdsLtWtQkwwXPdh0yqY37PgO_150FilA,103782
|
|
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.149.dist-info/METADATA,sha256=g__8aYidDAd_oKi-MQ7PLGX0Ss63NHRbxjudCfdGtF8,63473
|
|
13
|
+
well_log_toolkit-0.1.149.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
14
|
+
well_log_toolkit-0.1.149.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
|
|
15
|
+
well_log_toolkit-0.1.149.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|