well-log-toolkit 0.1.135__py3-none-any.whl → 0.1.136__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.
@@ -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 (linear, polynomial, etc.)
42
- **kwargs: Additional parameters (e.g., degree for polynomial)
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
- raise ValueError(
61
- f"Unknown regression type: {regression_type}. "
62
- f"Choose from: linear, logarithmic, exponential, polynomial, power"
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
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.135
3
+ Version: 0.1.136
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,12 +4,12 @@ well_log_toolkit/las_file.py,sha256=Tj0mRfX1aX2s6uug7BBlY1m_mu3G50EGxHGzD0eEedE,
4
4
  well_log_toolkit/manager.py,sha256=PuHF8rqypirNIN77STHcvg8WneExikpq6ZvkcRbcQpg,109776
5
5
  well_log_toolkit/operations.py,sha256=z8j8fGBOwoJGUQFy-Vawjq9nm3OD_dUt0oaNh8yuG7o,18515
6
6
  well_log_toolkit/property.py,sha256=B-3mXNJmvIqjjMdsu1kgVSwMgEwbJ36wn_n_oppdJFw,76769
7
- well_log_toolkit/regression.py,sha256=7D3oI-1XVlFb-mOoHTxTTtUHERFyvQSBAzJzAGVoZnk,25192
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
- well_log_toolkit/visualization.py,sha256=vnO8QjSNvvnQHGKXpe7BsLaQ0CMdLr0ruBt7poD-8Mc,199727
10
+ well_log_toolkit/visualization.py,sha256=HsTpd4UQCbu6pluVJ3AD9WWRg5mue8XGnlNhJUVIhF8,202585
11
11
  well_log_toolkit/well.py,sha256=Aav5Y-rui8YsJdvk7BFndNPUu1O9mcjwDApAGyqV9kw,104535
12
- well_log_toolkit-0.1.135.dist-info/METADATA,sha256=q39tCQUCWVP2XaMzNgtega97YSMCukEnIgSnodrCxfI,59810
13
- well_log_toolkit-0.1.135.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- well_log_toolkit-0.1.135.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.135.dist-info/RECORD,,
12
+ well_log_toolkit-0.1.136.dist-info/METADATA,sha256=AzJXzMRdRNY-AF61SUz-BBdxWrXNGaPd_8eyzozMX6w,59810
13
+ well_log_toolkit-0.1.136.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ well_log_toolkit-0.1.136.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.136.dist-info/RECORD,,