well-log-toolkit 0.1.135__tar.gz → 0.1.136__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.
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/PKG-INFO +1 -1
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/pyproject.toml +1 -1
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit/regression.py +172 -1
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit/visualization.py +75 -9
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/PKG-INFO +1 -1
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/README.md +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/setup.cfg +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit/__init__.py +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit/exceptions.py +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit/las_file.py +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit/manager.py +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit/operations.py +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit/property.py +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit/statistics.py +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit/utils.py +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit/well.py +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/requires.txt +0 -0
- {well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "well-log-toolkit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.136"
|
|
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"
|
|
@@ -757,11 +757,182 @@ class PowerRegression(RegressionBase):
|
|
|
757
757
|
return f"y = {self.a:.4f}*x^{self.b:.4f}"
|
|
758
758
|
|
|
759
759
|
|
|
760
|
+
class PolynomialExponentialRegression(RegressionBase):
|
|
761
|
+
"""Polynomial-Exponential regression: y = 10^(a + b*x + c*x² + ... + n*x^degree)
|
|
762
|
+
|
|
763
|
+
This is an exponential function with a polynomial in the exponent.
|
|
764
|
+
Equivalent to: log₁₀(y) = a + b*x + c*x² + ... + n*x^degree
|
|
765
|
+
|
|
766
|
+
This form is particularly useful for petrophysical relationships like
|
|
767
|
+
porosity-permeability where data spans orders of magnitude and the
|
|
768
|
+
relationship has curvature in log-space.
|
|
769
|
+
|
|
770
|
+
Note: Only valid for positive y values.
|
|
771
|
+
|
|
772
|
+
Example:
|
|
773
|
+
>>> # Quadratic exponential (default degree=2)
|
|
774
|
+
>>> reg = PolynomialExponentialRegression(degree=2)
|
|
775
|
+
>>> reg.fit([0.1, 0.15, 0.2, 0.25], [0.1, 1.0, 10.0, 50.0])
|
|
776
|
+
>>> reg.predict([0.3])
|
|
777
|
+
array([150.])
|
|
778
|
+
>>> print(reg.equation())
|
|
779
|
+
y = 10^(-2.5694 + 25.2696*x - 21.0434*x²)
|
|
780
|
+
|
|
781
|
+
# Linear exponential (degree=1, same as exponential but base 10)
|
|
782
|
+
>>> reg = PolynomialExponentialRegression(degree=1)
|
|
783
|
+
>>> reg.fit(x, y)
|
|
784
|
+
|
|
785
|
+
# Lock specific coefficients
|
|
786
|
+
>>> reg = PolynomialExponentialRegression(degree=2, locked_params={'c0': 0.0})
|
|
787
|
+
>>> reg.fit(x, y) # Forces constant term to 0
|
|
788
|
+
"""
|
|
789
|
+
|
|
790
|
+
def __init__(self, degree: int = 2, locked_params: Optional[Dict[str, float]] = None):
|
|
791
|
+
"""Initialize polynomial-exponential regression.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
degree: Polynomial degree in the exponent (default: 2 for quadratic)
|
|
795
|
+
locked_params: Dictionary to lock coefficients. Keys: 'c0', 'c1', ..., 'c{degree}'
|
|
796
|
+
where c0 is the constant term, c1 is the linear coefficient, etc.
|
|
797
|
+
"""
|
|
798
|
+
super().__init__(locked_params)
|
|
799
|
+
if degree < 1:
|
|
800
|
+
raise ValueError("Polynomial degree must be at least 1")
|
|
801
|
+
self.degree = degree
|
|
802
|
+
self.coefficients: Optional[np.ndarray] = None
|
|
803
|
+
|
|
804
|
+
def fit(self, x: ArrayLike, y: ArrayLike) -> 'PolynomialExponentialRegression':
|
|
805
|
+
"""Fit polynomial-exponential regression model.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
x: Independent variable values
|
|
809
|
+
y: Dependent variable values (must be positive)
|
|
810
|
+
|
|
811
|
+
Returns:
|
|
812
|
+
Self for method chaining
|
|
813
|
+
"""
|
|
814
|
+
x_clean, y_clean = self._prepare_data(x, y)
|
|
815
|
+
|
|
816
|
+
# Check for positive y values
|
|
817
|
+
if np.any(y_clean <= 0):
|
|
818
|
+
raise ValueError("Polynomial-Exponential regression requires all y values to be positive")
|
|
819
|
+
|
|
820
|
+
# Transform to polynomial: log₁₀(y) = a + b*x + c*x² + ...
|
|
821
|
+
log_y = np.log10(y_clean)
|
|
822
|
+
|
|
823
|
+
# Check for locked coefficients
|
|
824
|
+
locked_indices = {int(k[1:]): v for k, v in self._locked_params.items() if k.startswith('c')}
|
|
825
|
+
|
|
826
|
+
if not locked_indices:
|
|
827
|
+
# No locked coefficients - use standard polyfit
|
|
828
|
+
# Note: polyfit returns coefficients from highest to lowest degree
|
|
829
|
+
# We need to reverse to get [constant, linear, quadratic, ...]
|
|
830
|
+
self.coefficients = np.polyfit(x_clean, log_y, self.degree)[::-1]
|
|
831
|
+
else:
|
|
832
|
+
# Initialize coefficients array [c0, c1, c2, ...]
|
|
833
|
+
self.coefficients = np.zeros(self.degree + 1)
|
|
834
|
+
|
|
835
|
+
# Set locked coefficients
|
|
836
|
+
for idx, val in locked_indices.items():
|
|
837
|
+
if idx < 0 or idx > self.degree:
|
|
838
|
+
raise ValueError(f"Coefficient index c{idx} out of range for degree {self.degree}")
|
|
839
|
+
self.coefficients[idx] = val
|
|
840
|
+
|
|
841
|
+
# Subtract contribution of locked coefficients from log(y)
|
|
842
|
+
log_y_adjusted = log_y.copy()
|
|
843
|
+
for idx, val in locked_indices.items():
|
|
844
|
+
log_y_adjusted -= val * (x_clean ** idx)
|
|
845
|
+
|
|
846
|
+
# Fit only unlocked coefficients
|
|
847
|
+
unlocked_indices = [i for i in range(self.degree + 1) if i not in locked_indices]
|
|
848
|
+
|
|
849
|
+
if unlocked_indices:
|
|
850
|
+
# Build design matrix for unlocked terms
|
|
851
|
+
X = np.column_stack([x_clean ** i for i in unlocked_indices])
|
|
852
|
+
|
|
853
|
+
# Solve least squares
|
|
854
|
+
coefs_unlocked = np.linalg.lstsq(X, log_y_adjusted, rcond=None)[0]
|
|
855
|
+
|
|
856
|
+
# Assign unlocked coefficients
|
|
857
|
+
for i, idx in enumerate(unlocked_indices):
|
|
858
|
+
self.coefficients[idx] = coefs_unlocked[i]
|
|
859
|
+
|
|
860
|
+
# Calculate metrics (in log space for better R² with data spanning orders of magnitude)
|
|
861
|
+
log_y_pred = np.sum([self.coefficients[i] * (x_clean ** i) for i in range(self.degree + 1)], axis=0)
|
|
862
|
+
y_pred = 10 ** log_y_pred
|
|
863
|
+
self._calculate_metrics(x_clean, y_clean, y_pred, use_log_space=True)
|
|
864
|
+
|
|
865
|
+
self.fitted = True
|
|
866
|
+
return self
|
|
867
|
+
|
|
868
|
+
def predict(self, x: ArrayLike) -> np.ndarray:
|
|
869
|
+
"""Predict y values using polynomial-exponential model.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
x: Independent variable values
|
|
873
|
+
|
|
874
|
+
Returns:
|
|
875
|
+
Predicted y values
|
|
876
|
+
"""
|
|
877
|
+
if not self.fitted:
|
|
878
|
+
raise ValueError("Model must be fitted before prediction. Call fit() first.")
|
|
879
|
+
|
|
880
|
+
x = np.asarray(x, dtype=float)
|
|
881
|
+
|
|
882
|
+
# Calculate polynomial in exponent
|
|
883
|
+
log_y = np.sum([self.coefficients[i] * (x ** i) for i in range(self.degree + 1)], axis=0)
|
|
884
|
+
|
|
885
|
+
# Return 10^(polynomial)
|
|
886
|
+
return 10 ** log_y
|
|
887
|
+
|
|
888
|
+
def equation(self) -> str:
|
|
889
|
+
"""Return the polynomial-exponential equation as a string."""
|
|
890
|
+
if not self.fitted:
|
|
891
|
+
return "Model not fitted"
|
|
892
|
+
|
|
893
|
+
# Build polynomial terms
|
|
894
|
+
terms = []
|
|
895
|
+
for i, coef in enumerate(self.coefficients):
|
|
896
|
+
if abs(coef) < 1e-10: # Skip near-zero coefficients
|
|
897
|
+
continue
|
|
898
|
+
|
|
899
|
+
# Format coefficient
|
|
900
|
+
if not terms: # First term
|
|
901
|
+
coef_str = f"{coef:.4f}"
|
|
902
|
+
else:
|
|
903
|
+
sign = "+" if coef >= 0 else "-"
|
|
904
|
+
coef_str = f"{sign} {abs(coef):.4f}"
|
|
905
|
+
|
|
906
|
+
# Format power
|
|
907
|
+
if i == 0:
|
|
908
|
+
term = coef_str
|
|
909
|
+
elif i == 1:
|
|
910
|
+
term = f"{coef_str}*x"
|
|
911
|
+
elif i == 2:
|
|
912
|
+
term = f"{coef_str}*x²"
|
|
913
|
+
elif i == 3:
|
|
914
|
+
term = f"{coef_str}*x³"
|
|
915
|
+
else:
|
|
916
|
+
term = f"{coef_str}*x^{i}"
|
|
917
|
+
|
|
918
|
+
terms.append(term)
|
|
919
|
+
|
|
920
|
+
if not terms:
|
|
921
|
+
return "y = 10^(0)"
|
|
922
|
+
|
|
923
|
+
poly_str = "".join(terms).strip()
|
|
924
|
+
# Clean up leading plus sign
|
|
925
|
+
poly_str = poly_str.replace("+ -", "- ")
|
|
926
|
+
|
|
927
|
+
return f"y = 10^({poly_str})"
|
|
928
|
+
|
|
929
|
+
|
|
760
930
|
__all__ = [
|
|
761
931
|
'RegressionBase',
|
|
762
932
|
'LinearRegression',
|
|
763
933
|
'LogarithmicRegression',
|
|
764
934
|
'ExponentialRegression',
|
|
765
935
|
'PolynomialRegression',
|
|
766
|
-
'PowerRegression'
|
|
936
|
+
'PowerRegression',
|
|
937
|
+
'PolynomialExponentialRegression'
|
|
767
938
|
]
|
|
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
|
|
|
23
23
|
# Import regression classes at module level for performance
|
|
24
24
|
from .regression import (
|
|
25
25
|
LinearRegression, LogarithmicRegression, ExponentialRegression,
|
|
26
|
-
PolynomialRegression, PowerRegression
|
|
26
|
+
PolynomialRegression, PowerRegression, PolynomialExponentialRegression
|
|
27
27
|
)
|
|
28
28
|
from .exceptions import PropertyNotFoundError
|
|
29
29
|
|
|
@@ -38,30 +38,96 @@ def _create_regression(regression_type: str, **kwargs):
|
|
|
38
38
|
"""Factory function to create regression objects efficiently.
|
|
39
39
|
|
|
40
40
|
Args:
|
|
41
|
-
regression_type: Type of regression
|
|
42
|
-
|
|
41
|
+
regression_type: Type of regression with optional degree suffix
|
|
42
|
+
Examples: 'linear', 'polynomial', 'polynomial_3', 'exponential-polynomial_4'
|
|
43
|
+
**kwargs: Additional parameters (deprecated, use suffix notation instead)
|
|
43
44
|
|
|
44
45
|
Returns:
|
|
45
46
|
Regression object instance
|
|
46
47
|
"""
|
|
47
48
|
regression_type = regression_type.lower()
|
|
49
|
+
|
|
50
|
+
# Parse degree suffix (e.g., polynomial_3 → degree=3)
|
|
51
|
+
degree = None
|
|
52
|
+
if "_" in regression_type:
|
|
53
|
+
parts = regression_type.split("_")
|
|
54
|
+
try:
|
|
55
|
+
degree = int(parts[-1])
|
|
56
|
+
regression_type = "_".join(parts[:-1]) # Remove degree suffix
|
|
57
|
+
except ValueError:
|
|
58
|
+
pass # Not a degree suffix, keep original
|
|
59
|
+
|
|
60
|
+
# Simple regression types
|
|
48
61
|
if regression_type == "linear":
|
|
49
62
|
return LinearRegression()
|
|
50
63
|
elif regression_type == "logarithmic":
|
|
51
64
|
return LogarithmicRegression()
|
|
52
65
|
elif regression_type == "exponential":
|
|
53
66
|
return ExponentialRegression()
|
|
54
|
-
elif regression_type == "polynomial":
|
|
55
|
-
degree = kwargs.get('degree', 2)
|
|
56
|
-
return PolynomialRegression(degree=degree)
|
|
57
67
|
elif regression_type == "power":
|
|
58
68
|
return PowerRegression()
|
|
69
|
+
|
|
70
|
+
# Polynomial with degree
|
|
71
|
+
elif regression_type == "polynomial":
|
|
72
|
+
# Use suffix degree, then kwargs, then default
|
|
73
|
+
if degree is None:
|
|
74
|
+
degree = kwargs.get('degree', 2)
|
|
75
|
+
return PolynomialRegression(degree=degree)
|
|
76
|
+
|
|
77
|
+
# Exponential-polynomial (renamed from polynomial-exponential)
|
|
78
|
+
elif regression_type == "exponential-polynomial":
|
|
79
|
+
if degree is None:
|
|
80
|
+
degree = kwargs.get('degree', 2)
|
|
81
|
+
return PolynomialExponentialRegression(degree=degree)
|
|
82
|
+
|
|
83
|
+
# Backward compatibility: old name polynomial-exponential
|
|
84
|
+
elif regression_type == "polynomial-exponential":
|
|
85
|
+
import warnings
|
|
86
|
+
warnings.warn(
|
|
87
|
+
"'polynomial-exponential' is deprecated. Use 'exponential-polynomial' instead.",
|
|
88
|
+
DeprecationWarning,
|
|
89
|
+
stacklevel=3
|
|
90
|
+
)
|
|
91
|
+
if degree is None:
|
|
92
|
+
degree = kwargs.get('degree', 2)
|
|
93
|
+
return PolynomialExponentialRegression(degree=degree)
|
|
94
|
+
|
|
59
95
|
else:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
f"
|
|
96
|
+
# Create detailed error message with equation formats
|
|
97
|
+
error_msg = (
|
|
98
|
+
f"Unknown regression type: '{regression_type}'. "
|
|
99
|
+
f"Available types:\n\n"
|
|
100
|
+
f" • linear: y = a*x + b\n"
|
|
101
|
+
f" • logarithmic: y = a*ln(x) + b\n"
|
|
102
|
+
f" • exponential: y = a*e^(b*x)\n"
|
|
103
|
+
f" • polynomial: y = a + b*x + c*x² (default: degree=2)\n"
|
|
104
|
+
f" - polynomial_1: y = a + b*x (1st degree)\n"
|
|
105
|
+
f" - polynomial_3: y = a + b*x + c*x² + d*x³ (3rd degree)\n"
|
|
106
|
+
f" • exponential-polynomial: y = 10^(a + b*x + c*x²) (default: degree=2)\n"
|
|
107
|
+
f" - exponential-polynomial_1: y = 10^(a + b*x) (1st degree)\n"
|
|
108
|
+
f" - exponential-polynomial_3: y = 10^(a + b*x + c*x² + d*x³) (3rd degree)\n"
|
|
109
|
+
f" • power: y = a*x^b\n\n"
|
|
110
|
+
f"Did you mean one of these?"
|
|
63
111
|
)
|
|
64
112
|
|
|
113
|
+
# Check for common typos
|
|
114
|
+
suggestions = []
|
|
115
|
+
if "expo" in regression_type or "exp" in regression_type:
|
|
116
|
+
suggestions.append("exponential or exponential-polynomial")
|
|
117
|
+
if "poly" in regression_type:
|
|
118
|
+
suggestions.append("polynomial or exponential-polynomial")
|
|
119
|
+
if "log" in regression_type:
|
|
120
|
+
suggestions.append("logarithmic")
|
|
121
|
+
if "lin" in regression_type:
|
|
122
|
+
suggestions.append("linear")
|
|
123
|
+
if "pow" in regression_type:
|
|
124
|
+
suggestions.append("power")
|
|
125
|
+
|
|
126
|
+
if suggestions:
|
|
127
|
+
error_msg += f"\n Possible matches: {', '.join(suggestions)}"
|
|
128
|
+
|
|
129
|
+
raise ValueError(error_msg)
|
|
130
|
+
|
|
65
131
|
|
|
66
132
|
def _downsample_for_plotting(depth: np.ndarray, values: np.ndarray, max_points: int = 2000) -> tuple[np.ndarray, np.ndarray]:
|
|
67
133
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.135 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|