well-log-toolkit 0.1.147__tar.gz → 0.1.148__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/PKG-INFO +1 -1
  2. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/pyproject.toml +1 -1
  3. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit/manager.py +109 -9
  4. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit.egg-info/PKG-INFO +1 -1
  5. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/README.md +0 -0
  6. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/setup.cfg +0 -0
  7. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit/__init__.py +0 -0
  8. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit/exceptions.py +0 -0
  9. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit/las_file.py +0 -0
  10. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit/operations.py +0 -0
  11. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit/property.py +0 -0
  12. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit/regression.py +0 -0
  13. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit/statistics.py +0 -0
  14. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit/utils.py +0 -0
  15. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit/visualization.py +0 -0
  16. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit/well.py +0 -0
  17. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
  18. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
  19. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit.egg-info/requires.txt +0 -0
  20. {well_log_toolkit-0.1.147 → well_log_toolkit-0.1.148}/well_log_toolkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.147
3
+ Version: 0.1.148
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "well-log-toolkit"
7
- version = "0.1.147"
7
+ version = "0.1.148"
8
8
  description = "Fast LAS file processing with lazy loading and filtering for well log analysis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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
- return None # Well doesn't have the saved intervals
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, ValueError):
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.147
3
+ Version: 0.1.148
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