well-log-toolkit 0.1.146__py3-none-any.whl → 0.1.148__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 +494 -0
- well_log_toolkit/property.py +4 -0
- {well_log_toolkit-0.1.146.dist-info → well_log_toolkit-0.1.148.dist-info}/METADATA +1 -1
- {well_log_toolkit-0.1.146.dist-info → well_log_toolkit-0.1.148.dist-info}/RECORD +6 -6
- {well_log_toolkit-0.1.146.dist-info → well_log_toolkit-0.1.148.dist-info}/WHEEL +0 -0
- {well_log_toolkit-0.1.146.dist-info → well_log_toolkit-0.1.148.dist-info}/top_level.txt +0 -0
well_log_toolkit/manager.py
CHANGED
|
@@ -1618,6 +1618,420 @@ class _ManagerPropertyProxy:
|
|
|
1618
1618
|
)
|
|
1619
1619
|
|
|
1620
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 __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
|
+
|
|
1662
|
+
def filter(
|
|
1663
|
+
self,
|
|
1664
|
+
property_name: str,
|
|
1665
|
+
insert_boundaries: Optional[bool] = None
|
|
1666
|
+
) -> '_ManagerMultiPropertyProxy':
|
|
1667
|
+
"""
|
|
1668
|
+
Add a filter (discrete property) to group statistics by.
|
|
1669
|
+
|
|
1670
|
+
Parameters
|
|
1671
|
+
----------
|
|
1672
|
+
property_name : str
|
|
1673
|
+
Name of discrete property to group by
|
|
1674
|
+
insert_boundaries : bool, optional
|
|
1675
|
+
Whether to insert boundary values at filter transitions
|
|
1676
|
+
|
|
1677
|
+
Returns
|
|
1678
|
+
-------
|
|
1679
|
+
_ManagerMultiPropertyProxy
|
|
1680
|
+
New proxy with filter added
|
|
1681
|
+
"""
|
|
1682
|
+
new_filters = self._filters + [(property_name, insert_boundaries)]
|
|
1683
|
+
return _ManagerMultiPropertyProxy(
|
|
1684
|
+
self._manager, self._property_names, new_filters, self._custom_intervals
|
|
1685
|
+
)
|
|
1686
|
+
|
|
1687
|
+
def filter_intervals(
|
|
1688
|
+
self,
|
|
1689
|
+
intervals: Union[str, list, dict],
|
|
1690
|
+
name: str = "Custom_Intervals",
|
|
1691
|
+
insert_boundaries: Optional[bool] = None,
|
|
1692
|
+
save: Optional[str] = None
|
|
1693
|
+
) -> '_ManagerMultiPropertyProxy':
|
|
1694
|
+
"""
|
|
1695
|
+
Filter by custom depth intervals.
|
|
1696
|
+
|
|
1697
|
+
Parameters
|
|
1698
|
+
----------
|
|
1699
|
+
intervals : str, list, or dict
|
|
1700
|
+
- str: Name of saved intervals to retrieve from each well
|
|
1701
|
+
- list: List of interval dicts [{"name": "Zone_A", "top": 2500, "base": 2700}, ...]
|
|
1702
|
+
- dict: Well-specific intervals {"well_name": [...], ...}
|
|
1703
|
+
name : str, default "Custom_Intervals"
|
|
1704
|
+
Name for the interval filter in results
|
|
1705
|
+
insert_boundaries : bool, optional
|
|
1706
|
+
Whether to insert boundary values at interval edges
|
|
1707
|
+
save : str, optional
|
|
1708
|
+
If provided, save intervals to wells with this name
|
|
1709
|
+
|
|
1710
|
+
Returns
|
|
1711
|
+
-------
|
|
1712
|
+
_ManagerMultiPropertyProxy
|
|
1713
|
+
New proxy with custom intervals set
|
|
1714
|
+
"""
|
|
1715
|
+
intervals_config = {
|
|
1716
|
+
'intervals': intervals,
|
|
1717
|
+
'name': name,
|
|
1718
|
+
'insert_boundaries': insert_boundaries,
|
|
1719
|
+
'save': save
|
|
1720
|
+
}
|
|
1721
|
+
return _ManagerMultiPropertyProxy(
|
|
1722
|
+
self._manager, self._property_names, self._filters, intervals_config
|
|
1723
|
+
)
|
|
1724
|
+
|
|
1725
|
+
def sums_avg(
|
|
1726
|
+
self,
|
|
1727
|
+
weighted: Optional[bool] = None,
|
|
1728
|
+
arithmetic: Optional[bool] = None,
|
|
1729
|
+
precision: int = 6
|
|
1730
|
+
) -> dict:
|
|
1731
|
+
"""
|
|
1732
|
+
Compute statistics for multiple properties across all wells.
|
|
1733
|
+
|
|
1734
|
+
Multi-property results nest property-specific stats (mean, median, etc.)
|
|
1735
|
+
under each property name, while common stats (depth_range, samples,
|
|
1736
|
+
thickness, etc.) remain at the group level.
|
|
1737
|
+
|
|
1738
|
+
Parameters
|
|
1739
|
+
----------
|
|
1740
|
+
weighted : bool, optional
|
|
1741
|
+
Include depth-weighted statistics.
|
|
1742
|
+
Default: True for continuous/discrete, False for sampled
|
|
1743
|
+
arithmetic : bool, optional
|
|
1744
|
+
Include arithmetic (unweighted) statistics.
|
|
1745
|
+
Default: False for continuous/discrete, True for sampled
|
|
1746
|
+
precision : int, default 6
|
|
1747
|
+
Number of decimal places for rounding numeric results
|
|
1748
|
+
|
|
1749
|
+
Returns
|
|
1750
|
+
-------
|
|
1751
|
+
dict
|
|
1752
|
+
Nested dictionary with structure:
|
|
1753
|
+
{
|
|
1754
|
+
"well_name": {
|
|
1755
|
+
"interval_name": { # if using filter_intervals
|
|
1756
|
+
"filter_value": {
|
|
1757
|
+
"PropertyA": {"mean": ..., "median": ..., ...},
|
|
1758
|
+
"PropertyB": {"mean": ..., "median": ..., ...},
|
|
1759
|
+
"depth_range": {...},
|
|
1760
|
+
"samples": ...,
|
|
1761
|
+
"thickness": ...,
|
|
1762
|
+
...
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
Examples
|
|
1769
|
+
--------
|
|
1770
|
+
>>> manager.properties(['PHIE', 'PERM']).filter('Facies').sums_avg()
|
|
1771
|
+
>>> # Returns stats for both properties grouped by facies
|
|
1772
|
+
|
|
1773
|
+
>>> manager.properties(['PHIE', 'PERM']).filter_intervals("Zones").sums_avg()
|
|
1774
|
+
>>> # Returns stats for both properties grouped by custom intervals
|
|
1775
|
+
|
|
1776
|
+
>>> # No filters - compute stats for full well
|
|
1777
|
+
>>> manager.properties(['PHIE', 'PERM']).sums_avg()
|
|
1778
|
+
"""
|
|
1779
|
+
result = {}
|
|
1780
|
+
|
|
1781
|
+
for well_name, well in self._manager._wells.items():
|
|
1782
|
+
well_result = self._compute_sums_avg_for_well(
|
|
1783
|
+
well, weighted, arithmetic, precision
|
|
1784
|
+
)
|
|
1785
|
+
if well_result is not None:
|
|
1786
|
+
result[well_name] = well_result
|
|
1787
|
+
|
|
1788
|
+
return _sanitize_for_json(result)
|
|
1789
|
+
|
|
1790
|
+
def _compute_sums_avg_for_well(
|
|
1791
|
+
self,
|
|
1792
|
+
well,
|
|
1793
|
+
weighted: Optional[bool],
|
|
1794
|
+
arithmetic: Optional[bool],
|
|
1795
|
+
precision: int
|
|
1796
|
+
):
|
|
1797
|
+
"""
|
|
1798
|
+
Compute multi-property sums_avg for a single well.
|
|
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
|
+
|
|
1812
|
+
# Collect results for each property
|
|
1813
|
+
property_results = {}
|
|
1814
|
+
|
|
1815
|
+
for prop_name in self._property_names:
|
|
1816
|
+
try:
|
|
1817
|
+
prop = well.get_property(prop_name)
|
|
1818
|
+
|
|
1819
|
+
# Apply filter_intervals if set
|
|
1820
|
+
if self._custom_intervals:
|
|
1821
|
+
prop = self._apply_filter_intervals(prop, well)
|
|
1822
|
+
if prop is None:
|
|
1823
|
+
continue # Skip this property if intervals can't be applied
|
|
1824
|
+
|
|
1825
|
+
# Apply all filters
|
|
1826
|
+
for filter_name, insert_boundaries in self._filters:
|
|
1827
|
+
if insert_boundaries is not None:
|
|
1828
|
+
prop = prop.filter(filter_name, insert_boundaries=insert_boundaries)
|
|
1829
|
+
else:
|
|
1830
|
+
prop = prop.filter(filter_name)
|
|
1831
|
+
|
|
1832
|
+
# Compute sums_avg
|
|
1833
|
+
result = prop.sums_avg(
|
|
1834
|
+
weighted=weighted,
|
|
1835
|
+
arithmetic=arithmetic,
|
|
1836
|
+
precision=precision
|
|
1837
|
+
)
|
|
1838
|
+
property_results[prop_name] = result
|
|
1839
|
+
|
|
1840
|
+
except (PropertyNotFoundError, PropertyTypeError, AttributeError, KeyError):
|
|
1841
|
+
# Property doesn't exist in this well or filter error, skip it
|
|
1842
|
+
pass
|
|
1843
|
+
|
|
1844
|
+
if not property_results:
|
|
1845
|
+
return None
|
|
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
|
+
|
|
1851
|
+
# Merge results: nest property-specific stats, keep common stats at group level
|
|
1852
|
+
return self._merge_property_results(property_results)
|
|
1853
|
+
|
|
1854
|
+
def _apply_filter_intervals(self, prop, well):
|
|
1855
|
+
"""
|
|
1856
|
+
Apply filter_intervals to a property if custom_intervals is set.
|
|
1857
|
+
|
|
1858
|
+
Returns None if the well doesn't have the required saved intervals.
|
|
1859
|
+
"""
|
|
1860
|
+
if not self._custom_intervals:
|
|
1861
|
+
return prop
|
|
1862
|
+
|
|
1863
|
+
intervals_config = self._custom_intervals
|
|
1864
|
+
intervals = intervals_config['intervals']
|
|
1865
|
+
name = intervals_config['name']
|
|
1866
|
+
insert_boundaries = intervals_config['insert_boundaries']
|
|
1867
|
+
save = intervals_config['save']
|
|
1868
|
+
|
|
1869
|
+
# Resolve intervals for this well
|
|
1870
|
+
if isinstance(intervals, str):
|
|
1871
|
+
# Saved filter name - check if this well has it
|
|
1872
|
+
if intervals not in well._saved_filter_intervals:
|
|
1873
|
+
return None # Skip wells that don't have this saved filter
|
|
1874
|
+
well_intervals = intervals
|
|
1875
|
+
elif isinstance(intervals, dict):
|
|
1876
|
+
# Well-specific intervals
|
|
1877
|
+
well_intervals = None
|
|
1878
|
+
if well.name in intervals:
|
|
1879
|
+
well_intervals = intervals[well.name]
|
|
1880
|
+
elif well.sanitized_name in intervals:
|
|
1881
|
+
well_intervals = intervals[well.sanitized_name]
|
|
1882
|
+
if well_intervals is None:
|
|
1883
|
+
return None # Skip wells not in the dict
|
|
1884
|
+
elif isinstance(intervals, list):
|
|
1885
|
+
# Direct list of intervals
|
|
1886
|
+
well_intervals = intervals
|
|
1887
|
+
else:
|
|
1888
|
+
return None
|
|
1889
|
+
|
|
1890
|
+
# Apply filter_intervals
|
|
1891
|
+
return prop.filter_intervals(
|
|
1892
|
+
well_intervals,
|
|
1893
|
+
name=name,
|
|
1894
|
+
insert_boundaries=insert_boundaries,
|
|
1895
|
+
save=save
|
|
1896
|
+
)
|
|
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
|
+
|
|
1946
|
+
def _merge_property_results(self, property_results: dict) -> dict:
|
|
1947
|
+
"""
|
|
1948
|
+
Merge results from multiple properties.
|
|
1949
|
+
|
|
1950
|
+
Nests property-specific stats under property names while keeping
|
|
1951
|
+
common stats at the group level.
|
|
1952
|
+
|
|
1953
|
+
Parameters
|
|
1954
|
+
----------
|
|
1955
|
+
property_results : dict
|
|
1956
|
+
{property_name: sums_avg_result}
|
|
1957
|
+
|
|
1958
|
+
Returns
|
|
1959
|
+
-------
|
|
1960
|
+
dict
|
|
1961
|
+
Merged result with structure:
|
|
1962
|
+
{
|
|
1963
|
+
"group_value": {
|
|
1964
|
+
"PropertyA": {"mean": ..., ...},
|
|
1965
|
+
"PropertyB": {"mean": ..., ...},
|
|
1966
|
+
"depth_range": {...},
|
|
1967
|
+
"samples": ...,
|
|
1968
|
+
...
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
"""
|
|
1972
|
+
if not property_results:
|
|
1973
|
+
return {}
|
|
1974
|
+
|
|
1975
|
+
# Use first property result as the structure template
|
|
1976
|
+
first_prop = next(iter(property_results.keys()))
|
|
1977
|
+
first_result = property_results[first_prop]
|
|
1978
|
+
|
|
1979
|
+
return self._merge_recursive(property_results, first_result)
|
|
1980
|
+
|
|
1981
|
+
def _merge_recursive(self, property_results: dict, template: dict) -> dict:
|
|
1982
|
+
"""
|
|
1983
|
+
Recursively merge property results following the template structure.
|
|
1984
|
+
"""
|
|
1985
|
+
result = {}
|
|
1986
|
+
|
|
1987
|
+
for key, value in template.items():
|
|
1988
|
+
if isinstance(value, dict):
|
|
1989
|
+
# Check if this is a stats dict (has property-specific keys)
|
|
1990
|
+
if any(k in value for k in self.PROPERTY_STATS):
|
|
1991
|
+
# This is a leaf stats dict - merge property stats here
|
|
1992
|
+
merged = {}
|
|
1993
|
+
|
|
1994
|
+
# Add property-specific stats for each property
|
|
1995
|
+
for prop_name, prop_result in property_results.items():
|
|
1996
|
+
# Navigate to the same key in this property's result
|
|
1997
|
+
prop_value = self._get_nested_value(prop_result, key)
|
|
1998
|
+
if prop_value and isinstance(prop_value, dict):
|
|
1999
|
+
# Extract property-specific stats
|
|
2000
|
+
prop_stats = {
|
|
2001
|
+
k: v for k, v in prop_value.items()
|
|
2002
|
+
if k in self.PROPERTY_STATS
|
|
2003
|
+
}
|
|
2004
|
+
if prop_stats:
|
|
2005
|
+
merged[prop_name] = prop_stats
|
|
2006
|
+
|
|
2007
|
+
# Add common stats from the first property
|
|
2008
|
+
for k, v in value.items():
|
|
2009
|
+
if k in self.COMMON_STATS:
|
|
2010
|
+
merged[k] = v
|
|
2011
|
+
|
|
2012
|
+
result[key] = merged
|
|
2013
|
+
else:
|
|
2014
|
+
# This is an intermediate nesting level - recurse
|
|
2015
|
+
# Collect corresponding sub-dicts from all properties
|
|
2016
|
+
sub_property_results = {}
|
|
2017
|
+
for prop_name, prop_result in property_results.items():
|
|
2018
|
+
prop_value = self._get_nested_value(prop_result, key)
|
|
2019
|
+
if prop_value and isinstance(prop_value, dict):
|
|
2020
|
+
sub_property_results[prop_name] = prop_value
|
|
2021
|
+
|
|
2022
|
+
if sub_property_results:
|
|
2023
|
+
result[key] = self._merge_recursive(sub_property_results, value)
|
|
2024
|
+
else:
|
|
2025
|
+
# Non-dict value, just copy from template
|
|
2026
|
+
result[key] = value
|
|
2027
|
+
|
|
2028
|
+
return result
|
|
2029
|
+
|
|
2030
|
+
def _get_nested_value(self, d: dict, key: str):
|
|
2031
|
+
"""Get value from dict, returning None if key doesn't exist."""
|
|
2032
|
+
return d.get(key) if isinstance(d, dict) else None
|
|
2033
|
+
|
|
2034
|
+
|
|
1621
2035
|
class WellDataManager:
|
|
1622
2036
|
"""
|
|
1623
2037
|
Global orchestrator for multi-well analysis.
|
|
@@ -1710,6 +2124,60 @@ class WellDataManager:
|
|
|
1710
2124
|
# Return a proxy that can be used for operations across all wells
|
|
1711
2125
|
return _ManagerPropertyProxy(self, name)
|
|
1712
2126
|
|
|
2127
|
+
def properties(self, property_names: list[str]) -> _ManagerMultiPropertyProxy:
|
|
2128
|
+
"""
|
|
2129
|
+
Create a multi-property proxy for computing statistics across multiple properties.
|
|
2130
|
+
|
|
2131
|
+
This allows computing statistics for multiple properties at once, with
|
|
2132
|
+
property-specific stats (mean, median, etc.) nested under property names
|
|
2133
|
+
and common stats (depth_range, samples, thickness, etc.) at the group level.
|
|
2134
|
+
|
|
2135
|
+
Parameters
|
|
2136
|
+
----------
|
|
2137
|
+
property_names : list[str]
|
|
2138
|
+
List of property names to include in statistics
|
|
2139
|
+
|
|
2140
|
+
Returns
|
|
2141
|
+
-------
|
|
2142
|
+
_ManagerMultiPropertyProxy
|
|
2143
|
+
Proxy that supports filter(), filter_intervals(), and sums_avg()
|
|
2144
|
+
|
|
2145
|
+
Examples
|
|
2146
|
+
--------
|
|
2147
|
+
>>> # Compute stats for multiple properties grouped by facies
|
|
2148
|
+
>>> manager.properties(['PHIE', 'PERM']).filter('Facies').sums_avg()
|
|
2149
|
+
>>> # Returns:
|
|
2150
|
+
>>> # {
|
|
2151
|
+
>>> # "well_A": {
|
|
2152
|
+
>>> # "Sand": {
|
|
2153
|
+
>>> # "PHIE": {"mean": 0.18, "median": 0.17, ...},
|
|
2154
|
+
>>> # "PERM": {"mean": 150, "median": 120, ...},
|
|
2155
|
+
>>> # "depth_range": {...},
|
|
2156
|
+
>>> # "samples": 387,
|
|
2157
|
+
>>> # "thickness": 29.4,
|
|
2158
|
+
>>> # ...
|
|
2159
|
+
>>> # }
|
|
2160
|
+
>>> # }
|
|
2161
|
+
>>> # }
|
|
2162
|
+
|
|
2163
|
+
>>> # With custom intervals
|
|
2164
|
+
>>> manager.properties(['PHIE', 'PERM']).filter('Facies').filter_intervals("Zones").sums_avg()
|
|
2165
|
+
>>> # Returns:
|
|
2166
|
+
>>> # {
|
|
2167
|
+
>>> # "well_A": {
|
|
2168
|
+
>>> # "Zone_1": {
|
|
2169
|
+
>>> # "Sand": {
|
|
2170
|
+
>>> # "PHIE": {"mean": 0.18, ...},
|
|
2171
|
+
>>> # "PERM": {"mean": 150, ...},
|
|
2172
|
+
>>> # "depth_range": {...},
|
|
2173
|
+
>>> # ...
|
|
2174
|
+
>>> # }
|
|
2175
|
+
>>> # }
|
|
2176
|
+
>>> # }
|
|
2177
|
+
>>> # }
|
|
2178
|
+
"""
|
|
2179
|
+
return _ManagerMultiPropertyProxy(self, property_names)
|
|
2180
|
+
|
|
1713
2181
|
def load_las(
|
|
1714
2182
|
self,
|
|
1715
2183
|
filepath: Union[str, Path, list[Union[str, Path]]],
|
|
@@ -2473,6 +2941,18 @@ class WellDataManager:
|
|
|
2473
2941
|
# Delete sources marked for deletion
|
|
2474
2942
|
well.delete_marked_sources(well_folder)
|
|
2475
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
|
+
|
|
2476
2956
|
# Save templates
|
|
2477
2957
|
if self._templates:
|
|
2478
2958
|
templates_folder = save_path / "templates"
|
|
@@ -2563,6 +3043,20 @@ class WellDataManager:
|
|
|
2563
3043
|
for las_file in las_files:
|
|
2564
3044
|
self.load_las(las_file, silent=True)
|
|
2565
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
|
+
|
|
2566
3060
|
return self
|
|
2567
3061
|
|
|
2568
3062
|
def add_well(self, well_name: str) -> Well:
|
well_log_toolkit/property.py
CHANGED
|
@@ -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(
|
|
@@ -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=GsiD9c4SfBw8ar7ZJXS0NNejPlpvFRHKck_eBR2lLmo,100965
|
|
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.148.dist-info/METADATA,sha256=k7HnPCJ1iza1s8kvTFazQhfvYziyfVQ24JJYxWjrtvQ,63473
|
|
13
|
+
well_log_toolkit-0.1.148.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
14
|
+
well_log_toolkit-0.1.148.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
|
|
15
|
+
well_log_toolkit-0.1.148.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|