well-log-toolkit 0.1.116__tar.gz → 0.1.118__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.116 → well_log_toolkit-0.1.118}/PKG-INFO +1 -1
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/pyproject.toml +1 -1
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/manager.py +12 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/visualization.py +199 -27
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/PKG-INFO +1 -1
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/README.md +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/setup.cfg +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/__init__.py +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/exceptions.py +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/las_file.py +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/operations.py +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/property.py +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/regression.py +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/statistics.py +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/utils.py +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/well.py +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/requires.txt +0 -0
- {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/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.118"
|
|
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"
|
|
@@ -2466,6 +2466,9 @@ class WellDataManager:
|
|
|
2466
2466
|
depth_range: Optional[tuple[float, float]] = None,
|
|
2467
2467
|
show_colorbar: bool = True,
|
|
2468
2468
|
show_legend: bool = True,
|
|
2469
|
+
show_regression_legend: bool = True,
|
|
2470
|
+
show_regression_equation: bool = True,
|
|
2471
|
+
show_regression_r2: bool = True,
|
|
2469
2472
|
regression: Optional[Union[str, dict]] = None,
|
|
2470
2473
|
regression_by_color: Optional[Union[str, dict]] = None,
|
|
2471
2474
|
regression_by_group: Optional[Union[str, dict]] = None,
|
|
@@ -2534,6 +2537,12 @@ class WellDataManager:
|
|
|
2534
2537
|
Show colorbar when using color mapping. Default: True
|
|
2535
2538
|
show_legend : bool, optional
|
|
2536
2539
|
Show legend. Default: True
|
|
2540
|
+
show_regression_legend : bool, optional
|
|
2541
|
+
Show separate legend for regression lines in lower right corner. Default: True
|
|
2542
|
+
show_regression_equation : bool, optional
|
|
2543
|
+
Include regression equation in regression legend labels. Default: True
|
|
2544
|
+
show_regression_r2 : bool, optional
|
|
2545
|
+
Include R² value in regression legend labels. Default: True
|
|
2537
2546
|
regression : str or dict, optional
|
|
2538
2547
|
Regression type to apply to all data points. Can be a string (e.g., "linear") or
|
|
2539
2548
|
dict with keys: type, line_color, line_width, line_style, line_alpha, x_range.
|
|
@@ -2624,6 +2633,9 @@ class WellDataManager:
|
|
|
2624
2633
|
depth_range=depth_range,
|
|
2625
2634
|
show_colorbar=show_colorbar,
|
|
2626
2635
|
show_legend=show_legend,
|
|
2636
|
+
show_regression_legend=show_regression_legend,
|
|
2637
|
+
show_regression_equation=show_regression_equation,
|
|
2638
|
+
show_regression_r2=show_regression_r2,
|
|
2627
2639
|
regression=regression,
|
|
2628
2640
|
regression_by_color=regression_by_color,
|
|
2629
2641
|
regression_by_group=regression_by_group,
|
|
@@ -2822,6 +2822,12 @@ class Crossplot:
|
|
|
2822
2822
|
Show colorbar when using color mapping. Default: True
|
|
2823
2823
|
show_legend : bool, optional
|
|
2824
2824
|
Show legend when using shape/well mapping. Default: True
|
|
2825
|
+
show_regression_legend : bool, optional
|
|
2826
|
+
Show separate legend for regression lines in lower right. Default: True
|
|
2827
|
+
show_regression_equation : bool, optional
|
|
2828
|
+
Show equations in regression legend. Default: True
|
|
2829
|
+
show_regression_r2 : bool, optional
|
|
2830
|
+
Show R² values in regression legend. Default: True
|
|
2825
2831
|
regression : str or dict, optional
|
|
2826
2832
|
Regression type to apply to all data points. Can be a string (e.g., "linear") or
|
|
2827
2833
|
dict with keys: type, line_color, line_width, line_style, line_alpha, x_range.
|
|
@@ -2912,9 +2918,12 @@ class Crossplot:
|
|
|
2912
2918
|
depth_range: Optional[tuple[float, float]] = None,
|
|
2913
2919
|
show_colorbar: bool = True,
|
|
2914
2920
|
show_legend: bool = True,
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2921
|
+
show_regression_legend: bool = True,
|
|
2922
|
+
show_regression_equation: bool = True,
|
|
2923
|
+
show_regression_r2: bool = True,
|
|
2924
|
+
regression: Optional[Union[str, dict]] = None,
|
|
2925
|
+
regression_by_color: Optional[Union[str, dict]] = None,
|
|
2926
|
+
regression_by_group: Optional[Union[str, dict]] = None,
|
|
2918
2927
|
):
|
|
2919
2928
|
# Store wells as list
|
|
2920
2929
|
if not isinstance(wells, list):
|
|
@@ -2948,6 +2957,9 @@ class Crossplot:
|
|
|
2948
2957
|
self.depth_range = depth_range
|
|
2949
2958
|
self.show_colorbar = show_colorbar
|
|
2950
2959
|
self.show_legend = show_legend
|
|
2960
|
+
self.show_regression_legend = show_regression_legend
|
|
2961
|
+
self.show_regression_equation = show_regression_equation
|
|
2962
|
+
self.show_regression_r2 = show_regression_r2
|
|
2951
2963
|
self.regression = regression
|
|
2952
2964
|
self.regression_by_color = regression_by_color
|
|
2953
2965
|
self.regression_by_group = regression_by_group
|
|
@@ -2961,6 +2973,7 @@ class Crossplot:
|
|
|
2961
2973
|
# Regression storage - nested structure: {type: {identifier: regression_obj}}
|
|
2962
2974
|
self._regressions = {}
|
|
2963
2975
|
self.regression_lines = {}
|
|
2976
|
+
self.regression_legend = None # Separate legend for regressions
|
|
2964
2977
|
|
|
2965
2978
|
# Pending regressions (added before plot() is called)
|
|
2966
2979
|
self._pending_regressions = []
|
|
@@ -3126,6 +3139,158 @@ class Crossplot:
|
|
|
3126
3139
|
self._regressions[reg_type] = {}
|
|
3127
3140
|
self._regressions[reg_type][identifier] = regression_obj
|
|
3128
3141
|
|
|
3142
|
+
def _find_best_legend_locations(self, data: pd.DataFrame) -> tuple[str, str]:
|
|
3143
|
+
"""Find the two best locations for legends based on data density.
|
|
3144
|
+
|
|
3145
|
+
Divides the plot into a 3x3 grid and finds the two squares with the least data points.
|
|
3146
|
+
|
|
3147
|
+
Args:
|
|
3148
|
+
data: DataFrame with 'x' and 'y' columns
|
|
3149
|
+
|
|
3150
|
+
Returns:
|
|
3151
|
+
Tuple of (primary_location, secondary_location) as matplotlib location strings
|
|
3152
|
+
"""
|
|
3153
|
+
# Get x and y bounds
|
|
3154
|
+
x_vals = data['x'].values
|
|
3155
|
+
y_vals = data['y'].values
|
|
3156
|
+
|
|
3157
|
+
# Handle log scales for binning
|
|
3158
|
+
if self.x_log:
|
|
3159
|
+
x_vals = np.log10(x_vals[x_vals > 0])
|
|
3160
|
+
if self.y_log:
|
|
3161
|
+
y_vals = np.log10(y_vals[y_vals > 0])
|
|
3162
|
+
|
|
3163
|
+
x_min, x_max = np.nanmin(x_vals), np.nanmax(x_vals)
|
|
3164
|
+
y_min, y_max = np.nanmin(y_vals), np.nanmax(y_vals)
|
|
3165
|
+
|
|
3166
|
+
# Create 3x3 grid and count points in each square
|
|
3167
|
+
x_bins = np.linspace(x_min, x_max, 4)
|
|
3168
|
+
y_bins = np.linspace(y_min, y_max, 4)
|
|
3169
|
+
|
|
3170
|
+
# Count points in each of 9 squares
|
|
3171
|
+
counts = {}
|
|
3172
|
+
for i in range(3):
|
|
3173
|
+
for j in range(3):
|
|
3174
|
+
x_mask = (x_vals >= x_bins[i]) & (x_vals < x_bins[i+1])
|
|
3175
|
+
y_mask = (y_vals >= y_bins[j]) & (y_vals < y_bins[j+1])
|
|
3176
|
+
counts[(i, j)] = np.sum(x_mask & y_mask)
|
|
3177
|
+
|
|
3178
|
+
# Map grid positions to matplotlib location strings
|
|
3179
|
+
# Grid: (0,2) (1,2) (2,2) -> upper left, upper center, upper right
|
|
3180
|
+
# (0,1) (1,1) (2,1) -> center left, center, center right
|
|
3181
|
+
# (0,0) (1,0) (2,0) -> lower left, lower center, lower right
|
|
3182
|
+
position_map = {
|
|
3183
|
+
(0, 2): 'upper left',
|
|
3184
|
+
(1, 2): 'upper center',
|
|
3185
|
+
(2, 2): 'upper right',
|
|
3186
|
+
(0, 1): 'center left',
|
|
3187
|
+
(1, 1): 'center',
|
|
3188
|
+
(2, 1): 'center right',
|
|
3189
|
+
(0, 0): 'lower left',
|
|
3190
|
+
(1, 0): 'lower center',
|
|
3191
|
+
(2, 0): 'lower right',
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
# Sort squares by count (ascending)
|
|
3195
|
+
sorted_squares = sorted(counts.items(), key=lambda x: x[1])
|
|
3196
|
+
|
|
3197
|
+
# Get two best locations
|
|
3198
|
+
best_pos = position_map[sorted_squares[0][0]]
|
|
3199
|
+
second_best_pos = position_map[sorted_squares[1][0]]
|
|
3200
|
+
|
|
3201
|
+
return best_pos, second_best_pos
|
|
3202
|
+
|
|
3203
|
+
def _format_regression_label(self, name: str, reg, include_equation: bool = None, include_r2: bool = None) -> str:
|
|
3204
|
+
"""Format a modern, compact regression label.
|
|
3205
|
+
|
|
3206
|
+
Args:
|
|
3207
|
+
name: Name of the regression
|
|
3208
|
+
reg: Regression object
|
|
3209
|
+
include_equation: Whether to include equation (uses self.show_regression_equation if None)
|
|
3210
|
+
include_r2: Whether to include R² (uses self.show_regression_r2 if None)
|
|
3211
|
+
|
|
3212
|
+
Returns:
|
|
3213
|
+
Formatted label string
|
|
3214
|
+
"""
|
|
3215
|
+
if include_equation is None:
|
|
3216
|
+
include_equation = self.show_regression_equation
|
|
3217
|
+
if include_r2 is None:
|
|
3218
|
+
include_r2 = self.show_regression_r2
|
|
3219
|
+
|
|
3220
|
+
# Format: "Name (equation)" with R² on second line
|
|
3221
|
+
# Equation and R² will be colored grey in the legend update method
|
|
3222
|
+
first_line = name
|
|
3223
|
+
if include_equation:
|
|
3224
|
+
eq = reg.equation()
|
|
3225
|
+
eq = eq.replace(' ', '') # Remove spaces for compactness
|
|
3226
|
+
# Add equation in parentheses (will be styled grey later)
|
|
3227
|
+
first_line = f"{name} ({eq})"
|
|
3228
|
+
|
|
3229
|
+
# Add R² on second line if requested (will be styled grey later)
|
|
3230
|
+
if include_r2:
|
|
3231
|
+
return f"{first_line}\nR² = {reg.r_squared:.3f}"
|
|
3232
|
+
else:
|
|
3233
|
+
return first_line
|
|
3234
|
+
|
|
3235
|
+
def _update_regression_legend(self) -> None:
|
|
3236
|
+
"""Create or update the separate regression legend with smart placement."""
|
|
3237
|
+
if not self.show_regression_legend or not self.regression_lines:
|
|
3238
|
+
return
|
|
3239
|
+
|
|
3240
|
+
if self.ax is None:
|
|
3241
|
+
return
|
|
3242
|
+
|
|
3243
|
+
# Remove old regression legend if it exists
|
|
3244
|
+
if self.regression_legend is not None:
|
|
3245
|
+
self.regression_legend.remove()
|
|
3246
|
+
self.regression_legend = None
|
|
3247
|
+
|
|
3248
|
+
# Create new regression legend with only regression lines
|
|
3249
|
+
regression_handles = []
|
|
3250
|
+
regression_labels = []
|
|
3251
|
+
|
|
3252
|
+
for line in self.regression_lines.values():
|
|
3253
|
+
regression_handles.append(line)
|
|
3254
|
+
regression_labels.append(line.get_label())
|
|
3255
|
+
|
|
3256
|
+
if regression_handles:
|
|
3257
|
+
# Get smart placement based on data density
|
|
3258
|
+
if self._data is not None:
|
|
3259
|
+
_, secondary_loc = self._find_best_legend_locations(self._data)
|
|
3260
|
+
else:
|
|
3261
|
+
# Fallback if data not available
|
|
3262
|
+
secondary_loc = 'lower right'
|
|
3263
|
+
|
|
3264
|
+
# Import legend from matplotlib
|
|
3265
|
+
from matplotlib.legend import Legend
|
|
3266
|
+
|
|
3267
|
+
# Create regression legend at secondary location
|
|
3268
|
+
self.regression_legend = Legend(
|
|
3269
|
+
self.ax,
|
|
3270
|
+
regression_handles,
|
|
3271
|
+
regression_labels,
|
|
3272
|
+
loc=secondary_loc,
|
|
3273
|
+
frameon=True,
|
|
3274
|
+
framealpha=0.95,
|
|
3275
|
+
edgecolor='#cccccc',
|
|
3276
|
+
fancybox=False,
|
|
3277
|
+
shadow=False,
|
|
3278
|
+
fontsize=9,
|
|
3279
|
+
title='Regressions',
|
|
3280
|
+
title_fontsize=10
|
|
3281
|
+
)
|
|
3282
|
+
|
|
3283
|
+
# Modern styling with grey text for equation and R²
|
|
3284
|
+
self.regression_legend.get_frame().set_linewidth(0.8)
|
|
3285
|
+
self.regression_legend.get_title().set_fontweight('600')
|
|
3286
|
+
|
|
3287
|
+
# Set text color to grey for all labels
|
|
3288
|
+
for text in self.regression_legend.get_texts():
|
|
3289
|
+
text.set_color('#555555')
|
|
3290
|
+
|
|
3291
|
+
# Add as artist to avoid replacing the primary legend
|
|
3292
|
+
self.ax.add_artist(self.regression_legend)
|
|
3293
|
+
|
|
3129
3294
|
def _add_automatic_regressions(self, data: pd.DataFrame) -> None:
|
|
3130
3295
|
"""Add automatic regressions based on initialization parameters."""
|
|
3131
3296
|
if not any([self.regression, self.regression_by_color, self.regression_by_group]):
|
|
@@ -3313,8 +3478,8 @@ class Crossplot:
|
|
|
3313
3478
|
warnings.warn(f"Could not generate plot data for {name} regression: {e}")
|
|
3314
3479
|
return
|
|
3315
3480
|
|
|
3316
|
-
# Create label
|
|
3317
|
-
label =
|
|
3481
|
+
# Create label using formatter
|
|
3482
|
+
label = self._format_regression_label(name, reg)
|
|
3318
3483
|
|
|
3319
3484
|
# Plot line with config parameters
|
|
3320
3485
|
line = self.ax.plot(
|
|
@@ -3328,9 +3493,9 @@ class Crossplot:
|
|
|
3328
3493
|
|
|
3329
3494
|
self.regression_lines[name] = line
|
|
3330
3495
|
|
|
3331
|
-
# Update legend if requested (skipped during batch operations for performance)
|
|
3496
|
+
# Update regression legend if requested (skipped during batch operations for performance)
|
|
3332
3497
|
if update_legend and self.ax is not None:
|
|
3333
|
-
self.
|
|
3498
|
+
self._update_regression_legend()
|
|
3334
3499
|
|
|
3335
3500
|
def plot(self) -> 'Crossplot':
|
|
3336
3501
|
"""Generate the crossplot figure."""
|
|
@@ -3394,13 +3559,12 @@ class Crossplot:
|
|
|
3394
3559
|
warnings.warn(f"Could not generate plot data for {reg_type} regression: {e}")
|
|
3395
3560
|
continue
|
|
3396
3561
|
|
|
3397
|
-
# Create label
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
label = "\n".join(label_parts)
|
|
3562
|
+
# Create label using formatter
|
|
3563
|
+
label = self._format_regression_label(
|
|
3564
|
+
reg_name, reg,
|
|
3565
|
+
include_equation=pending['show_equation'],
|
|
3566
|
+
include_r2=pending['show_r2']
|
|
3567
|
+
)
|
|
3404
3568
|
|
|
3405
3569
|
# Plot line
|
|
3406
3570
|
line = self.ax.plot(
|
|
@@ -3414,9 +3578,9 @@ class Crossplot:
|
|
|
3414
3578
|
|
|
3415
3579
|
self.regression_lines[reg_name] = line
|
|
3416
3580
|
|
|
3417
|
-
# Update legend once after all pending regressions
|
|
3581
|
+
# Update regression legend once after all pending regressions
|
|
3418
3582
|
if self.ax is not None:
|
|
3419
|
-
self.
|
|
3583
|
+
self._update_regression_legend()
|
|
3420
3584
|
|
|
3421
3585
|
# Clear pending list
|
|
3422
3586
|
self._pending_regressions = []
|
|
@@ -3545,17 +3709,26 @@ class Crossplot:
|
|
|
3545
3709
|
if first_scatter is None and self.color:
|
|
3546
3710
|
first_scatter = scatter
|
|
3547
3711
|
|
|
3548
|
-
# Add legend
|
|
3712
|
+
# Add legend with smart placement
|
|
3549
3713
|
if self.show_legend:
|
|
3714
|
+
# Get best location based on data density
|
|
3715
|
+
if self._data is not None:
|
|
3716
|
+
primary_loc, _ = self._find_best_legend_locations(self._data)
|
|
3717
|
+
else:
|
|
3718
|
+
primary_loc = 'best'
|
|
3719
|
+
|
|
3550
3720
|
legend = self.ax.legend(
|
|
3551
3721
|
title=group_label,
|
|
3552
|
-
loc=
|
|
3722
|
+
loc=primary_loc,
|
|
3553
3723
|
frameon=True,
|
|
3554
3724
|
framealpha=0.9,
|
|
3555
3725
|
edgecolor='black'
|
|
3556
3726
|
)
|
|
3557
3727
|
legend.get_title().set_fontweight('bold')
|
|
3558
3728
|
|
|
3729
|
+
# Store the primary legend so it persists when regression legend is added
|
|
3730
|
+
self.ax.add_artist(legend)
|
|
3731
|
+
|
|
3559
3732
|
# Add colorbar if using color mapping
|
|
3560
3733
|
if self.color and self.show_colorbar and first_scatter:
|
|
3561
3734
|
self.colorbar = self.fig.colorbar(first_scatter, ax=self.ax)
|
|
@@ -3650,13 +3823,12 @@ class Crossplot:
|
|
|
3650
3823
|
warnings.warn(f"Could not generate plot data for {regression_type} regression: {e}")
|
|
3651
3824
|
return self
|
|
3652
3825
|
|
|
3653
|
-
# Create label
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
label = "\n".join(label_parts)
|
|
3826
|
+
# Create label using formatter
|
|
3827
|
+
label = self._format_regression_label(
|
|
3828
|
+
reg_name, reg,
|
|
3829
|
+
include_equation=show_equation,
|
|
3830
|
+
include_r2=show_r2
|
|
3831
|
+
)
|
|
3660
3832
|
|
|
3661
3833
|
# Plot line
|
|
3662
3834
|
line = self.ax.plot(
|
|
@@ -3670,8 +3842,8 @@ class Crossplot:
|
|
|
3670
3842
|
|
|
3671
3843
|
self.regression_lines[reg_name] = line
|
|
3672
3844
|
|
|
3673
|
-
# Update legend
|
|
3674
|
-
self.
|
|
3845
|
+
# Update regression legend
|
|
3846
|
+
self._update_regression_legend()
|
|
3675
3847
|
else:
|
|
3676
3848
|
# Store for later when plot() is called
|
|
3677
3849
|
self._pending_regressions.append({
|
|
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.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|