well-log-toolkit 0.1.134__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.134 → well_log_toolkit-0.1.136}/PKG-INFO +1 -1
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/pyproject.toml +1 -1
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit/regression.py +172 -1
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit/visualization.py +177 -57
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/PKG-INFO +1 -1
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/README.md +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/setup.cfg +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit/__init__.py +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit/exceptions.py +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit/las_file.py +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit/manager.py +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit/operations.py +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit/property.py +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit/statistics.py +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit/utils.py +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit/well.py +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
- {well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/requires.txt +0 -0
- {well_log_toolkit-0.1.134 → 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
|
"""
|
|
@@ -3475,22 +3541,48 @@ class Crossplot:
|
|
|
3475
3541
|
Returns:
|
|
3476
3542
|
Tuple of (segment_number, matplotlib_location_string)
|
|
3477
3543
|
"""
|
|
3478
|
-
# Get x and y
|
|
3544
|
+
# Get x and y data values
|
|
3479
3545
|
x_vals = data['x'].values
|
|
3480
3546
|
y_vals = data['y'].values
|
|
3481
3547
|
|
|
3482
|
-
#
|
|
3548
|
+
# Get axes limits to convert data coordinates to axes-normalized coordinates (0-1)
|
|
3549
|
+
# This ensures we're dividing the GRAPH AREA, not the data space
|
|
3550
|
+
if self.ax is not None:
|
|
3551
|
+
x_lim = self.ax.get_xlim()
|
|
3552
|
+
y_lim = self.ax.get_ylim()
|
|
3553
|
+
else:
|
|
3554
|
+
# Fallback if ax not available yet
|
|
3555
|
+
x_lim = (np.nanmin(x_vals), np.nanmax(x_vals))
|
|
3556
|
+
y_lim = (np.nanmin(y_vals), np.nanmax(y_vals))
|
|
3557
|
+
|
|
3558
|
+
# Handle logarithmic axes - transform to log space for proper visual segment calculation
|
|
3559
|
+
# On log axes, equal visual spacing corresponds to equal ratios, not equal differences
|
|
3483
3560
|
if self.x_log:
|
|
3484
|
-
|
|
3561
|
+
# Filter out non-positive values before log transform
|
|
3562
|
+
x_valid = x_vals > 0
|
|
3563
|
+
x_vals_transformed = np.where(x_valid, np.log10(x_vals), np.nan)
|
|
3564
|
+
x_lim_transformed = (np.log10(max(x_lim[0], 1e-10)), np.log10(max(x_lim[1], 1e-10)))
|
|
3565
|
+
else:
|
|
3566
|
+
x_vals_transformed = x_vals
|
|
3567
|
+
x_lim_transformed = x_lim
|
|
3568
|
+
|
|
3485
3569
|
if self.y_log:
|
|
3486
|
-
|
|
3570
|
+
# Filter out non-positive values before log transform
|
|
3571
|
+
y_valid = y_vals > 0
|
|
3572
|
+
y_vals_transformed = np.where(y_valid, np.log10(y_vals), np.nan)
|
|
3573
|
+
y_lim_transformed = (np.log10(max(y_lim[0], 1e-10)), np.log10(max(y_lim[1], 1e-10)))
|
|
3574
|
+
else:
|
|
3575
|
+
y_vals_transformed = y_vals
|
|
3576
|
+
y_lim_transformed = y_lim
|
|
3487
3577
|
|
|
3488
|
-
|
|
3489
|
-
|
|
3578
|
+
# Normalize transformed coordinates to axes coordinates (0-1)
|
|
3579
|
+
# This divides the visible graph area properly, accounting for log scales
|
|
3580
|
+
x_norm = (x_vals_transformed - x_lim_transformed[0]) / (x_lim_transformed[1] - x_lim_transformed[0])
|
|
3581
|
+
y_norm = (y_vals_transformed - y_lim_transformed[0]) / (y_lim_transformed[1] - y_lim_transformed[0])
|
|
3490
3582
|
|
|
3491
|
-
# Create 3x3 grid
|
|
3492
|
-
x_bins = np.linspace(
|
|
3493
|
-
y_bins = np.linspace(
|
|
3583
|
+
# Create 3x3 grid in axes-normalized space (0-1)
|
|
3584
|
+
x_bins = np.linspace(0, 1, 4)
|
|
3585
|
+
y_bins = np.linspace(0, 1, 4)
|
|
3494
3586
|
|
|
3495
3587
|
# Map segments 1-9 to grid positions (i, j)
|
|
3496
3588
|
# Segment numbering:
|
|
@@ -3522,12 +3614,12 @@ class Crossplot:
|
|
|
3522
3614
|
9: 'lower right',
|
|
3523
3615
|
}
|
|
3524
3616
|
|
|
3525
|
-
# Count points in each segment
|
|
3526
|
-
total_points = len(
|
|
3617
|
+
# Count points in each segment using normalized coordinates
|
|
3618
|
+
total_points = len(x_norm)
|
|
3527
3619
|
segment_counts = {}
|
|
3528
3620
|
for segment, (i, j) in segment_to_grid.items():
|
|
3529
|
-
x_mask = (
|
|
3530
|
-
y_mask = (
|
|
3621
|
+
x_mask = (x_norm >= x_bins[i]) & (x_norm < x_bins[i+1])
|
|
3622
|
+
y_mask = (y_norm >= y_bins[j]) & (y_norm < y_bins[j+1])
|
|
3531
3623
|
count = np.sum(x_mask & y_mask)
|
|
3532
3624
|
segment_counts[segment] = count
|
|
3533
3625
|
|
|
@@ -3638,20 +3730,24 @@ class Crossplot:
|
|
|
3638
3730
|
is_edge = self._is_edge_location(location)
|
|
3639
3731
|
|
|
3640
3732
|
# Determine base anchor point from location string
|
|
3641
|
-
# Map location to (x, y) coordinates in
|
|
3733
|
+
# Map location to (x, y) coordinates in AXES space (0-1 within the graph area)
|
|
3734
|
+
# These match the segment corners:
|
|
3735
|
+
# Segment 1=upper left (0,1), 2=upper center (0.5,1), 3=upper right (1,1)
|
|
3736
|
+
# Segment 4=center left (0,0.5), 5=center (0.5,0.5), 6=center right (1,0.5)
|
|
3737
|
+
# Segment 7=lower left (0,0), 8=lower center (0.5,0), 9=lower right (1,0)
|
|
3642
3738
|
anchor_map = {
|
|
3643
|
-
'upper left': (0
|
|
3644
|
-
'upper center': (0.5,
|
|
3645
|
-
'upper right': (
|
|
3646
|
-
'center left': (0
|
|
3739
|
+
'upper left': (0, 1),
|
|
3740
|
+
'upper center': (0.5, 1),
|
|
3741
|
+
'upper right': (1, 1),
|
|
3742
|
+
'center left': (0, 0.5),
|
|
3647
3743
|
'center': (0.5, 0.5),
|
|
3648
|
-
'center right': (
|
|
3649
|
-
'lower left': (0
|
|
3650
|
-
'lower center': (0.5, 0
|
|
3651
|
-
'lower right': (
|
|
3744
|
+
'center right': (1, 0.5),
|
|
3745
|
+
'lower left': (0, 0),
|
|
3746
|
+
'lower center': (0.5, 0),
|
|
3747
|
+
'lower right': (1, 0),
|
|
3652
3748
|
}
|
|
3653
3749
|
|
|
3654
|
-
base_x, base_y = anchor_map.get(location, (
|
|
3750
|
+
base_x, base_y = anchor_map.get(location, (1, 1))
|
|
3655
3751
|
|
|
3656
3752
|
if is_edge:
|
|
3657
3753
|
# Stack vertically on edges
|
|
@@ -3664,23 +3760,23 @@ class Crossplot:
|
|
|
3664
3760
|
framealpha=0.9,
|
|
3665
3761
|
edgecolor='black',
|
|
3666
3762
|
bbox_to_anchor=(base_x, base_y),
|
|
3667
|
-
bbox_transform=self.
|
|
3763
|
+
bbox_transform=self.ax.transAxes
|
|
3668
3764
|
)
|
|
3669
3765
|
shape_legend.get_title().set_fontweight('bold')
|
|
3670
3766
|
shape_legend.set_clip_on(False) # Prevent clipping outside axes
|
|
3671
3767
|
self.ax.add_artist(shape_legend)
|
|
3672
3768
|
|
|
3673
3769
|
# Calculate offset for color legend below shape legend
|
|
3674
|
-
# Estimate shape legend height
|
|
3675
|
-
shape_height = len(shape_handles) * 0.
|
|
3770
|
+
# Estimate shape legend height in axes coordinates
|
|
3771
|
+
shape_height = len(shape_handles) * 0.05 + 0.08 # Adjusted for axes space
|
|
3676
3772
|
|
|
3677
3773
|
# Adjust y position for color legend
|
|
3678
3774
|
if 'upper' in location:
|
|
3679
|
-
color_y = base_y - shape_height
|
|
3775
|
+
color_y = base_y - shape_height # Stack below
|
|
3680
3776
|
elif 'lower' in location:
|
|
3681
|
-
color_y = base_y + shape_height
|
|
3777
|
+
color_y = base_y + shape_height # Stack above
|
|
3682
3778
|
else: # center
|
|
3683
|
-
color_y = base_y - shape_height / 2
|
|
3779
|
+
color_y = base_y - shape_height / 2 # Stack below
|
|
3684
3780
|
|
|
3685
3781
|
color_legend = self.ax.legend(
|
|
3686
3782
|
handles=color_handles,
|
|
@@ -3690,19 +3786,19 @@ class Crossplot:
|
|
|
3690
3786
|
framealpha=0.9,
|
|
3691
3787
|
edgecolor='black',
|
|
3692
3788
|
bbox_to_anchor=(base_x, color_y),
|
|
3693
|
-
bbox_transform=self.
|
|
3789
|
+
bbox_transform=self.ax.transAxes
|
|
3694
3790
|
)
|
|
3695
3791
|
color_legend.get_title().set_fontweight('bold')
|
|
3696
3792
|
color_legend.set_clip_on(False) # Prevent clipping outside axes
|
|
3697
3793
|
else:
|
|
3698
3794
|
# Place side by side for non-edge locations (top, bottom, center)
|
|
3699
|
-
# Estimate width of each legend
|
|
3700
|
-
legend_width = 0.
|
|
3795
|
+
# Estimate width of each legend in axes coordinates
|
|
3796
|
+
legend_width = 0.20
|
|
3701
3797
|
|
|
3702
3798
|
if 'center' in location and location != 'center left' and location != 'center right':
|
|
3703
3799
|
# For center positions, place them side by side
|
|
3704
|
-
shape_x = base_x - legend_width / 2
|
|
3705
|
-
color_x = base_x + legend_width / 2
|
|
3800
|
+
shape_x = base_x - legend_width / 2
|
|
3801
|
+
color_x = base_x + legend_width / 2
|
|
3706
3802
|
|
|
3707
3803
|
shape_legend = self.ax.legend(
|
|
3708
3804
|
handles=shape_handles,
|
|
@@ -3712,7 +3808,7 @@ class Crossplot:
|
|
|
3712
3808
|
framealpha=0.9,
|
|
3713
3809
|
edgecolor='black',
|
|
3714
3810
|
bbox_to_anchor=(shape_x, base_y),
|
|
3715
|
-
bbox_transform=self.
|
|
3811
|
+
bbox_transform=self.ax.transAxes
|
|
3716
3812
|
)
|
|
3717
3813
|
shape_legend.get_title().set_fontweight('bold')
|
|
3718
3814
|
shape_legend.set_clip_on(False) # Prevent clipping outside axes
|
|
@@ -3726,7 +3822,7 @@ class Crossplot:
|
|
|
3726
3822
|
framealpha=0.9,
|
|
3727
3823
|
edgecolor='black',
|
|
3728
3824
|
bbox_to_anchor=(color_x, base_y),
|
|
3729
|
-
bbox_transform=self.
|
|
3825
|
+
bbox_transform=self.ax.transAxes
|
|
3730
3826
|
)
|
|
3731
3827
|
color_legend.get_title().set_fontweight('bold')
|
|
3732
3828
|
color_legend.set_clip_on(False) # Prevent clipping outside axes
|
|
@@ -3738,18 +3834,20 @@ class Crossplot:
|
|
|
3738
3834
|
loc=location,
|
|
3739
3835
|
frameon=True,
|
|
3740
3836
|
framealpha=0.9,
|
|
3741
|
-
edgecolor='black'
|
|
3837
|
+
edgecolor='black',
|
|
3838
|
+
bbox_to_anchor=(base_x, base_y),
|
|
3839
|
+
bbox_transform=self.ax.transAxes
|
|
3742
3840
|
)
|
|
3743
3841
|
shape_legend.get_title().set_fontweight('bold')
|
|
3744
3842
|
shape_legend.set_clip_on(False) # Prevent clipping outside axes
|
|
3745
3843
|
self.ax.add_artist(shape_legend)
|
|
3746
3844
|
|
|
3747
|
-
# Estimate offset
|
|
3748
|
-
shape_height = len(shape_handles) * 0.
|
|
3845
|
+
# Estimate offset in axes coordinates
|
|
3846
|
+
shape_height = len(shape_handles) * 0.05 + 0.08
|
|
3749
3847
|
if 'upper' in location:
|
|
3750
|
-
color_y = base_y - shape_height
|
|
3848
|
+
color_y = base_y - shape_height
|
|
3751
3849
|
else:
|
|
3752
|
-
color_y = base_y + shape_height
|
|
3850
|
+
color_y = base_y + shape_height
|
|
3753
3851
|
|
|
3754
3852
|
color_legend = self.ax.legend(
|
|
3755
3853
|
handles=color_handles,
|
|
@@ -3759,7 +3857,7 @@ class Crossplot:
|
|
|
3759
3857
|
framealpha=0.9,
|
|
3760
3858
|
edgecolor='black',
|
|
3761
3859
|
bbox_to_anchor=(base_x, color_y),
|
|
3762
|
-
bbox_transform=self.
|
|
3860
|
+
bbox_transform=self.ax.transAxes
|
|
3763
3861
|
)
|
|
3764
3862
|
color_legend.get_title().set_fontweight('bold')
|
|
3765
3863
|
color_legend.set_clip_on(False) # Prevent clipping outside axes
|
|
@@ -3837,14 +3935,32 @@ class Crossplot:
|
|
|
3837
3935
|
secondary_loc = 'lower right'
|
|
3838
3936
|
|
|
3839
3937
|
# Determine descriptive title based on regression type
|
|
3938
|
+
# Extract the regression type and add it to the title
|
|
3939
|
+
reg_type_str = None
|
|
3840
3940
|
if self.regression_by_color_and_shape:
|
|
3841
|
-
|
|
3941
|
+
base_title = 'Regressions by color and shape'
|
|
3942
|
+
config = self._parse_regression_config(self.regression_by_color_and_shape)
|
|
3943
|
+
reg_type_str = config.get('type', None)
|
|
3842
3944
|
elif self.regression_by_color:
|
|
3843
|
-
|
|
3945
|
+
base_title = 'Regressions by color'
|
|
3946
|
+
config = self._parse_regression_config(self.regression_by_color)
|
|
3947
|
+
reg_type_str = config.get('type', None)
|
|
3844
3948
|
elif self.regression_by_group:
|
|
3845
|
-
|
|
3949
|
+
base_title = 'Regressions by group'
|
|
3950
|
+
config = self._parse_regression_config(self.regression_by_group)
|
|
3951
|
+
reg_type_str = config.get('type', None)
|
|
3846
3952
|
else:
|
|
3847
|
-
|
|
3953
|
+
base_title = 'Regressions'
|
|
3954
|
+
if self.regression:
|
|
3955
|
+
config = self._parse_regression_config(self.regression)
|
|
3956
|
+
reg_type_str = config.get('type', None)
|
|
3957
|
+
|
|
3958
|
+
# Add regression type to title (e.g., "Regressions by color - Power")
|
|
3959
|
+
if reg_type_str:
|
|
3960
|
+
reg_type_display = reg_type_str.capitalize()
|
|
3961
|
+
regression_title = f"{base_title} - {reg_type_display}"
|
|
3962
|
+
else:
|
|
3963
|
+
regression_title = base_title
|
|
3848
3964
|
|
|
3849
3965
|
# Import legend from matplotlib
|
|
3850
3966
|
from matplotlib.legend import Legend
|
|
@@ -4284,11 +4400,15 @@ class Crossplot:
|
|
|
4284
4400
|
if self.y_log:
|
|
4285
4401
|
self.ax.set_yscale('log')
|
|
4286
4402
|
|
|
4287
|
-
# Disable scientific notation on axes
|
|
4403
|
+
# Disable scientific notation on linear axes only
|
|
4404
|
+
# (log axes use matplotlib's default log formatter for proper log scale labels)
|
|
4288
4405
|
from matplotlib.ticker import ScalarFormatter
|
|
4289
4406
|
formatter = ScalarFormatter(useOffset=False)
|
|
4290
4407
|
formatter.set_scientific(False)
|
|
4291
|
-
|
|
4408
|
+
|
|
4409
|
+
# Only apply to linear axes - log axes need their default formatter
|
|
4410
|
+
if not self.y_log:
|
|
4411
|
+
self.ax.yaxis.set_major_formatter(formatter)
|
|
4292
4412
|
if not self.x_log:
|
|
4293
4413
|
self.ax.xaxis.set_major_formatter(formatter)
|
|
4294
4414
|
|
|
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.134 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.134 → well_log_toolkit-0.1.136}/well_log_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|