logsuite 0.2.0__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 (69) hide show
  1. logsuite-0.2.0/PKG-INFO +2413 -0
  2. logsuite-0.2.0/README.md +2369 -0
  3. logsuite-0.2.0/logsuite/__init__.py +123 -0
  4. logsuite-0.2.0/logsuite/_version.py +23 -0
  5. logsuite-0.2.0/logsuite/analysis/__init__.py +54 -0
  6. logsuite-0.2.0/logsuite/analysis/regression.py +950 -0
  7. logsuite-0.2.0/logsuite/analysis/statistics.py +687 -0
  8. logsuite-0.2.0/logsuite/analysis/sums_avg.py +648 -0
  9. logsuite-0.2.0/logsuite/core/__init__.py +20 -0
  10. logsuite-0.2.0/logsuite/core/operations.py +492 -0
  11. logsuite-0.2.0/logsuite/core/property.py +2836 -0
  12. logsuite-0.2.0/logsuite/core/well.py +2718 -0
  13. logsuite-0.2.0/logsuite/exceptions.py +57 -0
  14. logsuite-0.2.0/logsuite/io/__init__.py +5 -0
  15. logsuite-0.2.0/logsuite/io/las_file.py +1378 -0
  16. logsuite-0.2.0/logsuite/manager/__init__.py +17 -0
  17. logsuite-0.2.0/logsuite/manager/data_manager.py +1648 -0
  18. logsuite-0.2.0/logsuite/manager/proxy.py +2048 -0
  19. logsuite-0.2.0/logsuite/utils.py +252 -0
  20. logsuite-0.2.0/logsuite/visualization/__init__.py +223 -0
  21. logsuite-0.2.0/logsuite/visualization/crossplot.py +2277 -0
  22. logsuite-0.2.0/logsuite/visualization/template.py +612 -0
  23. logsuite-0.2.0/logsuite/visualization/wellview.py +2180 -0
  24. logsuite-0.2.0/logsuite.egg-info/PKG-INFO +2413 -0
  25. logsuite-0.2.0/logsuite.egg-info/SOURCES.txt +67 -0
  26. logsuite-0.2.0/logsuite.egg-info/dependency_links.txt +1 -0
  27. logsuite-0.2.0/logsuite.egg-info/requires.txt +19 -0
  28. logsuite-0.2.0/logsuite.egg-info/top_level.txt +1 -0
  29. logsuite-0.2.0/pyproject.toml +153 -0
  30. logsuite-0.2.0/setup.cfg +4 -0
  31. logsuite-0.2.0/tests/test_crossplot.py +142 -0
  32. logsuite-0.2.0/tests/test_crossplot_extensive.py +762 -0
  33. logsuite-0.2.0/tests/test_crossplot_integration.py +603 -0
  34. logsuite-0.2.0/tests/test_crossplot_well_colors.py +285 -0
  35. logsuite-0.2.0/tests/test_discrete_labels_in_legend.py +213 -0
  36. logsuite-0.2.0/tests/test_discrete_regression_by_color.py +227 -0
  37. logsuite-0.2.0/tests/test_discrete_shape_color.py +331 -0
  38. logsuite-0.2.0/tests/test_error_handling.py +218 -0
  39. logsuite-0.2.0/tests/test_extended_statistics.py +176 -0
  40. logsuite-0.2.0/tests/test_filename_fix.py +224 -0
  41. logsuite-0.2.0/tests/test_filtered_comprehensive.py +226 -0
  42. logsuite-0.2.0/tests/test_flexible_loading.py +198 -0
  43. logsuite-0.2.0/tests/test_grouped_legends.py +319 -0
  44. logsuite-0.2.0/tests/test_hyphen_fix.py +224 -0
  45. logsuite-0.2.0/tests/test_las3_support.py +50 -0
  46. logsuite-0.2.0/tests/test_las_export_roundtrip.py +97 -0
  47. logsuite-0.2.0/tests/test_lint.py +34 -0
  48. logsuite-0.2.0/tests/test_log_scale_labels.py +139 -0
  49. logsuite-0.2.0/tests/test_md_well_standardization.py +224 -0
  50. logsuite-0.2.0/tests/test_naming_refactor.py +217 -0
  51. logsuite-0.2.0/tests/test_new_features.py +263 -0
  52. logsuite-0.2.0/tests/test_optimized_legend_placement.py +323 -0
  53. logsuite-0.2.0/tests/test_overwrite.py +177 -0
  54. logsuite-0.2.0/tests/test_param_locking.py +118 -0
  55. logsuite-0.2.0/tests/test_performance.py +205 -0
  56. logsuite-0.2.0/tests/test_polynomial_exponential_regression.py +289 -0
  57. logsuite-0.2.0/tests/test_printing.py +327 -0
  58. logsuite-0.2.0/tests/test_project_export.py +184 -0
  59. logsuite-0.2.0/tests/test_property_operations.py +497 -0
  60. logsuite-0.2.0/tests/test_regression_alias.py +245 -0
  61. logsuite-0.2.0/tests/test_regression_by_color_and_shape.py +307 -0
  62. logsuite-0.2.0/tests/test_regression_degree_suffixes.py +303 -0
  63. logsuite-0.2.0/tests/test_regression_title_with_type.py +170 -0
  64. logsuite-0.2.0/tests/test_resample_edge_cases.py +94 -0
  65. logsuite-0.2.0/tests/test_save_load.py +241 -0
  66. logsuite-0.2.0/tests/test_source_aware.py +189 -0
  67. logsuite-0.2.0/tests/test_statistics.py +383 -0
  68. logsuite-0.2.0/tests/test_sums_avg_report.py +123 -0
  69. logsuite-0.2.0/tests/test_thickness_accuracy.py +1198 -0
@@ -0,0 +1,2413 @@
1
+ Metadata-Version: 2.4
2
+ Name: logsuite
3
+ Version: 0.2.0
4
+ Summary: Petrophysical well log analysis with depth-weighted statistics, hierarchical filtering, and template-driven visualization
5
+ Author-email: Kristian dF Kollsgård <kkollsg@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kkollsga/logsuite
8
+ Project-URL: Documentation, https://logsuite.readthedocs.io
9
+ Project-URL: Repository, https://github.com/kkollsga/logsuite
10
+ Project-URL: Issues, https://github.com/kkollsga/logsuite/issues
11
+ Project-URL: Changelog, https://github.com/kkollsga/logsuite/blob/main/CHANGELOG.md
12
+ Keywords: well-log,las-file,petrophysics,geoscience,oil-and-gas,petroleum-engineering
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Operating System :: OS Independent
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ Requires-Dist: numpy>=1.20.0
28
+ Requires-Dist: pandas>=1.3.0
29
+ Requires-Dist: scipy>=1.7.0
30
+ Requires-Dist: matplotlib>=3.5.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
34
+ Requires-Dist: black>=23.0.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
36
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
37
+ Requires-Dist: towncrier>=23.6.0; extra == "dev"
38
+ Provides-Extra: docs
39
+ Requires-Dist: sphinx>=7.0; extra == "docs"
40
+ Requires-Dist: furo>=2024.0; extra == "docs"
41
+ Requires-Dist: myst-parser>=2.0; extra == "docs"
42
+ Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
43
+ Requires-Dist: sphinx-autodoc-typehints>=1.25; extra == "docs"
44
+
45
+ # logSuite
46
+
47
+ Fast, intuitive Python library for petrophysical well log analysis. Load LAS files, filter by zones, compute depth-weighted statistics, and create publication-quality log displays—all in just a few lines.
48
+
49
+ [![PyPI version](https://img.shields.io/pypi/v/logsuite.svg)](https://pypi.org/project/logsuite/)
50
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
51
+ [![CI](https://github.com/kkollsga/logsuite/actions/workflows/build-and-publish.yml/badge.svg)](https://github.com/kkollsga/logsuite/actions)
52
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
53
+
54
+ ## Key Features
55
+
56
+ - **🚀 Lazy Loading** - Parse headers instantly, load data on demand
57
+ - **🧮 Numpy-Style Operations** - `well.HC_Volume = well.PHIE * (1 - well.SW)`
58
+ - **🔍 Hierarchical Filtering** - Chain filters: `well.PHIE.filter('Zone').filter('Facies').sums_avg()`
59
+ - **⚖️ Depth-Weighted Statistics** - Proper averaging for irregular sampling
60
+ - **📊 Multi-Well Analytics** - Cross-well statistics: `manager.PHIE.filter('Zone').percentile(50)`
61
+ - **🎨 Professional Visualization** - Create customizable well log displays with templates
62
+ - **📊 Interactive Crossplots** - Beautiful scatter plots with color/size/shape mapping by property
63
+ - **📈 Regression Analysis** - 5 regression types (linear, polynomial, exponential, log, power)
64
+ - **💾 Project Persistence** - Save/load entire projects with metadata and templates
65
+
66
+ ---
67
+
68
+ ## Table of Contents
69
+
70
+ ### Getting Started
71
+ - [Installation](#installation)
72
+ - [1-Minute Tutorial](#1-minute-tutorial)
73
+ - [5-Minute Quick Start](#5-minute-quick-start)
74
+
75
+ ### Learning Path
76
+ - [Core Concepts](#core-concepts) - Essential patterns and workflows
77
+ - [Visualization Guide](#visualization-guide) - Creating well log displays
78
+ - [Crossplot & Regression Guide](#crossplot--regression-guide) - Data analysis and trend visualization
79
+ - [Advanced Topics](#advanced-topics) - Deep dives into specific features
80
+
81
+ ### Quick Reference
82
+ - [Style & Marker Reference](#style--marker-reference) - Line styles, markers, colors
83
+ - [Colormap Reference](#colormap-reference) - Available colormaps
84
+ - [API Reference](#api-reference) - Classes, methods, exceptions
85
+ - [Common Patterns](#common-patterns) - Copy-paste examples
86
+ - [Troubleshooting](#troubleshooting) - Solutions to common issues
87
+
88
+ ---
89
+
90
+ ## Installation
91
+
92
+ ```bash
93
+ pip install logsuite
94
+ ```
95
+
96
+ **Requirements:** Python 3.9+, numpy, pandas, scipy, matplotlib
97
+
98
+ ---
99
+
100
+ ## 1-Minute Tutorial
101
+
102
+ Load LAS files, filter by zones, and compute statistics:
103
+
104
+ ```python
105
+ from logsuite import WellDataManager
106
+
107
+ # Load and analyze
108
+ manager = WellDataManager()
109
+ manager.load_las('well.las')
110
+
111
+ well = manager.well_12_3_4_A
112
+ stats = well.PHIE.filter('Zone').sums_avg()
113
+
114
+ print(stats['Top_Brent']['mean']) # → 0.182 (depth-weighted)
115
+ ```
116
+
117
+ **That's it!** Three lines to go from LAS file to zonal statistics.
118
+
119
+ > **New to this?** Continue to [5-Minute Quick Start](#5-minute-quick-start) for a complete walkthrough.
120
+
121
+ ---
122
+
123
+ ## 5-Minute Quick Start
124
+
125
+ ### Step 1: Load Your Data
126
+
127
+ ```python
128
+ from logsuite import WellDataManager
129
+ import pandas as pd
130
+
131
+ # Create manager and load LAS files
132
+ manager = WellDataManager()
133
+ manager.load_las('well_A.las')
134
+ manager.load_las('well_B.las')
135
+
136
+ # Load formation tops from DataFrame
137
+ tops_df = pd.DataFrame({
138
+ 'Well': ['12/3-4 A', '12/3-4 A', '12/3-4 B'],
139
+ 'Surface': ['Top_Brent', 'Top_Statfjord', 'Top_Brent'],
140
+ 'MD': [2850.0, 3100.0, 2900.0]
141
+ })
142
+
143
+ manager.load_tops(tops_df, well_col='Well', discrete_col='Surface', depth_col='MD')
144
+ ```
145
+
146
+ ### Step 2: Access Wells and Properties
147
+
148
+ ```python
149
+ # Access well (special characters auto-sanitized)
150
+ well = manager.well_12_3_4_A
151
+
152
+ # Access properties directly
153
+ phie = well.PHIE
154
+ sw = well.SW
155
+
156
+ # List everything
157
+ print(well.properties) # ['PHIE', 'SW', 'PERM', 'Zone', ...]
158
+ print(well.sources) # ['Petrophysics', 'Imported_Tops']
159
+ ```
160
+
161
+ ### Step 3: Compute Statistics
162
+
163
+ ```python
164
+ # Single filter - group by Zone
165
+ stats = well.PHIE.filter('Zone').sums_avg()
166
+ # → {'Top_Brent': {'mean': 0.182, 'thickness': 250.0, ...}, ...}
167
+
168
+ # Chain filters - hierarchical grouping
169
+ stats = well.PHIE.filter('Zone').filter('Facies').sums_avg()
170
+ # → {'Top_Brent': {'Sandstone': {...}, 'Shale': {...}}, ...}
171
+ ```
172
+
173
+ > **💡 Key Insight:** Statistics are **depth-weighted** by default, accounting for irregular sampling.
174
+
175
+ ### Step 4: Create Computed Properties
176
+
177
+ ```python
178
+ # Mathematical expressions (numpy-style)
179
+ well.HC_Volume = well.PHIE * (1 - well.SW)
180
+ well.PHIE_percent = well.PHIE * 100
181
+
182
+ # Comparison operations (creates discrete flags)
183
+ well.Reservoir = (well.PHIE > 0.15) & (well.SW < 0.35)
184
+
185
+ # Apply to all wells at once
186
+ manager.PHIE_percent = manager.PHIE * 100
187
+ ```
188
+
189
+ ### Step 5: Visualize Well Logs
190
+
191
+ ```python
192
+ from logsuite import Template
193
+
194
+ # Create template
195
+ template = Template("basic")
196
+
197
+ # Add GR track
198
+ template.add_track(
199
+ track_type="continuous",
200
+ logs=[{"name": "GR", "x_range": [0, 150], "color": "green"}],
201
+ title="Gamma Ray"
202
+ )
203
+
204
+ # Add depth track
205
+ template.add_track(track_type="depth", width=0.3)
206
+
207
+ # Display
208
+ view = well.WellView(depth_range=[2800, 3000], template=template)
209
+ view.show()
210
+ view.save("well_log.png", dpi=300)
211
+ ```
212
+
213
+ ### Step 6: Save Your Work
214
+
215
+ ```python
216
+ # Save entire project
217
+ manager.save('my_project/')
218
+
219
+ # Load later
220
+ manager = WellDataManager('my_project/')
221
+ ```
222
+
223
+ **Done!** You've learned the core workflow in 5 minutes.
224
+
225
+ > **Next Steps:** Explore [Core Concepts](#core-concepts) to understand the library's design patterns, or jump to [Visualization Guide](#visualization-guide) for creating professional log displays.
226
+
227
+ ---
228
+
229
+ ## Core Concepts
230
+
231
+ ### Understanding Well Log Data
232
+
233
+ Well log data consists of measurements taken at various depths. This library organizes data into three key components:
234
+
235
+ 1. **Wells** - Individual wellbores (e.g., "12/3-4 A")
236
+ 2. **Properties** - Measurements or computed values (e.g., PHIE, SW, GR)
237
+ 3. **Sources** - Origin of data (e.g., "Petrophysics", "CoreData", "computed")
238
+
239
+ ```python
240
+ # Access structure
241
+ well = manager.well_12_3_4_A
242
+ print(well.sources) # ['Petrophysics', 'CoreData']
243
+ print(well.properties) # ['PHIE', 'SW', 'GR', ...]
244
+
245
+ # Get property
246
+ phie = well.PHIE # Shorthand
247
+ phie = well.get_property('PHIE') # Explicit
248
+ phie = well.Petrophysics.PHIE # From specific source
249
+ ```
250
+
251
+ ### Property Types
252
+
253
+ Properties can be **continuous** (numeric measurements), **discrete** (categories), or **sampled** (point measurements like core plugs):
254
+
255
+ ```python
256
+ # Continuous (default) - log curves
257
+ well.PHIE.type # → 'continuous'
258
+
259
+ # Discrete - zones, facies, flags
260
+ zone = well.get_property('Zone')
261
+ zone.type = 'discrete'
262
+ zone.labels = {0: 'Top_Brent', 1: 'Top_Statfjord', 2: 'Top_Cook'}
263
+
264
+ # Sampled - core plugs (arithmetic mean instead of depth-weighted)
265
+ core_phie = well.get_property('CorePHIE')
266
+ core_phie.type = 'sampled'
267
+ ```
268
+
269
+ ### Hierarchical Filtering
270
+
271
+ Filter properties by discrete logs to compute grouped statistics:
272
+
273
+ ```python
274
+ # Single filter
275
+ stats = well.PHIE.filter('Zone').sums_avg()
276
+ # {
277
+ # 'Top_Brent': {'mean': 0.21, 'thickness': 150.0, ...},
278
+ # 'Top_Statfjord': {'mean': 0.17, 'thickness': 180.0, ...}
279
+ # }
280
+
281
+ # Chain multiple filters for hierarchical grouping
282
+ stats = well.PHIE.filter('Zone').filter('Facies').sums_avg()
283
+ # {
284
+ # 'Top_Brent': {
285
+ # 'Sandstone': {'mean': 0.23, 'thickness': 120.0, ...},
286
+ # 'Shale': {'mean': 0.08, 'thickness': 30.0, ...}
287
+ # },
288
+ # 'Top_Statfjord': {...}
289
+ # }
290
+ ```
291
+
292
+ **Statistics include:**
293
+ - `mean`, `sum`, `std_dev` - Depth-weighted by default
294
+ - `percentile` - p10, p50, p90 values
295
+ - `thickness` - Depth interval thickness
296
+ - `samples` - Number of valid measurements
297
+ - `range`, `depth_range` - Min/max values and depths
298
+
299
+ ### Custom Interval Filtering
300
+
301
+ Define custom depth intervals without needing a discrete property in the well:
302
+
303
+ ```python
304
+ # Define intervals with name, top, and base
305
+ intervals = [
306
+ {"name": "Zone_A", "top": 2500, "base": 2650},
307
+ {"name": "Zone_B", "top": 2650, "base": 2800}
308
+ ]
309
+
310
+ # Use with sums_avg or discrete_summary
311
+ stats = well.PHIE.filter_intervals(intervals).sums_avg()
312
+ # → {'Zone_A': {'mean': 0.18, ...}, 'Zone_B': {'mean': 0.21, ...}}
313
+
314
+ facies_stats = well.Facies.filter_intervals(intervals).discrete_summary()
315
+ ```
316
+
317
+ **Overlapping intervals** are supported - each interval is calculated independently:
318
+
319
+ ```python
320
+ # These intervals overlap at 2600-2700m
321
+ intervals = [
322
+ {"name": "Full_Reservoir", "top": 2500, "base": 2800},
323
+ {"name": "Upper_Section", "top": 2500, "base": 2700}
324
+ ]
325
+ # Depths 2500-2700 are counted in BOTH zones
326
+ stats = well.PHIE.filter_intervals(intervals).sums_avg()
327
+ ```
328
+
329
+ **Save intervals for reuse:**
330
+
331
+ ```python
332
+ # Save intervals to the well
333
+ well.PHIE.filter_intervals(intervals, save="Reservoir_Zones")
334
+
335
+ # Use saved intervals by name
336
+ stats = well.PHIE.filter_intervals("Reservoir_Zones").sums_avg()
337
+
338
+ # List saved intervals
339
+ print(well.saved_intervals) # ['Reservoir_Zones']
340
+
341
+ # Retrieve intervals
342
+ intervals = well.get_intervals("Reservoir_Zones")
343
+ ```
344
+
345
+ **Save different intervals for multiple wells:**
346
+
347
+ ```python
348
+ # Define well-specific intervals
349
+ manager.well_A.PHIE.filter_intervals({
350
+ "Well_A": [{"name": "Zone_A", "top": 2500, "base": 2700}],
351
+ "Well_B": [{"name": "Zone_A", "top": 2600, "base": 2800}]
352
+ }, save="My_Zones")
353
+
354
+ # Both wells now have "My_Zones" saved with their respective intervals
355
+ ```
356
+
357
+ **Chain with other filters:**
358
+
359
+ ```python
360
+ # Combine custom intervals with property filters
361
+ stats = well.PHIE.filter_intervals(intervals).filter("NetFlag").sums_avg()
362
+ # → {'Zone_A': {'Net': {...}, 'NonNet': {...}}, 'Zone_B': {...}}
363
+ ```
364
+
365
+ > **💡 Key Difference:** Unlike `.filter('Well_Tops')` where each depth belongs to exactly one zone, `filter_intervals()` allows overlapping intervals where the same depths can contribute to multiple zones.
366
+
367
+ ### Property Operations
368
+
369
+ Create computed properties using natural mathematical syntax:
370
+
371
+ ```python
372
+ # Arithmetic operations (requires matching depth grids)
373
+ well.HC_Volume = well.PHIE * (1 - well.SW)
374
+ well.Porosity_Avg = (well.PHIE + well.PHIT) / 2
375
+
376
+ # Comparison operations (auto-creates discrete properties)
377
+ well.High_Poro = well.PHIE > 0.15
378
+ well.Reservoir = (well.PHIE > 0.15) & (well.SW < 0.35)
379
+
380
+ # Use computed properties in filtering
381
+ stats = well.PHIE.filter('Reservoir').sums_avg()
382
+ # → {False: {...}, True: {...}}
383
+ ```
384
+
385
+ > **💡 Pro Tip:** Computed properties are stored in the `'computed'` source and can be exported to LAS files.
386
+
387
+ ### Depth Alignment
388
+
389
+ Operations require matching depth grids (like numpy arrays) to prevent silent interpolation errors:
390
+
391
+ ```python
392
+ # This fails if depths don't match
393
+ result = well.PHIE + well.CorePHIE # DepthAlignmentError
394
+
395
+ # Explicit resampling required
396
+ core_resampled = well.CorePHIE.resample(well.PHIE)
397
+ result = well.PHIE + core_resampled # ✓ Works
398
+ ```
399
+
400
+ ### Multi-Well Analytics
401
+
402
+ Compute statistics across all wells in a single call:
403
+
404
+ ```python
405
+ # Single statistic across all wells
406
+ p50 = manager.PHIE.percentile(50)
407
+ # → {'well_A': 0.182, 'well_B': 0.195, 'well_C': 0.173}
408
+
409
+ # With filtering - grouped by filter values per well
410
+ stats = manager.PHIE.filter('Zone').percentile(50)
411
+ # → {
412
+ # 'well_A': {'Top_Brent': 0.21, 'Top_Statfjord': 0.15},
413
+ # 'well_B': {'Top_Brent': 0.19, 'Top_Statfjord': 0.17}
414
+ # }
415
+
416
+ # Chain filters for hierarchical grouping
417
+ stats = manager.PHIE.filter('Zone').filter('Facies').mean()
418
+
419
+ # All statistics: min, max, mean, median, std, percentile
420
+ ```
421
+
422
+ **Ambiguous properties** (existing in multiple sources) automatically nest by source:
423
+
424
+ ```python
425
+ # If well_A has PHIE in both 'log' and 'core' sources:
426
+ p50 = manager.PHIE.percentile(50)
427
+ # → {'well_A': {'log': 0.182, 'core': 0.205}, 'well_B': 0.195}
428
+ ```
429
+
430
+ ### Manager Broadcasting
431
+
432
+ Apply operations to all wells at once:
433
+
434
+ ```python
435
+ # Broadcast to all wells with PHIE
436
+ manager.PHIE_percent = manager.PHIE * 100
437
+
438
+ # Broadcast complex operations
439
+ manager.HC_Volume = manager.PHIE * (1 - manager.SW)
440
+ # ✓ Created property 'HC_Volume' in 12 well(s)
441
+ # ⚠ Skipped 3 well(s) without property 'PHIE' or 'SW'
442
+ ```
443
+
444
+ ### Depth-Weighted vs Arithmetic Statistics
445
+
446
+ Standard arithmetic mean fails with irregular sampling:
447
+
448
+ ```python
449
+ # Example: NTG flag at depths 1500m, 1501m, 1505m with values 0, 1, 0
450
+ # Arithmetic mean: (0+1+0)/3 = 0.33 ❌ (treats all samples equally)
451
+ # Weighted mean: accounts for 2.5m interval at 1501m = 0.50 ✓
452
+
453
+ # Compare both methods
454
+ stats = well.NTG.filter('Zone').sums_avg(arithmetic=True)
455
+ # Returns: {'mean': {'weighted': 0.50, 'arithmetic': 0.33}, ...}
456
+ ```
457
+
458
+ > **✨ Key Insight:** Depth-weighted statistics properly handle irregular sample spacing by accounting for depth intervals.
459
+
460
+ ### Project Persistence
461
+
462
+ Save and restore entire projects:
463
+
464
+ ```python
465
+ # Save project structure
466
+ manager.save('my_project/')
467
+ # Creates: my_project/well_12_3_4_A/Petrophysics.las, templates/*.json, ...
468
+
469
+ # Load project (restores everything)
470
+ manager = WellDataManager('my_project/')
471
+ ```
472
+
473
+ ---
474
+
475
+ ## Visualization Guide
476
+
477
+ Create publication-quality well log displays optimized for Jupyter Lab. Build customizable templates with multiple tracks showing continuous logs, discrete properties, fills, formation tops, and markers.
478
+
479
+ ### Quick Start
480
+
481
+ ```python
482
+ from logsuite import WellDataManager
483
+
484
+ # Load data
485
+ manager = WellDataManager()
486
+ manager.load_las("well.las")
487
+ well = manager.well_36_7_5_A
488
+
489
+ # Simple display with default template
490
+ view = well.WellView(depth_range=[2800, 3000])
491
+ view.show() # Displays inline in Jupyter
492
+
493
+ # Save to file
494
+ view.save("well_log.png", dpi=300)
495
+ ```
496
+
497
+ #### Auto-Calculate Depth Range from Tops
498
+
499
+ Instead of manually specifying depth ranges, let WellView automatically calculate the range from formation tops:
500
+
501
+ ```python
502
+ # Load formation tops
503
+ manager.load_tops(tops_df, well_col='Well', discrete_col='Surface', depth_col='MD')
504
+
505
+ # Add tops to template
506
+ template = Template("reservoir")
507
+ template.add_tops(property_name='Zone')
508
+
509
+ # Auto-calculate depth range from specific tops
510
+ view = well.WellView(
511
+ tops=['Top_Brent', 'Top_Statfjord'], # Specify which tops to show
512
+ template=template
513
+ )
514
+ view.show()
515
+ # Automatically shows Top_Brent to Top_Statfjord with 5% padding (min 50m range)
516
+ ```
517
+
518
+ **How it works:**
519
+ - Finds the minimum and maximum depths of specified tops
520
+ - Adds 5% padding above and below
521
+ - Ensures minimum range of 50 meters
522
+ - Perfect for focusing on specific intervals without manual depth calculations
523
+
524
+ ### Building Templates
525
+
526
+ Templates define the layout and styling of well log displays. Think of a template as a blueprint that can be reused across multiple wells.
527
+
528
+ #### Basic Template Structure
529
+
530
+ ```python
531
+ from logsuite import Template
532
+
533
+ # Create template
534
+ template = Template("reservoir")
535
+
536
+ # Add tracks (order matters - left to right)
537
+ template.add_track(track_type="continuous", logs=[...], title="GR")
538
+ template.add_track(track_type="continuous", logs=[...], title="Resistivity")
539
+ template.add_track(track_type="discrete", logs=[...], title="Facies")
540
+ template.add_track(track_type="depth", width=0.3, title="Depth")
541
+
542
+ # Add to project (saves with manager.save())
543
+ manager.add_template(template) # Uses template name "reservoir"
544
+
545
+ # Or save standalone file
546
+ template.save("reservoir_template.json")
547
+ ```
548
+
549
+ #### Track Types Explained
550
+
551
+ **1. Continuous Tracks** - For numeric log curves
552
+
553
+ Shows one or more curves with configurable scales, styles, fills, and markers.
554
+
555
+ ```python
556
+ template.add_track(
557
+ track_type="continuous",
558
+ logs=[
559
+ {
560
+ "name": "GR", # Property name
561
+ "x_range": [0, 150], # Scale limits [left, right]
562
+ "color": "green", # Line color
563
+ "style": "solid", # Line style (solid/dashed/dotted/none)
564
+ "thickness": 1.5, # Line width
565
+ "alpha": 0.8 # Transparency (0-1)
566
+ }
567
+ ],
568
+ title="Gamma Ray (API)",
569
+ log_scale=False # Use logarithmic scale?
570
+ )
571
+ ```
572
+
573
+ **2. Discrete Tracks** - For categorical data
574
+
575
+ Displays colored bands for facies, zones, or other categorical properties.
576
+
577
+ ```python
578
+ template.add_track(
579
+ track_type="discrete",
580
+ logs=[{"name": "Facies"}],
581
+ title="Lithofacies"
582
+ )
583
+ ```
584
+
585
+ Colors come from the property's color mapping:
586
+ ```python
587
+ facies = well.get_property('Facies')
588
+ facies.colors = {
589
+ 0: 'yellow', # Sand
590
+ 1: 'gray', # Shale
591
+ 2: 'lightblue' # Limestone
592
+ }
593
+ ```
594
+
595
+ **3. Depth Tracks** - Show depth axis
596
+
597
+ ```python
598
+ template.add_track(
599
+ track_type="depth",
600
+ width=0.3, # Narrow width
601
+ title="MD (m)"
602
+ )
603
+ ```
604
+
605
+ ### Styling Log Curves
606
+
607
+ #### Line Styles
608
+
609
+ ```python
610
+ logs=[
611
+ {"name": "GR", "style": "solid"}, # ─────
612
+ {"name": "CALI", "style": "dashed"}, # ─ ─ ─
613
+ {"name": "SP", "style": "dotted"}, # ·····
614
+ {"name": "TEMP", "style": "dashdot"}, # ─·─·─
615
+ {"name": "POINTS", "style": "none"} # (markers only)
616
+ ]
617
+ ```
618
+
619
+ **Supported styles:** `"solid"` (`"-"`), `"dashed"` (`"--"`), `"dotted"` (`":"`), `"dashdot"` (`"-."`), `"none"` (`""`)
620
+
621
+ #### Colors
622
+
623
+ ```python
624
+ logs=[
625
+ {"name": "RHOB", "color": "red"}, # Color names
626
+ {"name": "NPHI", "color": "#1f77b4"}, # Hex codes
627
+ {"name": "GR", "color": (0.2, 0.5, 0.8)} # RGB tuples
628
+ ]
629
+ ```
630
+
631
+ #### Thickness and Transparency
632
+
633
+ ```python
634
+ logs=[
635
+ {"name": "ILD", "thickness": 2.0, "alpha": 1.0}, # Thick, opaque
636
+ {"name": "ILM", "thickness": 1.0, "alpha": 0.6} # Thin, transparent
637
+ ]
638
+ ```
639
+
640
+ ### Markers for Data Points
641
+
642
+ Display markers at each data point to show actual measurement locations. Useful for sparse data like core plugs, pressure tests, or sample points.
643
+
644
+ #### Basic Markers
645
+
646
+ ```python
647
+ # Markers with line
648
+ logs=[{
649
+ "name": "PERM",
650
+ "x_range": [0.1, 1000],
651
+ "color": "green",
652
+ "style": "solid", # Show connecting line
653
+ "marker": "circle", # Add circular markers
654
+ "marker_size": 4, # Marker size
655
+ "marker_fill": "lightgreen" # Fill color (optional)
656
+ }]
657
+
658
+ # Markers only (no line)
659
+ logs=[{
660
+ "name": "CORE_PHIE",
661
+ "x_range": [0, 0.4],
662
+ "color": "blue",
663
+ "style": "none", # No connecting line
664
+ "marker": "diamond", # Diamond markers
665
+ "marker_size": 8,
666
+ "marker_outline_color": "darkblue",
667
+ "marker_fill": "yellow"
668
+ }]
669
+ ```
670
+
671
+ #### Marker Types
672
+
673
+ **Common markers:**
674
+ - `"circle"` (○), `"square"` (□), `"diamond"` (◇)
675
+ - `"triangle_up"` (△), `"triangle_down"` (▽)
676
+ - `"star"` (★), `"plus"` (+), `"cross"` (×)
677
+
678
+ **All supported markers:** See [Style & Marker Reference](#style--marker-reference)
679
+
680
+ #### Marker Configuration
681
+
682
+ ```python
683
+ logs=[{
684
+ "name": "SAMPLE_POINTS",
685
+ "marker": "circle", # Marker shape
686
+ "marker_size": 6, # Size (default: 6)
687
+ "marker_outline_color": "red", # Edge color (defaults to line color)
688
+ "marker_fill": "yellow", # Fill color (optional, default: unfilled)
689
+ "marker_interval": 5, # Show every 5th marker (default: 1)
690
+ }]
691
+ ```
692
+
693
+ **Marker interval** is useful for dense data - showing every nth marker reduces clutter:
694
+ ```python
695
+ # Show every 10th marker on a high-resolution log
696
+ {"name": "GR", "marker": "point", "marker_interval": 10}
697
+ ```
698
+
699
+ ### Fill Patterns
700
+
701
+ Fills highlight areas between curves or track edges. Useful for showing porosity, crossover, or lithology.
702
+
703
+ #### Solid Color Fill
704
+
705
+ Fill between a curve and a fixed value:
706
+
707
+ ```python
708
+ template.add_track(
709
+ track_type="continuous",
710
+ logs=[{"name": "PHIE", "x_range": [0.45, 0], "color": "blue"}],
711
+ fill={
712
+ "left": "PHIE", # Curve name
713
+ "right": 0, # Fixed value
714
+ "color": "lightblue",
715
+ "alpha": 0.5
716
+ }
717
+ )
718
+ ```
719
+
720
+ Fill between track edge and curve:
721
+
722
+ ```python
723
+ fill={
724
+ "left": "track_edge", # Left edge of track
725
+ "right": "GR", # GR curve
726
+ "color": "lightgreen",
727
+ "alpha": 0.3
728
+ }
729
+ ```
730
+
731
+ #### Colormap Fill
732
+
733
+ Create horizontal color bands where each depth interval is colored based on curve values:
734
+
735
+ ```python
736
+ template.add_track(
737
+ track_type="continuous",
738
+ logs=[{"name": "GR", "x_range": [0, 150], "color": "black"}],
739
+ fill={
740
+ "left": "track_edge",
741
+ "right": "GR",
742
+ "colormap": "viridis", # Colormap name
743
+ "color_range": [20, 150], # GR values map to colors
744
+ "alpha": 0.7
745
+ },
746
+ title="Gamma Ray"
747
+ )
748
+ # Low GR (20) → dark purple, High GR (150) → bright yellow
749
+ ```
750
+
751
+ **Popular colormaps:**
752
+ - `"viridis"` - Perceptually uniform (recommended)
753
+ - `"inferno"`, `"plasma"` - Dark to bright
754
+ - `"RdYlGn"` - Red-Yellow-Green (diverging)
755
+ - `"jet"` - Rainbow (not recommended for scientific use)
756
+
757
+ See [Colormap Reference](#colormap-reference) for all options.
758
+
759
+ #### Fill Between Two Curves
760
+
761
+ ```python
762
+ template.add_track(
763
+ track_type="continuous",
764
+ logs=[
765
+ {"name": "RHOB", "x_range": [1.95, 2.95], "color": "red"},
766
+ {"name": "NPHI", "x_range": [0.45, -0.15], "color": "blue"}
767
+ ],
768
+ fill={
769
+ "left": "RHOB",
770
+ "right": "NPHI",
771
+ "colormap": "RdYlGn",
772
+ "colormap_curve": "NPHI", # Use NPHI values for colors
773
+ "color_range": [-0.15, 0.45],
774
+ "alpha": 0.6
775
+ },
776
+ title="Density-Neutron Crossover"
777
+ )
778
+ ```
779
+
780
+ #### Multiple Fills
781
+
782
+ Apply multiple fills to a single track (drawn in order):
783
+
784
+ ```python
785
+ template.add_track(
786
+ track_type="continuous",
787
+ logs=[
788
+ {"name": "PHIE", "x_range": [0.45, 0], "color": "blue"},
789
+ {"name": "SW", "x_range": [0, 1], "color": "red"}
790
+ ],
791
+ fill=[
792
+ # Fill 1: PHIE to zero
793
+ {
794
+ "left": "PHIE",
795
+ "right": 0,
796
+ "color": "lightblue",
797
+ "alpha": 0.3
798
+ },
799
+ # Fill 2: SW to one
800
+ {
801
+ "left": "SW",
802
+ "right": 1,
803
+ "color": "lightcoral",
804
+ "alpha": 0.3
805
+ }
806
+ ]
807
+ )
808
+ ```
809
+
810
+ ### Formation Tops
811
+
812
+ Add horizontal lines marking formation boundaries across all tracks:
813
+
814
+ ```python
815
+ # Add tops to template (applies to all wells using this template)
816
+ template.add_tops(property_name='Zone')
817
+
818
+ # Or add tops to specific view (only this display)
819
+ view = well.WellView(template=template)
820
+ view.add_tops(property_name='Zone')
821
+ view.show()
822
+
823
+ # Or provide tops manually
824
+ view.add_tops(
825
+ tops_dict={
826
+ 2850.0: 'Top Brent',
827
+ 3100.0: 'Top Statfjord',
828
+ 3400.0: 'Base Statfjord'
829
+ },
830
+ colors={
831
+ 2850.0: 'yellow',
832
+ 3100.0: 'orange',
833
+ 3400.0: 'brown'
834
+ }
835
+ )
836
+ ```
837
+
838
+ Tops can also be added to individual tracks:
839
+
840
+ ```python
841
+ template.add_track(
842
+ track_type="discrete",
843
+ logs=[{"name": "Facies"}],
844
+ tops={
845
+ "name": "Zone", # Property containing tops
846
+ "line_style": "--", # Dashed lines
847
+ "line_width": 2.0, # Line thickness
848
+ "title_size": 9, # Label font size
849
+ "title_weight": "bold", # Font weight
850
+ "title_orientation": "right", # Label position (left/center/right)
851
+ "line_offset": 0.0 # Horizontal offset
852
+ }
853
+ )
854
+ ```
855
+
856
+ ### Logarithmic Scales
857
+
858
+ Use logarithmic scales for resistivity, permeability, or other exponential data:
859
+
860
+ ```python
861
+ # Track-level log scale (applies to all logs in track)
862
+ template.add_track(
863
+ track_type="continuous",
864
+ logs=[
865
+ {"name": "ILD", "x_range": [0.2, 2000], "color": "red"},
866
+ {"name": "ILM", "x_range": [0.2, 2000], "color": "green"}
867
+ ],
868
+ title="Resistivity",
869
+ log_scale=True # Logarithmic x-axis
870
+ )
871
+
872
+ # Per-log scale override
873
+ template.add_track(
874
+ track_type="continuous",
875
+ logs=[
876
+ {"name": "ILD", "x_range": [0.2, 2000], "color": "red"}, # Uses track log_scale
877
+ {"name": "GR", "x_range": [0, 150], "scale": "linear", "color": "green"} # Override
878
+ ],
879
+ log_scale=True # Default for track
880
+ )
881
+ ```
882
+
883
+ ### Using Templates
884
+
885
+ **Option 1: Pass template directly**
886
+ ```python
887
+ view = well.WellView(depth_range=[2800, 3000], template=template)
888
+ view.show()
889
+ ```
890
+
891
+ **Option 2: Store in manager (recommended for multi-well projects)**
892
+ ```python
893
+ # Store template in manager (uses template.name automatically)
894
+ manager.add_template(template)
895
+
896
+ # Use by name in any well
897
+ view = well.WellView(depth_range=[2800, 3000], template="reservoir")
898
+ view.show()
899
+
900
+ # List all templates
901
+ print(manager.list_templates()) # ['reservoir', 'qc', 'basic']
902
+
903
+ # Templates save with projects
904
+ manager.save("my_project/")
905
+ # Creates: my_project/templates/reservoir.json
906
+ ```
907
+
908
+ **Option 3: Load from file**
909
+ ```python
910
+ template = Template.load("reservoir_template.json")
911
+ view = well.WellView(depth_range=[2800, 3000], template=template)
912
+ ```
913
+
914
+ ### Template Management
915
+
916
+ ```python
917
+ # Retrieve template
918
+ template = manager.get_template("reservoir")
919
+
920
+ # List all templates
921
+ templates = manager.list_templates()
922
+
923
+ # View tracks in template
924
+ df = template.list_tracks()
925
+ print(df)
926
+ # Index Type Logs Title Width
927
+ # 0 0 continuous [GR] Gamma Ray 1.0
928
+ # 1 1 continuous [PHIE, SW] Porosity 1.0
929
+ # 2 2 depth [] Depth 0.3
930
+
931
+ # Edit track
932
+ template.edit_track(0, title="New Title")
933
+
934
+ # Remove track
935
+ template.remove_track(2)
936
+
937
+ # Add new track
938
+ template.add_track(track_type="continuous", logs=[{"name": "RT"}])
939
+
940
+ # Save changes
941
+ manager.add_template(template) # Update in manager (uses template.name)
942
+ template.save("updated_template.json") # Save to file
943
+ ```
944
+
945
+ ### Customization
946
+
947
+ #### Figure Settings
948
+
949
+ ```python
950
+ view = well.WellView(
951
+ depth_range=[2800, 3000],
952
+ template="reservoir",
953
+ figsize=(12, 10), # Width x height in inches
954
+ dpi=100 # Resolution (default: 100)
955
+ )
956
+ ```
957
+
958
+ #### Track Widths
959
+
960
+ Control relative track widths:
961
+
962
+ ```python
963
+ template.add_track(track_type="continuous", logs=[...], width=1.0) # Normal
964
+ template.add_track(track_type="discrete", logs=[...], width=1.5) # 50% wider
965
+ template.add_track(track_type="depth", width=0.3) # Narrow
966
+ ```
967
+
968
+ #### Export Options
969
+
970
+ ```python
971
+ # PNG for presentations (raster)
972
+ view.save("well_log.png", dpi=300)
973
+
974
+ # PDF for publications (vector)
975
+ view.save("well_log.pdf")
976
+
977
+ # SVG for editing in Illustrator/Inkscape (vector)
978
+ view.save("well_log.svg")
979
+ ```
980
+
981
+ ### Complete Example
982
+
983
+ A comprehensive template showcasing all features:
984
+
985
+ ```python
986
+ from logsuite import WellDataManager, Template
987
+
988
+ # Setup
989
+ manager = WellDataManager()
990
+ manager.load_las("well.las")
991
+ well = manager.well_36_7_5_A
992
+
993
+ # Create template
994
+ template = Template("comprehensive")
995
+
996
+ # Track 1: GR with colormap and markers
997
+ template.add_track(
998
+ track_type="continuous",
999
+ logs=[{
1000
+ "name": "GR",
1001
+ "x_range": [0, 150],
1002
+ "color": "black",
1003
+ "marker": "point",
1004
+ "marker_interval": 20 # Show every 20th sample
1005
+ }],
1006
+ fill={
1007
+ "left": "track_edge",
1008
+ "right": "GR",
1009
+ "colormap": "viridis",
1010
+ "color_range": [20, 150],
1011
+ "alpha": 0.7
1012
+ },
1013
+ title="Gamma Ray (API)"
1014
+ )
1015
+
1016
+ # Track 2: Resistivity (log scale)
1017
+ template.add_track(
1018
+ track_type="continuous",
1019
+ logs=[
1020
+ {"name": "ILD", "x_range": [0.2, 2000], "color": "red", "thickness": 1.5},
1021
+ {"name": "ILM", "x_range": [0.2, 2000], "color": "green"}
1022
+ ],
1023
+ title="Resistivity (ohmm)",
1024
+ log_scale=True
1025
+ )
1026
+
1027
+ # Track 3: Density-Neutron with crossover
1028
+ template.add_track(
1029
+ track_type="continuous",
1030
+ logs=[
1031
+ {"name": "RHOB", "x_range": [1.95, 2.95], "color": "red"},
1032
+ {"name": "NPHI", "x_range": [0.45, -0.15], "color": "blue"}
1033
+ ],
1034
+ fill={
1035
+ "left": "RHOB",
1036
+ "right": "NPHI",
1037
+ "colormap": "RdYlGn",
1038
+ "alpha": 0.5
1039
+ },
1040
+ title="Density-Neutron"
1041
+ )
1042
+
1043
+ # Track 4: Porosity & Saturation
1044
+ template.add_track(
1045
+ track_type="continuous",
1046
+ logs=[
1047
+ {"name": "PHIE", "x_range": [0.45, 0], "color": "blue"},
1048
+ {"name": "SW", "x_range": [0, 1], "color": "red"}
1049
+ ],
1050
+ fill={
1051
+ "left": "PHIE",
1052
+ "right": 0,
1053
+ "color": "lightblue",
1054
+ "alpha": 0.5
1055
+ },
1056
+ title="PHIE & SW"
1057
+ )
1058
+
1059
+ # Track 5: Core data (markers only, no lines)
1060
+ template.add_track(
1061
+ track_type="continuous",
1062
+ logs=[{
1063
+ "name": "CorePHIE",
1064
+ "x_range": [0, 0.4],
1065
+ "color": "darkblue",
1066
+ "style": "none", # No connecting line
1067
+ "marker": "diamond",
1068
+ "marker_size": 8,
1069
+ "marker_outline_color": "darkblue",
1070
+ "marker_fill": "yellow"
1071
+ }],
1072
+ title="Core Porosity"
1073
+ )
1074
+
1075
+ # Track 6: Facies with tops
1076
+ template.add_track(
1077
+ track_type="discrete",
1078
+ logs=[{"name": "Facies"}],
1079
+ title="Lithofacies"
1080
+ )
1081
+
1082
+ # Track 7: Depth
1083
+ template.add_track(track_type="depth", width=0.3, title="MD (m)")
1084
+
1085
+ # Add formation tops spanning all tracks
1086
+ template.add_tops(property_name='Zone')
1087
+
1088
+ # Add to project and display
1089
+ manager.add_template(template)
1090
+ view = well.WellView(depth_range=[2800, 3200], template="comprehensive")
1091
+ view.save("comprehensive_log.png", dpi=300)
1092
+ ```
1093
+
1094
+ ---
1095
+
1096
+ ## Crossplot & Regression Guide
1097
+
1098
+ Create beautiful, publication-quality crossplots for petrophysical analysis with sophisticated color/size/shape mapping and built-in regression analysis.
1099
+
1100
+ ### Quick Start
1101
+
1102
+ ```python
1103
+ from logsuite import WellDataManager
1104
+
1105
+ manager = WellDataManager()
1106
+ manager.load_las("well.las")
1107
+ well = manager.well_36_7_5_A
1108
+
1109
+ # Simple crossplot
1110
+ plot = well.Crossplot(x="RHOB", y="NPHI")
1111
+ plot.show()
1112
+ ```
1113
+
1114
+ That's it! One line to create a scatter plot from any two properties.
1115
+
1116
+ ### Basic Crossplots
1117
+
1118
+ #### Single Well Analysis
1119
+
1120
+ ```python
1121
+ # Density vs Neutron Porosity
1122
+ plot = well.Crossplot(
1123
+ x="RHOB",
1124
+ y="NPHI",
1125
+ title="Density-Neutron Crossplot"
1126
+ )
1127
+ plot.show()
1128
+
1129
+ # Save high-resolution image
1130
+ plot.save("density_neutron.png", dpi=300)
1131
+ ```
1132
+
1133
+ #### Multi-Well Comparison
1134
+
1135
+ Compare multiple wells on the same plot:
1136
+
1137
+ ```python
1138
+ # All wells with different markers
1139
+ plot = manager.Crossplot(
1140
+ x="PHIE",
1141
+ y="SW",
1142
+ shape="well", # Different marker shape per well
1143
+ title="Multi-Well Porosity vs Saturation"
1144
+ )
1145
+ plot.show()
1146
+
1147
+ # Specific wells only
1148
+ plot = manager.Crossplot(
1149
+ x="RHOB",
1150
+ y="NPHI",
1151
+ wells=["Well_A", "Well_B", "Well_C"],
1152
+ shape="well"
1153
+ )
1154
+ plot.show()
1155
+ ```
1156
+
1157
+ ### Advanced Mapping
1158
+
1159
+ #### Color by Property or Depth
1160
+
1161
+ Visualize a third dimension using color:
1162
+
1163
+ ```python
1164
+ # Color by depth
1165
+ plot = well.Crossplot(
1166
+ x="PHIE",
1167
+ y="SW",
1168
+ color="depth",
1169
+ colortemplate="viridis",
1170
+ color_range=[2000, 2500], # Depth range in meters
1171
+ title="Porosity vs SW (colored by depth)"
1172
+ )
1173
+ plot.show()
1174
+
1175
+ # Color by shale volume
1176
+ plot = well.Crossplot(
1177
+ x="PHIE",
1178
+ y="PERM",
1179
+ color="VSH",
1180
+ colortemplate="RdYlGn_r", # Red=high shale, Green=low shale
1181
+ title="Porosity-Permeability (colored by VSH)"
1182
+ )
1183
+ plot.show()
1184
+ ```
1185
+
1186
+ **Available colormaps:** `"viridis"`, `"plasma"`, `"coolwarm"`, `"RdYlGn"`, `"jet"`, and 100+ more matplotlib colormaps.
1187
+
1188
+ #### Size by Property
1189
+
1190
+ Make marker size represent a fourth dimension:
1191
+
1192
+ ```python
1193
+ plot = well.Crossplot(
1194
+ x="PHIE",
1195
+ y="SW",
1196
+ size="PERM", # Bigger markers = higher permeability
1197
+ size_range=(20, 200), # Min/max marker sizes
1198
+ color="depth",
1199
+ colortemplate="viridis",
1200
+ title="Porosity vs SW (sized by PERM)"
1201
+ )
1202
+ plot.show()
1203
+ ```
1204
+
1205
+ #### Shape by Category
1206
+
1207
+ Use different marker shapes for different groups:
1208
+
1209
+ ```python
1210
+ # Different shapes for different facies
1211
+ plot = well.Crossplot(
1212
+ x="PHIE",
1213
+ y="PERM",
1214
+ shape="Facies", # Different marker per facies type
1215
+ color="depth",
1216
+ title="Porosity-Permeability by Facies"
1217
+ )
1218
+ plot.show()
1219
+
1220
+ # Multi-well: different shapes per well
1221
+ plot = manager.Crossplot(
1222
+ x="PHIE",
1223
+ y="SW",
1224
+ shape="well", # Circle, square, triangle, etc.
1225
+ color="VSH",
1226
+ size="PERM"
1227
+ )
1228
+ plot.show()
1229
+ ```
1230
+
1231
+ #### All Features Combined
1232
+
1233
+ Combine color, size, and shape for comprehensive visualization:
1234
+
1235
+ ```python
1236
+ plot = manager.Crossplot(
1237
+ x="PHIE",
1238
+ y="SW",
1239
+ wells=["Well_A", "Well_B"], # Specific wells
1240
+ shape="well", # Different marker per well
1241
+ color="depth", # Color by depth
1242
+ size="PERM", # Size by permeability
1243
+ colortemplate="viridis",
1244
+ color_range=[2000, 2500],
1245
+ size_range=(30, 200),
1246
+ title="Multi-Dimensional Analysis",
1247
+ figsize=(12, 10),
1248
+ dpi=150
1249
+ )
1250
+ plot.show()
1251
+ ```
1252
+
1253
+ #### Multi-Layer Crossplots
1254
+
1255
+ Combine different data types (Core vs Sidewall, different property pairs) in a single plot with automatic shape/color encoding:
1256
+
1257
+ ```python
1258
+ # Compare Core and Sidewall data with regression by well
1259
+ plot = manager.Crossplot(
1260
+ layers={
1261
+ "Core": ['CorePor', 'CorePerm'],
1262
+ "Sidewall": ["SidewallPor", "SidewallPerm"]
1263
+ },
1264
+ color="Formation", # Color by formation
1265
+ shape="NetSand", # Shape by net sand flag
1266
+ regression_by_color="exponential-polynomial", # Separate trend per formation
1267
+ y_log=True, # Log scale for permeability
1268
+ title="Core vs Sidewall Analysis"
1269
+ )
1270
+ plot.show()
1271
+
1272
+ # Simpler version - automatic defaults
1273
+ manager.Crossplot(
1274
+ layers={
1275
+ "Core": ['CorePor', 'CorePerm'],
1276
+ "Sidewall": ["SidewallPor", "SidewallPerm"]
1277
+ },
1278
+ y_log=True
1279
+ ).show()
1280
+ # Automatically uses shape="label" (different markers per layer)
1281
+ # and color="well" (different colors per well)
1282
+ ```
1283
+
1284
+ **How it works:**
1285
+
1286
+ - `layers` dict maps labels to [x, y] property pairs
1287
+ - Each layer gets combined in one plot with unified axes
1288
+ - Shape defaults to `"label"` (Core gets circles, Sidewall gets squares)
1289
+ - Color defaults to `"well"` for multi-well visualization
1290
+ - Perfect for comparing different measurement types (Core plugs vs Formation tests)
1291
+
1292
+ ### Logarithmic Scales
1293
+
1294
+ Perfect for permeability and resistivity data:
1295
+
1296
+ ```python
1297
+ # Log scale on x-axis (permeability)
1298
+ plot = well.Crossplot(
1299
+ x="PERM",
1300
+ y="PHIE",
1301
+ x_log=True,
1302
+ title="Porosity-Permeability (log scale)"
1303
+ )
1304
+ plot.show()
1305
+
1306
+ # Log-log plot
1307
+ plot = well.Crossplot(
1308
+ x="PERM",
1309
+ y="Pressure",
1310
+ x_log=True,
1311
+ y_log=True,
1312
+ title="Log-Log Analysis"
1313
+ )
1314
+ plot.show()
1315
+ ```
1316
+
1317
+ ### Depth Filtering
1318
+
1319
+ Focus on specific intervals:
1320
+
1321
+ ```python
1322
+ # Reservoir zone only
1323
+ plot = well.Crossplot(
1324
+ x="PHIE",
1325
+ y="SW",
1326
+ depth_range=(2000, 2500),
1327
+ color="VSH",
1328
+ title="Reservoir Zone Analysis (2000-2500m)"
1329
+ )
1330
+ plot.show()
1331
+ ```
1332
+
1333
+ ### Regression Analysis
1334
+
1335
+ Add trend lines to identify relationships between properties.
1336
+
1337
+ #### Linear Regression
1338
+
1339
+ ```python
1340
+ plot = well.Crossplot(x="RHOB", y="NPHI", title="Density-Neutron")
1341
+
1342
+ # Add linear regression
1343
+ plot.add_regression("linear", line_color="red", line_width=2)
1344
+ plot.show()
1345
+
1346
+ # Access regression results
1347
+ reg = plot.regressions["linear"]
1348
+ print(reg.equation()) # y = -0.2956x + 0.9305
1349
+ print(f"R² = {reg.r_squared:.4f}") # R² = 0.8147
1350
+ print(f"RMSE = {reg.rmse:.4f}") # RMSE = 0.0208
1351
+ ```
1352
+
1353
+ #### Multiple Regression Types
1354
+
1355
+ Compare different regression models:
1356
+
1357
+ ```python
1358
+ plot = well.Crossplot(x="PHIE", y="SW", title="Porosity vs Saturation")
1359
+
1360
+ # Add multiple regressions
1361
+ plot.add_regression("linear", line_color="red")
1362
+ plot.add_regression("polynomial", degree=2, line_color="blue")
1363
+ plot.add_regression("exponential", line_color="green")
1364
+
1365
+ plot.show()
1366
+
1367
+ # Compare R² values
1368
+ for name, reg in plot.regressions.items():
1369
+ print(f"{name}: R² = {reg.r_squared:.4f}")
1370
+ # linear: R² = 0.0144
1371
+ # polynomial: R² = 0.0155
1372
+ # exponential: R² = 0.0201 ← Best fit
1373
+ ```
1374
+
1375
+ #### Available Regression Types
1376
+
1377
+ | Type | Equation | Use Case | Example |
1378
+ |------|----------|----------|---------|
1379
+ | `"linear"` | y = ax + b | Straight trends | Density-Porosity |
1380
+ | `"polynomial"` | y = aₙxⁿ + ... + a₁x + a₀ | Curved relationships | Sonic-Porosity |
1381
+ | `"exponential"` | y = ae^(bx) | Exponential growth | Production decline |
1382
+ | `"logarithmic"` | y = a·ln(x) + b | Diminishing returns | Time-dependent |
1383
+ | `"power"` | y = ax^b | Power law | Porosity-Permeability |
1384
+
1385
+ #### Polynomial Regression
1386
+
1387
+ Fit higher-order polynomials for curved relationships:
1388
+
1389
+ ```python
1390
+ plot = well.Crossplot(x="DT", y="PHIE")
1391
+
1392
+ # Quadratic (degree 2)
1393
+ plot.add_regression("polynomial", degree=2, line_color="blue")
1394
+
1395
+ # Cubic (degree 3)
1396
+ plot.add_regression("polynomial", degree=3, line_color="green", name="cubic")
1397
+
1398
+ plot.show()
1399
+ ```
1400
+
1401
+ #### Regression Customization
1402
+
1403
+ Control regression line appearance:
1404
+
1405
+ ```python
1406
+ plot.add_regression(
1407
+ "linear",
1408
+ name="best_fit", # Custom name
1409
+ line_color="red", # Line color
1410
+ line_width=2, # Line thickness
1411
+ line_style="--", # Dashed: "--", dotted: ":", solid: "-"
1412
+ line_alpha=0.8, # Transparency (0-1)
1413
+ show_equation=True, # Show equation in legend
1414
+ show_r2=True # Show R² value
1415
+ )
1416
+ ```
1417
+
1418
+ #### Using Regression for Predictions
1419
+
1420
+ Extract regression objects and use them for calculations:
1421
+
1422
+ ```python
1423
+ plot = well.Crossplot(x="RHOB", y="NPHI")
1424
+ plot.add_regression("linear")
1425
+
1426
+ # Get regression object
1427
+ reg = plot.regressions["linear"]
1428
+
1429
+ # Predict values
1430
+ density_values = [2.3, 2.4, 2.5, 2.6]
1431
+ predicted_nphi = reg(density_values)
1432
+ print(predicted_nphi) # [0.249, 0.220, 0.191, 0.161]
1433
+
1434
+ # Or use predict method
1435
+ predicted_nphi = reg.predict(density_values)
1436
+
1437
+ # Get statistics
1438
+ print(f"Equation: {reg.equation()}")
1439
+ print(f"R²: {reg.r_squared:.4f}")
1440
+ print(f"RMSE: {reg.rmse:.4f}")
1441
+ ```
1442
+
1443
+ ### Standalone Regression Classes
1444
+
1445
+ Use regression classes independently for data analysis:
1446
+
1447
+ ```python
1448
+ from logsuite import LinearRegression, PolynomialRegression
1449
+ import numpy as np
1450
+
1451
+ # Prepare data
1452
+ x_data = np.array([2.2, 2.3, 2.4, 2.5, 2.6])
1453
+ y_data = np.array([0.28, 0.25, 0.22, 0.19, 0.16])
1454
+
1455
+ # Fit linear regression
1456
+ reg = LinearRegression()
1457
+ reg.fit(x_data, y_data)
1458
+
1459
+ # Get results
1460
+ print(reg.equation()) # y = -0.3000x + 0.9400
1461
+ print(f"R² = {reg.r_squared}") # R² = 1.0000
1462
+ print(f"RMSE = {reg.rmse}") # RMSE = 0.0000
1463
+
1464
+ # Make predictions
1465
+ new_densities = [2.35, 2.45, 2.55]
1466
+ predicted = reg(new_densities)
1467
+ print(predicted) # [0.235, 0.205, 0.175]
1468
+
1469
+ # Try polynomial
1470
+ poly = PolynomialRegression(degree=2)
1471
+ poly.fit(x_data, y_data)
1472
+ print(poly.equation())
1473
+ ```
1474
+
1475
+ #### All Regression Classes
1476
+
1477
+ ```python
1478
+ from logsuite import (
1479
+ LinearRegression, # y = ax + b
1480
+ PolynomialRegression, # y = aₙxⁿ + ... + a₀
1481
+ ExponentialRegression, # y = ae^(bx)
1482
+ LogarithmicRegression, # y = a·ln(x) + b
1483
+ PowerRegression # y = ax^b
1484
+ )
1485
+
1486
+ # Each has the same interface
1487
+ reg = LinearRegression()
1488
+ reg.fit(x, y)
1489
+ y_pred = reg.predict(x_new)
1490
+ print(reg.equation())
1491
+ print(reg.r_squared)
1492
+ print(reg.rmse)
1493
+ ```
1494
+
1495
+ ### Customization Options
1496
+
1497
+ Fine-tune your crossplot appearance:
1498
+
1499
+ ```python
1500
+ plot = well.Crossplot(
1501
+ x="RHOB",
1502
+ y="NPHI",
1503
+ # Plot settings
1504
+ title="Custom Crossplot",
1505
+ xlabel="Bulk Density (g/cc)",
1506
+ ylabel="Neutron Porosity (v/v)",
1507
+ figsize=(12, 10), # Figure size (width, height)
1508
+ dpi=150, # Resolution
1509
+
1510
+ # Marker settings
1511
+ marker="D", # Diamond markers
1512
+ marker_size=80, # Larger markers
1513
+ marker_alpha=0.7, # 70% opaque
1514
+ edge_color="darkblue", # Marker outline color
1515
+ edge_width=1.5, # Outline thickness
1516
+
1517
+ # Grid settings
1518
+ grid=True,
1519
+ grid_alpha=0.3, # Subtle grid
1520
+
1521
+ # Display options
1522
+ show_colorbar=True, # Show colorbar
1523
+ show_legend=True # Show legend
1524
+ )
1525
+ plot.show()
1526
+ ```
1527
+
1528
+ **Marker styles:** `"o"` (circle), `"s"` (square), `"^"` (triangle), `"D"` (diamond), `"v"` (inverted triangle), `"*"` (star), `"+"` (plus), `"x"` (cross)
1529
+
1530
+ ### Practical Examples
1531
+
1532
+ #### Porosity-Permeability Analysis
1533
+
1534
+ ```python
1535
+ # Classic log-scale relationship
1536
+ plot = well.Crossplot(
1537
+ x="PHIE",
1538
+ y="PERM",
1539
+ y_log=True, # Log scale for permeability
1540
+ color="depth",
1541
+ colortemplate="viridis",
1542
+ title="Porosity-Permeability Transform"
1543
+ )
1544
+
1545
+ # Add power law regression (typical for poro-perm)
1546
+ plot.add_regression("power", line_color="red", line_width=2)
1547
+ plot.show()
1548
+
1549
+ # Use regression for permeability prediction
1550
+ power_reg = plot.regressions["power"]
1551
+ print(power_reg.equation()) # y = 2.5*x^3.2
1552
+
1553
+ # Predict permeability from porosity
1554
+ porosities = [0.10, 0.15, 0.20, 0.25, 0.30]
1555
+ perms = power_reg(porosities)
1556
+ print(perms) # [0.003, 0.025, 0.100, 0.275, 0.562] mD
1557
+ ```
1558
+
1559
+ #### Reservoir Quality Classification
1560
+
1561
+ ```python
1562
+ # Multi-well reservoir quality
1563
+ plot = manager.Crossplot(
1564
+ x="PHIE",
1565
+ y="SW",
1566
+ shape="well", # Different marker per well
1567
+ color="VSH", # Color by shale volume
1568
+ size="PERM", # Size by permeability
1569
+ colortemplate="RdYlGn_r", # Red=shaly, Green=clean
1570
+ title="Reservoir Quality Classification"
1571
+ )
1572
+
1573
+ # Add cutoff lines
1574
+ plot.add_regression("linear", line_color="red", show_equation=False)
1575
+ plot.show()
1576
+
1577
+ # Identify sweet spots: PHIE > 0.15 and SW < 0.4
1578
+ ```
1579
+
1580
+ #### Lithology Identification
1581
+
1582
+ ```python
1583
+ # Density-Neutron crossplot for lithology
1584
+ plot = well.Crossplot(
1585
+ x="RHOB",
1586
+ y="NPHI",
1587
+ color="GR", # Color by gamma ray
1588
+ colortemplate="viridis",
1589
+ color_range=[0, 150],
1590
+ title="Density-Neutron Lithology Plot"
1591
+ )
1592
+
1593
+ # Add lithology lines
1594
+ plot.add_regression("linear", line_color="yellow", name="Sandstone")
1595
+ plot.add_regression("polynomial", degree=2, line_color="gray", name="Shale")
1596
+ plot.show()
1597
+ ```
1598
+
1599
+ ### Best Practices
1600
+
1601
+ 1. **Choose appropriate scales:** Use log scales for permeability, resistivity
1602
+ 2. **Color consistency:** Specify `color_range` to keep colors consistent across plots
1603
+ 3. **Multiple regressions:** Try different types and compare R² values
1604
+ 4. **Depth filtering:** Focus on specific intervals with `depth_range`
1605
+ 5. **Save high-res:** Use `dpi=300` for publication-quality images
1606
+
1607
+ ### Quick Reference
1608
+
1609
+ ```python
1610
+ # Basic crossplot
1611
+ plot = well.Crossplot(x="RHOB", y="NPHI")
1612
+ plot.show()
1613
+
1614
+ # With color and size
1615
+ plot = well.Crossplot(x="PHIE", y="SW", color="depth", size="PERM")
1616
+ plot.show()
1617
+
1618
+ # Multi-well
1619
+ plot = manager.Crossplot(x="PHIE", y="SW", shape="well")
1620
+ plot.show()
1621
+
1622
+ # With regression
1623
+ plot = well.Crossplot(x="RHOB", y="NPHI")
1624
+ plot.add_regression("linear", line_color="red")
1625
+ plot.show()
1626
+
1627
+ # Standalone regression
1628
+ from logsuite import LinearRegression
1629
+ reg = LinearRegression()
1630
+ reg.fit(x, y)
1631
+ predictions = reg([10, 20, 30])
1632
+ ```
1633
+
1634
+ For comprehensive examples and API details, see:
1635
+ - **[CROSSPLOT_README.md](CROSSPLOT_README.md)** - Complete documentation
1636
+ - **[CROSSPLOT_QUICK_REFERENCE.md](CROSSPLOT_QUICK_REFERENCE.md)** - Quick reference card
1637
+ - **[examples/crossplot_examples.py](examples/crossplot_examples.py)** - 15+ examples
1638
+
1639
+ ---
1640
+
1641
+ ## Style & Marker Reference
1642
+
1643
+ ### Line Styles
1644
+
1645
+ | Style Name | Code | Example | Usage |
1646
+ |------------|------|---------|-------|
1647
+ | `"solid"` | `"-"` | ───── | Default, primary curves |
1648
+ | `"dashed"` | `"--"` | ─ ─ ─ | Secondary curves |
1649
+ | `"dotted"` | `":"` | ····· | Tertiary curves |
1650
+ | `"dashdot"` | `"-."` | ─·─·─ | Alternate curves |
1651
+ | `"none"` | `""` | (none) | Markers only |
1652
+
1653
+ ### Markers
1654
+
1655
+ #### Basic Shapes
1656
+
1657
+ | Name | Code | Symbol | Usage |
1658
+ |------|------|--------|-------|
1659
+ | `"circle"` | `"o"` | ○ | General purpose, most common |
1660
+ | `"square"` | `"s"` | □ | Grid data, regular samples |
1661
+ | `"diamond"` | `"D"` | ◇ | Special points, core data |
1662
+ | `"star"` | `"*"` | ★ | Important points |
1663
+ | `"plus"` | `"+"` | + | Crosshairs, reference points |
1664
+ | `"cross"` | `"x"` | × | Outliers, rejected points |
1665
+
1666
+ #### Triangles
1667
+
1668
+ | Name | Code | Symbol | Usage |
1669
+ |------|------|--------|-------|
1670
+ | `"triangle_up"` | `"^"` | △ | Increasing trend |
1671
+ | `"triangle_down"` | `"v"` | ▽ | Decreasing trend |
1672
+ | `"triangle_left"` | `"<"` | ◁ | Directional indicators |
1673
+ | `"triangle_right"` | `">"` | ▷ | Directional indicators |
1674
+
1675
+ #### Special
1676
+
1677
+ | Name | Code | Symbol | Usage |
1678
+ |------|------|--------|-------|
1679
+ | `"pentagon"` | `"p"` | ⬟ | Alternative shape |
1680
+ | `"hexagon"` | `"h"` | ⬢ | Honeycomb patterns |
1681
+ | `"point"` | `"."` | · | Dense data, minimal marker |
1682
+ | `"pixel"` | `","` | , | Very dense data |
1683
+ | `"vline"` | `"|"` | │ | Vertical emphasis |
1684
+ | `"hline"` | `"_"` | ─ | Horizontal emphasis |
1685
+
1686
+ ### Color Names
1687
+
1688
+ **Basic colors:** `"red"`, `"blue"`, `"green"`, `"yellow"`, `"orange"`, `"purple"`, `"pink"`, `"brown"`, `"gray"`, `"black"`, `"white"`
1689
+
1690
+ **Light colors:** `"lightblue"`, `"lightgreen"`, `"lightcoral"`, `"lightgray"`, `"lightyellow"`
1691
+
1692
+ **Dark colors:** `"darkblue"`, `"darkgreen"`, `"darkred"`, `"darkgray"`
1693
+
1694
+ **Advanced:** Use hex codes (`"#1f77b4"`) or RGB tuples (`(0.2, 0.5, 0.8)`) for precise colors.
1695
+
1696
+ ---
1697
+
1698
+ ## Colormap Reference
1699
+
1700
+ ### Sequential (Light to Dark)
1701
+
1702
+ Perfect for showing magnitude or intensity:
1703
+
1704
+ | Colormap | Description | Use Case |
1705
+ |----------|-------------|----------|
1706
+ | `"viridis"` | Yellow-green-blue (perceptually uniform) | **Recommended default** |
1707
+ | `"plasma"` | Purple-pink-yellow | High contrast |
1708
+ | `"inferno"` | Black-purple-yellow | Dark backgrounds |
1709
+ | `"magma"` | Black-purple-white | Maximum contrast |
1710
+ | `"cividis"` | Blue-yellow (colorblind-safe) | Accessibility |
1711
+
1712
+ ### Diverging (Low-Mid-High)
1713
+
1714
+ Perfect for data with a meaningful center (e.g., 0, neutral point):
1715
+
1716
+ | Colormap | Description | Use Case |
1717
+ |----------|-------------|----------|
1718
+ | `"RdYlGn"` | Red-Yellow-Green | Good/bad (e.g., quality) |
1719
+ | `"RdBu"` | Red-Blue | Hot/cold, positive/negative |
1720
+ | `"PiYG"` | Pink-Yellow-Green | Alternative diverging |
1721
+ | `"BrBG"` | Brown-Blue-Green | Earth tones |
1722
+
1723
+ ### Qualitative
1724
+
1725
+ For categorical data (use discrete tracks instead):
1726
+
1727
+ | Colormap | Description |
1728
+ |----------|-------------|
1729
+ | `"tab10"` | 10 distinct colors |
1730
+ | `"tab20"` | 20 distinct colors |
1731
+ | `"Paired"` | Paired colors |
1732
+
1733
+ ### Classic (Not Recommended)
1734
+
1735
+ | Colormap | Issue |
1736
+ |----------|-------|
1737
+ | `"jet"` | Not perceptually uniform, creates false boundaries |
1738
+ | `"rainbow"` | Similar issues to jet |
1739
+
1740
+ > **💡 Recommendation:** Use `"viridis"` for general purposes. Use `"RdYlGn"` for diverging data. Avoid `"jet"`.
1741
+
1742
+ ---
1743
+
1744
+ ## Advanced Topics
1745
+
1746
+ ### Formation Tops Setup
1747
+
1748
+ Formation tops create discrete zones that start at each top and extend to the next:
1749
+
1750
+ ```python
1751
+ import pandas as pd
1752
+
1753
+ # Create tops DataFrame
1754
+ tops_df = pd.DataFrame({
1755
+ 'Well': ['12/3-4 A', '12/3-4 A', '12/3-4 A'],
1756
+ 'Surface': ['Top_Brent', 'Top_Statfjord', 'Top_Cook'],
1757
+ 'MD': [2850.0, 3100.0, 3400.0]
1758
+ })
1759
+
1760
+ # Load tops
1761
+ manager.load_tops(
1762
+ tops_df,
1763
+ property_name='Zone', # Name for discrete property
1764
+ source_name='Tops', # Source name
1765
+ well_col='Well', # Column with well names
1766
+ discrete_col='Surface', # Column with formation names
1767
+ depth_col='MD' # Column with depths
1768
+ )
1769
+
1770
+ # How it works:
1771
+ # - Top_Brent applies from 2850m to 3100m
1772
+ # - Top_Statfjord applies from 3100m to 3400m
1773
+ # - Top_Cook applies from 3400m to bottom of log
1774
+ ```
1775
+
1776
+ ### Discrete Properties & Labels
1777
+
1778
+ ```python
1779
+ # Create or modify discrete property
1780
+ ntg = well.get_property('NTG_Flag')
1781
+ ntg.type = 'discrete'
1782
+ ntg.labels = {0: 'NonNet', 1: 'Net'}
1783
+
1784
+ # Use in filtering
1785
+ stats = well.PHIE.filter('NTG_Flag').sums_avg()
1786
+ # Returns: {'NonNet': {...}, 'Net': {...}}
1787
+
1788
+ # Add colors for visualization
1789
+ ntg.colors = {0: 'gray', 1: 'yellow'}
1790
+ ```
1791
+
1792
+ ### Understanding Statistics
1793
+
1794
+ Each statistics group provides comprehensive information:
1795
+
1796
+ ```python
1797
+ stats = well.PHIE.filter('Zone').sums_avg()
1798
+
1799
+ # Example output structure:
1800
+ {
1801
+ 'Top_Brent': {
1802
+ 'mean': 0.182, # Depth-weighted average
1803
+ 'sum': 45.5, # Sum (for flags: net thickness)
1804
+ 'std_dev': 0.044, # Standard deviation
1805
+ 'percentile': {
1806
+ 'p10': 0.09, # 10th percentile (pessimistic)
1807
+ 'p50': 0.18, # Median
1808
+ 'p90': 0.24 # 90th percentile (optimistic)
1809
+ },
1810
+ 'range': {'min': 0.05, 'max': 0.28},
1811
+ 'depth_range': {'min': 2850.0, 'max': 3100.0},
1812
+ 'samples': 250, # Number of valid measurements
1813
+ 'thickness': 250.0, # Interval thickness
1814
+ 'gross_thickness': 555.0, # Total across all zones
1815
+ 'thickness_fraction': 0.45, # Fraction of total
1816
+ 'calculation': 'weighted' # Method used
1817
+ }
1818
+ }
1819
+ ```
1820
+
1821
+ ### Export Options
1822
+
1823
+ **To DataFrame:**
1824
+ ```python
1825
+ # All properties (default: errors if depths don't match exactly)
1826
+ df = well.data()
1827
+
1828
+ # Specific properties
1829
+ df = well.data(include=['PHIE', 'SW', 'PERM'])
1830
+
1831
+ # Interpolate to common depth grid if depths don't align
1832
+ df = well.data(merge_method='resample')
1833
+
1834
+ # Use labels for discrete properties
1835
+ df = well.data(discrete_labels=True)
1836
+ ```
1837
+
1838
+ **To LAS:**
1839
+ ```python
1840
+ # Export all properties
1841
+ well.export_to_las('output.las')
1842
+
1843
+ # Specific properties
1844
+ well.export_to_las('output.las', include=['PHIE', 'SW'])
1845
+
1846
+ # Use original LAS as template (preserves headers)
1847
+ well.export_to_las('output.las', use_template=True)
1848
+
1849
+ # Export each source separately
1850
+ well.export_sources('output_folder/')
1851
+ # Creates: Petrophysics.las, CoreData.las, computed.las
1852
+ ```
1853
+
1854
+ ### Managing Sources
1855
+
1856
+ ```python
1857
+ # List sources
1858
+ print(well.sources) # ['Petrophysics', 'CoreData']
1859
+
1860
+ # Access through source
1861
+ phie_log = well.Petrophysics.PHIE
1862
+ phie_core = well.CoreData.CorePHIE
1863
+
1864
+ # Rename source
1865
+ well.rename_source('CoreData', 'Core_Analysis')
1866
+
1867
+ # Remove source (deletes all properties)
1868
+ well.remove_source('Core_Analysis')
1869
+ ```
1870
+
1871
+ ### Adding External Data
1872
+
1873
+ ```python
1874
+ import pandas as pd
1875
+
1876
+ # Create DataFrame
1877
+ external_df = pd.DataFrame({
1878
+ 'DEPT': [2800, 2801, 2802],
1879
+ 'CorePHIE': [0.20, 0.22, 0.19],
1880
+ 'CorePERM': [150, 200, 120]
1881
+ })
1882
+
1883
+ # Add to well
1884
+ well.add_dataframe(
1885
+ external_df,
1886
+ source_name='CoreData',
1887
+ unit_mappings={'CorePHIE': 'v/v', 'CorePERM': 'mD'},
1888
+ type_mappings={'CorePHIE': 'continuous', 'CorePERM': 'continuous'}
1889
+ )
1890
+ ```
1891
+
1892
+ ### Sampled Data (Core Plugs)
1893
+
1894
+ Core plugs are point samples requiring arithmetic (not depth-weighted) statistics:
1895
+
1896
+ ```python
1897
+ # Load as sampled
1898
+ manager.load_las('core_plugs.las', sampled=True)
1899
+
1900
+ # Or mark properties as sampled
1901
+ well.CorePHIE.type = 'sampled'
1902
+
1903
+ # Statistics use arithmetic mean
1904
+ stats = well.CorePHIE.filter('Zone').sums_avg()
1905
+ # → {'calculation': 'arithmetic'} (each plug counts equally)
1906
+ ```
1907
+
1908
+ ### Managing Wells
1909
+
1910
+ ```python
1911
+ # List wells
1912
+ print(manager.wells) # ['well_12_3_4_A', 'well_12_3_4_B']
1913
+
1914
+ # Access by name
1915
+ well = manager.well_12_3_4_A # Sanitized name (attribute)
1916
+ well = manager.get_well('12/3-4 A') # Original name
1917
+ well = manager.get_well('12_3_4_A') # Sanitized name
1918
+ well = manager.get_well('well_12_3_4_A') # With prefix
1919
+
1920
+ # Add well
1921
+ well = manager.add_well('12/3-4 C')
1922
+
1923
+ # Remove well
1924
+ manager.remove_well('12_3_4_A')
1925
+ ```
1926
+
1927
+ ### Property Inspection
1928
+
1929
+ ```python
1930
+ # Print property (auto-clips large arrays)
1931
+ print(well.PHIE)
1932
+ # [PHIE] (1001 samples)
1933
+ # depth: [2800.00, 2801.00, ..., 3800.00]
1934
+ # values (v/v): [0.180, 0.185, ..., 0.210]
1935
+
1936
+ # Print filtered property
1937
+ filtered = well.PHIE.filter('Zone')
1938
+ print(filtered)
1939
+ # [PHIE] (1001 samples)
1940
+ # Filters: Zone: [Top_Brent, Top_Brent, ...]
1941
+
1942
+ # Print manager-level property
1943
+ print(manager.PHIE)
1944
+ # [PHIE] across 3 well(s):
1945
+ # Well: well_12_3_4_A
1946
+ # [PHIE] (1001 samples)
1947
+ # ...
1948
+ ```
1949
+
1950
+ ---
1951
+
1952
+ ## API Reference
1953
+
1954
+ ### Main Classes
1955
+
1956
+ ```python
1957
+ from logsuite import WellDataManager, Well, Property, LasFile
1958
+ ```
1959
+
1960
+ **WellDataManager** - Manages multiple wells
1961
+ - `load_las(filepath, sampled=False)` - Load LAS file
1962
+ - `load_tops(df, well_col, discrete_col, depth_col)` - Load formation tops
1963
+ - `add_well(name)` - Add empty well
1964
+ - `get_well(name)` - Get well by name
1965
+ - `remove_well(name)` - Remove well
1966
+ - `save(directory)` - Save project
1967
+ - `load(directory)` - Load project
1968
+ - `add_template(template)` - Store template (uses template.name)
1969
+ - `set_template(name, template)` - Store template with custom name
1970
+ - `get_template(name)` - Retrieve template
1971
+ - `list_templates()` - List template names
1972
+ - `Crossplot(x, y, wells=None, shape="well", ...)` - Create multi-well crossplot
1973
+
1974
+ **Well** - Individual wellbore
1975
+ - `get_property(name, source=None)` - Get property
1976
+ - `add_dataframe(df, source_name, ...)` - Add external data
1977
+ - `data(include=None, exclude=None)` - Export to DataFrame
1978
+ - `export_to_las(filepath, ...)` - Export to LAS
1979
+ - `export_sources(directory)` - Export each source
1980
+ - `rename_source(old, new)` - Rename source
1981
+ - `remove_source(name)` - Remove source
1982
+ - `WellView(depth_range=None, tops=None, template, ...)` - Create log visualization
1983
+ - `Crossplot(x, y, color=None, size=None, shape=None, ...)` - Create crossplot
1984
+
1985
+ **Property** - Single measurement or computed value
1986
+ - `filter(discrete_property)` - Filter by discrete property
1987
+ - `sums_avg(arithmetic=False)` - Compute statistics
1988
+ - `resample(reference_property)` - Resample to new depth grid
1989
+ - Attributes: `name`, `depth`, `values`, `unit`, `type`, `labels`, `colors`
1990
+
1991
+ ### Visualization Classes
1992
+
1993
+ ```python
1994
+ from logsuite import Template, WellView, Crossplot
1995
+ ```
1996
+
1997
+ **Template** - Display layout configuration
1998
+ - `add_track(track_type, logs, fill, tops, ...)` - Add track
1999
+ - `add_tops(property_name, tops_dict, ...)` - Add formation tops
2000
+ - `edit_track(index, **kwargs)` - Edit track
2001
+ - `remove_track(index)` - Remove track
2002
+ - `get_track(index)` - Get track config
2003
+ - `list_tracks()` - List all tracks
2004
+ - `save(filepath)` - Save to JSON
2005
+ - `load(filepath)` - Load from JSON (classmethod)
2006
+ - `to_dict()`, `from_dict(data)` - Dict conversion
2007
+
2008
+ **WellView** - Well log display
2009
+ - `plot()` - Create matplotlib figure
2010
+ - `show()` - Display in Jupyter
2011
+ - `save(filepath, dpi)` - Save to file
2012
+ - `close()` - Close figure
2013
+ - `add_track(...)` - Add temporary track
2014
+ - `add_tops(...)` - Add temporary tops
2015
+
2016
+ **Crossplot** - Scatter plot with regression analysis
2017
+ - `plot()` - Create matplotlib figure
2018
+ - `show()` - Display plot
2019
+ - `save(filepath, dpi)` - Save to file
2020
+ - `close()` - Close figure
2021
+ - `add_regression(type, **kwargs)` - Add regression line
2022
+ - `remove_regression(name)` - Remove regression
2023
+ - Attributes: `regressions`, `fig`, `ax`
2024
+
2025
+ ### Regression Classes
2026
+
2027
+ ```python
2028
+ from logsuite import (
2029
+ LinearRegression,
2030
+ PolynomialRegression,
2031
+ ExponentialRegression,
2032
+ LogarithmicRegression,
2033
+ PowerRegression
2034
+ )
2035
+ ```
2036
+
2037
+ All regression classes share the same interface:
2038
+
2039
+ - `fit(x, y)` - Fit regression model to data
2040
+ - `predict(x)` - Predict y values for given x
2041
+ - `__call__(x)` - Alternative prediction syntax: `reg([1, 2, 3])`
2042
+ - `equation()` - Get equation string (e.g., "y = 2.5x + 1.3")
2043
+ - Attributes: `r_squared`, `rmse`, `x_data`, `y_data`
2044
+
2045
+ **PolynomialRegression** - Additional parameter:
2046
+ - `__init__(degree=2)` - Specify polynomial degree
2047
+
2048
+ ### Statistics Functions
2049
+
2050
+ ```python
2051
+ from logsuite import compute_intervals, mean, sum, std, percentile
2052
+ ```
2053
+
2054
+ These are low-level functions used internally. Most users should use the high-level filtering API (`property.filter().sums_avg()`).
2055
+
2056
+ ### Exceptions
2057
+
2058
+ ```python
2059
+ from logsuite import (
2060
+ DepthAlignmentError,
2061
+ PropertyNotFoundError,
2062
+ PropertyTypeError
2063
+ )
2064
+ ```
2065
+
2066
+ - `DepthAlignmentError` - Raised when combining properties with different depth grids
2067
+ - `PropertyNotFoundError` - Raised when accessing non-existent property
2068
+ - `PropertyTypeError` - Raised when property has wrong type (e.g., filtering by continuous property)
2069
+
2070
+ ---
2071
+
2072
+ ## Common Patterns
2073
+
2074
+ Copy-paste examples for common tasks:
2075
+
2076
+ ### Load and Analyze
2077
+
2078
+ ```python
2079
+ manager = WellDataManager()
2080
+ manager.load_las('well.las')
2081
+ stats = manager.well_12_3_4_A.PHIE.filter('Zone').sums_avg()
2082
+ ```
2083
+
2084
+ ### Chain Multiple Filters
2085
+
2086
+ ```python
2087
+ stats = well.PHIE.filter('Zone').filter('Facies').filter('NTG_Flag').sums_avg()
2088
+ ```
2089
+
2090
+ ### Multi-Well Statistics
2091
+
2092
+ ```python
2093
+ # P50 by zone across all wells
2094
+ p50 = manager.PHIE.filter('Zone').percentile(50)
2095
+
2096
+ # All statistics
2097
+ means = manager.PHIE.filter('Zone').mean()
2098
+ stds = manager.PHIE.filter('Zone').std()
2099
+ ```
2100
+
2101
+ ### Create Computed Properties
2102
+
2103
+ ```python
2104
+ well.HC_Volume = well.PHIE * (1 - well.SW)
2105
+ well.Reservoir = (well.PHIE > 0.15) & (well.SW < 0.35)
2106
+ ```
2107
+
2108
+ ### Broadcast Across Wells
2109
+
2110
+ ```python
2111
+ manager.PHIE_percent = manager.PHIE * 100
2112
+ manager.Reservoir = (manager.PHIE > 0.15) & (manager.SW < 0.35)
2113
+ ```
2114
+
2115
+ ### Quick Visualization
2116
+
2117
+ ```python
2118
+ # With depth range
2119
+ view = well.WellView(depth_range=[2800, 3000])
2120
+ view.show()
2121
+
2122
+ # Auto-calculate from tops
2123
+ view = well.WellView(tops=['Top_Brent', 'Top_Statfjord'])
2124
+ view.show()
2125
+ ```
2126
+
2127
+ ### Build Custom Template
2128
+
2129
+ ```python
2130
+ template = Template("custom")
2131
+ template.add_track(
2132
+ track_type="continuous",
2133
+ logs=[{"name": "GR", "x_range": [0, 150], "color": "green"}],
2134
+ title="Gamma Ray"
2135
+ )
2136
+ manager.add_template(template) # Stored as "custom"
2137
+ view = well.WellView(template="custom")
2138
+ view.save("log.png", dpi=300)
2139
+ ```
2140
+
2141
+ ### Crossplots
2142
+
2143
+ ```python
2144
+ # Simple crossplot
2145
+ plot = well.Crossplot(x="RHOB", y="NPHI")
2146
+ plot.show()
2147
+
2148
+ # With color and regression
2149
+ plot = well.Crossplot(x="PHIE", y="SW", color="depth")
2150
+ plot.add_regression("linear", line_color="red")
2151
+ plot.show()
2152
+
2153
+ # Multi-well
2154
+ plot = manager.Crossplot(x="PHIE", y="SW", shape="well")
2155
+ plot.show()
2156
+ ```
2157
+
2158
+ ### Regression Analysis
2159
+
2160
+ ```python
2161
+ # With crossplot
2162
+ plot = well.Crossplot(x="RHOB", y="NPHI")
2163
+ plot.add_regression("linear")
2164
+ reg = plot.regressions["linear"]
2165
+ predictions = reg([2.3, 2.4, 2.5])
2166
+
2167
+ # Standalone
2168
+ from logsuite import LinearRegression
2169
+ reg = LinearRegression()
2170
+ reg.fit(x_data, y_data)
2171
+ print(reg.equation())
2172
+ y_pred = reg(new_x_values)
2173
+ ```
2174
+
2175
+ ### Save and Load Projects
2176
+
2177
+ ```python
2178
+ manager.save('project/')
2179
+ manager = WellDataManager('project/')
2180
+ ```
2181
+
2182
+ ---
2183
+
2184
+ ## Troubleshooting
2185
+
2186
+ ### DepthAlignmentError
2187
+
2188
+ **Problem:** Properties have different depth grids
2189
+
2190
+ ```python
2191
+ result = well.PHIE + well.CorePHIE # Error!
2192
+ ```
2193
+
2194
+ **Solution:** Explicitly resample
2195
+
2196
+ ```python
2197
+ core_resampled = well.CorePHIE.resample(well.PHIE)
2198
+ result = well.PHIE + core_resampled # Works!
2199
+ ```
2200
+
2201
+ ### PropertyNotFoundError
2202
+
2203
+ **Problem:** Property doesn't exist
2204
+
2205
+ ```python
2206
+ phie = well.PHIE_TOTAL # Error if property doesn't exist
2207
+ ```
2208
+
2209
+ **Solution:** Check available properties
2210
+
2211
+ ```python
2212
+ print(well.properties) # List all
2213
+ print(well.sources) # Check sources
2214
+
2215
+ # Or handle gracefully
2216
+ try:
2217
+ phie = well.get_property('PHIE_TOTAL')
2218
+ except PropertyNotFoundError:
2219
+ phie = well.PHIE # Use fallback
2220
+ ```
2221
+
2222
+ ### PropertyTypeError
2223
+
2224
+ **Problem:** Filtering by non-discrete property
2225
+
2226
+ ```python
2227
+ stats = well.PHIE.filter('PERM').sums_avg() # Error!
2228
+ ```
2229
+
2230
+ **Solution:** Mark as discrete
2231
+
2232
+ ```python
2233
+ perm = well.get_property('PERM')
2234
+ perm.type = 'discrete'
2235
+ perm.labels = {0: 'Low', 1: 'Medium', 2: 'High'}
2236
+ stats = well.PHIE.filter('PERM').sums_avg() # Works!
2237
+ ```
2238
+
2239
+ ### Missing Statistics for Some Zones
2240
+
2241
+ **Problem:** No valid data in some zones
2242
+
2243
+ ```python
2244
+ stats = well.PHIE.filter('Zone').sums_avg()
2245
+ # Some zones missing if all PHIE values are NaN
2246
+ ```
2247
+
2248
+ **Solution:** Check raw data
2249
+
2250
+ ```python
2251
+ print(well.PHIE.values) # Look for NaN
2252
+ print(well.Zone.values) # Check distribution
2253
+
2254
+ # Filter NaN values
2255
+ import numpy as np
2256
+ valid_mask = ~np.isnan(well.PHIE.values)
2257
+ ```
2258
+
2259
+ ### Template Not Found
2260
+
2261
+ **Problem:** Template doesn't exist
2262
+
2263
+ ```python
2264
+ view = well.WellView(template="missing") # Error!
2265
+ ```
2266
+
2267
+ **Solution:** Check available templates
2268
+
2269
+ ```python
2270
+ print(manager.list_templates()) # ['reservoir', 'qc']
2271
+
2272
+ # Or pass template directly
2273
+ template = Template("custom")
2274
+ view = well.WellView(template=template)
2275
+ ```
2276
+
2277
+ ### Visualization Not Showing
2278
+
2279
+ **Problem:** Display doesn't appear in Jupyter
2280
+
2281
+ ```python
2282
+ view = well.WellView(template="reservoir")
2283
+ # Nothing shows
2284
+ ```
2285
+
2286
+ **Solution:** Call show() explicitly
2287
+
2288
+ ```python
2289
+ view = well.WellView(template="reservoir")
2290
+ view.show() # Required in Jupyter
2291
+ ```
2292
+
2293
+ ### Markers Not Appearing
2294
+
2295
+ **Problem:** Markers not visible in log display
2296
+
2297
+ ```python
2298
+ logs=[{"name": "GR", "marker": "circle"}]
2299
+ # No markers show
2300
+ ```
2301
+
2302
+ **Solution:** Check marker configuration
2303
+
2304
+ ```python
2305
+ # Ensure marker size is visible
2306
+ logs=[{"name": "GR", "marker": "circle", "marker_size": 6}]
2307
+
2308
+ # If line is very thick, markers might be hidden
2309
+ logs=[{
2310
+ "name": "GR",
2311
+ "marker": "circle",
2312
+ "marker_size": 8, # Larger markers
2313
+ "marker_outline_color": "red", # Distinct color
2314
+ "marker_fill": "yellow" # Filled markers stand out
2315
+ }]
2316
+
2317
+ # For markers only, use style="none"
2318
+ logs=[{
2319
+ "name": "CORE_PHIE",
2320
+ "style": "none", # Remove line
2321
+ "marker": "diamond",
2322
+ "marker_size": 10
2323
+ }]
2324
+ ```
2325
+
2326
+ ### Tops Parameter Error
2327
+
2328
+ **Problem:** No formation tops loaded
2329
+
2330
+ ```python
2331
+ view = well.WellView(tops=['Top_Brent', 'Top_Statfjord'])
2332
+ # ValueError: No formation tops have been loaded
2333
+ ```
2334
+
2335
+ **Solution:** Add tops to template or view first
2336
+
2337
+ ```python
2338
+ # Option 1: Add tops to template
2339
+ template = Template("reservoir")
2340
+ template.add_tops(property_name='Zone')
2341
+ view = well.WellView(tops=['Top_Brent', 'Top_Statfjord'], template=template)
2342
+
2343
+ # Option 2: Add tops to view
2344
+ view = well.WellView(template=template)
2345
+ view.add_tops(property_name='Zone')
2346
+ # Note: Can't use tops parameter if tops aren't in template
2347
+
2348
+ # Option 3: Use depth_range instead
2349
+ view = well.WellView(depth_range=[2800, 3000], template=template)
2350
+ ```
2351
+
2352
+ **Problem:** Specified tops not found
2353
+
2354
+ ```python
2355
+ view = well.WellView(tops=['Top_Missing'])
2356
+ # ValueError: Formation tops not found: ['Top_Missing']
2357
+ ```
2358
+
2359
+ **Solution:** Check available tops
2360
+
2361
+ ```python
2362
+ # Load and check tops
2363
+ manager.load_tops(tops_df, well_col='Well', discrete_col='Surface', depth_col='MD')
2364
+
2365
+ # Check what tops are available
2366
+ zone = well.get_property('Zone')
2367
+ print(zone.labels) # {0: 'Top_Brent', 1: 'Top_Statfjord', ...}
2368
+
2369
+ # Use correct names
2370
+ view = well.WellView(tops=['Top_Brent', 'Top_Statfjord'], template=template)
2371
+ ```
2372
+
2373
+ ---
2374
+
2375
+ ## Performance
2376
+
2377
+ All operations use **vectorized numpy** for maximum speed:
2378
+
2379
+ - **100M+ samples/second** throughput
2380
+ - Typical well logs (1k-10k samples) process in **< 1ms**
2381
+ - Filtered statistics (2 filters, 10 wells): **~9ms**
2382
+ - Manager-level operations optimized with property caching
2383
+ - I/O bottleneck eliminated with lazy loading
2384
+
2385
+ ---
2386
+
2387
+ ## Requirements
2388
+
2389
+ - Python >= 3.9
2390
+ - numpy >= 1.20.0
2391
+ - pandas >= 1.3.0
2392
+ - scipy >= 1.7.0
2393
+ - matplotlib >= 3.5.0
2394
+
2395
+ ---
2396
+
2397
+ ## Contributing
2398
+
2399
+ Contributions welcome! Please submit a Pull Request.
2400
+
2401
+ ---
2402
+
2403
+ ## License
2404
+
2405
+ MIT License
2406
+
2407
+ ---
2408
+
2409
+ ## Need Help?
2410
+
2411
+ - **Issues:** [GitHub Issues](https://github.com/kkollsga/logsuite/issues)
2412
+ - **Changelog:** [CHANGELOG.md](CHANGELOG.md)
2413
+ - **Documentation:** See sections above