well-log-toolkit 0.1.140__tar.gz → 0.1.141__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.140 → well_log_toolkit-0.1.141}/PKG-INFO +1 -1
  2. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/pyproject.toml +1 -1
  3. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit/property.py +225 -0
  4. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit.egg-info/PKG-INFO +1 -1
  5. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/README.md +0 -0
  6. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/setup.cfg +0 -0
  7. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit/__init__.py +0 -0
  8. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit/exceptions.py +0 -0
  9. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit/las_file.py +0 -0
  10. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit/manager.py +0 -0
  11. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit/operations.py +0 -0
  12. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit/regression.py +0 -0
  13. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit/statistics.py +0 -0
  14. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit/utils.py +0 -0
  15. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit/visualization.py +0 -0
  16. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit/well.py +0 -0
  17. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
  18. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
  19. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/well_log_toolkit.egg-info/requires.txt +0 -0
  20. {well_log_toolkit-0.1.140 → well_log_toolkit-0.1.141}/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.140
3
+ Version: 0.1.141
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.140"
7
+ version = "0.1.141"
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"
@@ -1520,6 +1520,231 @@ class Property(PropertyOperationsMixin):
1520
1520
  precision=precision
1521
1521
  )
1522
1522
 
1523
+ def discrete_summary(self, precision: int = 6) -> dict:
1524
+ """
1525
+ Compute summary statistics for discrete/categorical properties.
1526
+
1527
+ This method is designed for discrete logs (like facies, lithology flags,
1528
+ or net/gross indicators) where categorical statistics are more meaningful
1529
+ than continuous statistics like mean or standard deviation.
1530
+
1531
+ Parameters
1532
+ ----------
1533
+ precision : int, default 6
1534
+ Number of decimal places for rounding numeric results
1535
+
1536
+ Returns
1537
+ -------
1538
+ dict
1539
+ Nested dictionary with statistics for each discrete value.
1540
+ If secondary properties (filters) exist, the structure is hierarchical.
1541
+
1542
+ For each discrete value, includes:
1543
+ - label: Human-readable name (if labels defined)
1544
+ - code: Numeric code for this category
1545
+ - count: Number of samples with this value
1546
+ - thickness: Total depth interval (meters) for this category
1547
+ - fraction: Proportion of total thickness (0-1)
1548
+ - depth_range: {min, max} depth extent
1549
+
1550
+ Examples
1551
+ --------
1552
+ >>> # Simple discrete summary (no filters)
1553
+ >>> facies = well.get_property('Facies')
1554
+ >>> stats = facies.discrete_summary()
1555
+ >>> # {'Sand': {'code': 1, 'count': 150, 'thickness': 25.5, 'fraction': 0.45, ...},
1556
+ >>> # 'Shale': {'code': 2, 'count': 180, 'thickness': 30.8, 'fraction': 0.55, ...}}
1557
+
1558
+ >>> # Grouped by zones
1559
+ >>> filtered = facies.filter('Well_Tops')
1560
+ >>> stats = filtered.discrete_summary()
1561
+ >>> # {'Zone_A': {'Sand': {...}, 'Shale': {...}},
1562
+ >>> # 'Zone_B': {'Sand': {...}, 'Shale': {...}}}
1563
+
1564
+ Notes
1565
+ -----
1566
+ For continuous properties, use `sums_avg()` instead.
1567
+ """
1568
+ # Calculate gross thickness for fraction calculation
1569
+ full_intervals = compute_intervals(self.depth)
1570
+ valid_mask = ~np.isnan(self.values)
1571
+ gross_thickness = float(np.sum(full_intervals[valid_mask]))
1572
+
1573
+ if not self.secondary_properties:
1574
+ # No filters, compute stats for all discrete values
1575
+ return self._compute_discrete_stats(
1576
+ np.ones(len(self.depth), dtype=bool),
1577
+ gross_thickness=gross_thickness,
1578
+ precision=precision
1579
+ )
1580
+
1581
+ # Build hierarchical grouping
1582
+ return self._recursive_discrete_group(
1583
+ 0,
1584
+ np.ones(len(self.depth), dtype=bool),
1585
+ gross_thickness=gross_thickness,
1586
+ precision=precision
1587
+ )
1588
+
1589
+ def _recursive_discrete_group(
1590
+ self,
1591
+ filter_idx: int,
1592
+ mask: np.ndarray,
1593
+ gross_thickness: float,
1594
+ precision: int = 6
1595
+ ) -> dict:
1596
+ """
1597
+ Recursively group discrete statistics by secondary properties.
1598
+
1599
+ Parameters
1600
+ ----------
1601
+ filter_idx : int
1602
+ Index of current secondary property
1603
+ mask : np.ndarray
1604
+ Boolean mask for current group
1605
+ gross_thickness : float
1606
+ Total gross thickness for fraction calculation
1607
+ precision : int, default 6
1608
+ Number of decimal places for rounding
1609
+
1610
+ Returns
1611
+ -------
1612
+ dict
1613
+ Discrete stats dict or nested dict of stats
1614
+ """
1615
+ if filter_idx >= len(self.secondary_properties):
1616
+ # Base case: compute discrete statistics for this group
1617
+ return self._compute_discrete_stats(mask, gross_thickness, precision)
1618
+
1619
+ # Get unique values for current filter
1620
+ current_filter = self.secondary_properties[filter_idx]
1621
+ current_filter_values = current_filter.values
1622
+ filter_values = current_filter_values[mask]
1623
+ unique_vals = np.unique(filter_values[~np.isnan(filter_values)])
1624
+
1625
+ if len(unique_vals) == 0:
1626
+ # No valid values, return stats for current mask
1627
+ return self._compute_discrete_stats(mask, gross_thickness, precision)
1628
+
1629
+ # Calculate parent thickness for fraction calculation in child groups
1630
+ depth_array = self.depth
1631
+ values_array = self.values
1632
+ parent_intervals = compute_intervals(depth_array)
1633
+ parent_valid = mask & ~np.isnan(values_array)
1634
+ parent_thickness = float(np.sum(parent_intervals[parent_valid]))
1635
+
1636
+ # Group by each unique value
1637
+ result = {}
1638
+ for val in unique_vals:
1639
+ sub_mask = mask & (current_filter_values == val)
1640
+
1641
+ # Create readable key with label if available
1642
+ if current_filter.type == 'discrete':
1643
+ int_val = int(val)
1644
+ else:
1645
+ int_val = int(val) if val == int(val) else None
1646
+
1647
+ if current_filter.labels is not None:
1648
+ if int_val is not None and int_val in current_filter.labels:
1649
+ key = current_filter.labels[int_val]
1650
+ elif val in current_filter.labels:
1651
+ key = current_filter.labels[val]
1652
+ elif int_val is not None:
1653
+ key = f"{current_filter.name}_{int_val}"
1654
+ else:
1655
+ key = f"{current_filter.name}_{val:.2f}"
1656
+ elif int_val is not None:
1657
+ key = f"{current_filter.name}_{int_val}"
1658
+ else:
1659
+ key = f"{current_filter.name}_{val:.2f}"
1660
+
1661
+ result[key] = self._recursive_discrete_group(
1662
+ filter_idx + 1, sub_mask, parent_thickness, precision
1663
+ )
1664
+
1665
+ return result
1666
+
1667
+ def _compute_discrete_stats(
1668
+ self,
1669
+ mask: np.ndarray,
1670
+ gross_thickness: float,
1671
+ precision: int = 6
1672
+ ) -> dict:
1673
+ """
1674
+ Compute categorical statistics for discrete property values.
1675
+
1676
+ Parameters
1677
+ ----------
1678
+ mask : np.ndarray
1679
+ Boolean mask selecting subset of data
1680
+ gross_thickness : float
1681
+ Total gross thickness for fraction calculation
1682
+ precision : int, default 6
1683
+ Number of decimal places for rounding
1684
+
1685
+ Returns
1686
+ -------
1687
+ dict
1688
+ Dictionary with stats for each discrete value:
1689
+ {value_label: {code, count, thickness, fraction, depth_range}}
1690
+ """
1691
+ values_array = self.values
1692
+ depth_array = self.depth
1693
+
1694
+ values = values_array[mask]
1695
+ depths = depth_array[mask]
1696
+
1697
+ # Compute intervals on full array then mask
1698
+ full_intervals = compute_intervals(depth_array)
1699
+ intervals = full_intervals[mask]
1700
+
1701
+ # Find unique discrete values
1702
+ valid_mask_local = ~np.isnan(values)
1703
+ valid_values = values[valid_mask_local]
1704
+ valid_depths = depths[valid_mask_local]
1705
+ valid_intervals = intervals[valid_mask_local]
1706
+
1707
+ if len(valid_values) == 0:
1708
+ return {}
1709
+
1710
+ unique_vals = np.unique(valid_values)
1711
+
1712
+ result = {}
1713
+ for val in unique_vals:
1714
+ val_mask = valid_values == val
1715
+ val_intervals = valid_intervals[val_mask]
1716
+ val_depths = valid_depths[val_mask]
1717
+
1718
+ thickness = float(np.sum(val_intervals))
1719
+ count = int(np.sum(val_mask))
1720
+ fraction = thickness / gross_thickness if gross_thickness > 0 else 0.0
1721
+
1722
+ # Determine the key and label
1723
+ int_val = int(val)
1724
+ if self.labels is not None and int_val in self.labels:
1725
+ key = self.labels[int_val]
1726
+ label = self.labels[int_val]
1727
+ else:
1728
+ key = f"{self.name}_{int_val}"
1729
+ label = None
1730
+
1731
+ stats = {
1732
+ 'code': int_val,
1733
+ 'count': count,
1734
+ 'thickness': round(thickness, precision),
1735
+ 'fraction': round(fraction, precision),
1736
+ 'depth_range': {
1737
+ 'min': round(float(np.min(val_depths)), precision),
1738
+ 'max': round(float(np.max(val_depths)), precision)
1739
+ }
1740
+ }
1741
+ if label is not None:
1742
+ stats['label'] = label
1743
+
1744
+ result[key] = stats
1745
+
1746
+ return result
1747
+
1523
1748
  def _recursive_group(
1524
1749
  self,
1525
1750
  filter_idx: int,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.140
3
+ Version: 0.1.141
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