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.
- logsuite-0.2.0/PKG-INFO +2413 -0
- logsuite-0.2.0/README.md +2369 -0
- logsuite-0.2.0/logsuite/__init__.py +123 -0
- logsuite-0.2.0/logsuite/_version.py +23 -0
- logsuite-0.2.0/logsuite/analysis/__init__.py +54 -0
- logsuite-0.2.0/logsuite/analysis/regression.py +950 -0
- logsuite-0.2.0/logsuite/analysis/statistics.py +687 -0
- logsuite-0.2.0/logsuite/analysis/sums_avg.py +648 -0
- logsuite-0.2.0/logsuite/core/__init__.py +20 -0
- logsuite-0.2.0/logsuite/core/operations.py +492 -0
- logsuite-0.2.0/logsuite/core/property.py +2836 -0
- logsuite-0.2.0/logsuite/core/well.py +2718 -0
- logsuite-0.2.0/logsuite/exceptions.py +57 -0
- logsuite-0.2.0/logsuite/io/__init__.py +5 -0
- logsuite-0.2.0/logsuite/io/las_file.py +1378 -0
- logsuite-0.2.0/logsuite/manager/__init__.py +17 -0
- logsuite-0.2.0/logsuite/manager/data_manager.py +1648 -0
- logsuite-0.2.0/logsuite/manager/proxy.py +2048 -0
- logsuite-0.2.0/logsuite/utils.py +252 -0
- logsuite-0.2.0/logsuite/visualization/__init__.py +223 -0
- logsuite-0.2.0/logsuite/visualization/crossplot.py +2277 -0
- logsuite-0.2.0/logsuite/visualization/template.py +612 -0
- logsuite-0.2.0/logsuite/visualization/wellview.py +2180 -0
- logsuite-0.2.0/logsuite.egg-info/PKG-INFO +2413 -0
- logsuite-0.2.0/logsuite.egg-info/SOURCES.txt +67 -0
- logsuite-0.2.0/logsuite.egg-info/dependency_links.txt +1 -0
- logsuite-0.2.0/logsuite.egg-info/requires.txt +19 -0
- logsuite-0.2.0/logsuite.egg-info/top_level.txt +1 -0
- logsuite-0.2.0/pyproject.toml +153 -0
- logsuite-0.2.0/setup.cfg +4 -0
- logsuite-0.2.0/tests/test_crossplot.py +142 -0
- logsuite-0.2.0/tests/test_crossplot_extensive.py +762 -0
- logsuite-0.2.0/tests/test_crossplot_integration.py +603 -0
- logsuite-0.2.0/tests/test_crossplot_well_colors.py +285 -0
- logsuite-0.2.0/tests/test_discrete_labels_in_legend.py +213 -0
- logsuite-0.2.0/tests/test_discrete_regression_by_color.py +227 -0
- logsuite-0.2.0/tests/test_discrete_shape_color.py +331 -0
- logsuite-0.2.0/tests/test_error_handling.py +218 -0
- logsuite-0.2.0/tests/test_extended_statistics.py +176 -0
- logsuite-0.2.0/tests/test_filename_fix.py +224 -0
- logsuite-0.2.0/tests/test_filtered_comprehensive.py +226 -0
- logsuite-0.2.0/tests/test_flexible_loading.py +198 -0
- logsuite-0.2.0/tests/test_grouped_legends.py +319 -0
- logsuite-0.2.0/tests/test_hyphen_fix.py +224 -0
- logsuite-0.2.0/tests/test_las3_support.py +50 -0
- logsuite-0.2.0/tests/test_las_export_roundtrip.py +97 -0
- logsuite-0.2.0/tests/test_lint.py +34 -0
- logsuite-0.2.0/tests/test_log_scale_labels.py +139 -0
- logsuite-0.2.0/tests/test_md_well_standardization.py +224 -0
- logsuite-0.2.0/tests/test_naming_refactor.py +217 -0
- logsuite-0.2.0/tests/test_new_features.py +263 -0
- logsuite-0.2.0/tests/test_optimized_legend_placement.py +323 -0
- logsuite-0.2.0/tests/test_overwrite.py +177 -0
- logsuite-0.2.0/tests/test_param_locking.py +118 -0
- logsuite-0.2.0/tests/test_performance.py +205 -0
- logsuite-0.2.0/tests/test_polynomial_exponential_regression.py +289 -0
- logsuite-0.2.0/tests/test_printing.py +327 -0
- logsuite-0.2.0/tests/test_project_export.py +184 -0
- logsuite-0.2.0/tests/test_property_operations.py +497 -0
- logsuite-0.2.0/tests/test_regression_alias.py +245 -0
- logsuite-0.2.0/tests/test_regression_by_color_and_shape.py +307 -0
- logsuite-0.2.0/tests/test_regression_degree_suffixes.py +303 -0
- logsuite-0.2.0/tests/test_regression_title_with_type.py +170 -0
- logsuite-0.2.0/tests/test_resample_edge_cases.py +94 -0
- logsuite-0.2.0/tests/test_save_load.py +241 -0
- logsuite-0.2.0/tests/test_source_aware.py +189 -0
- logsuite-0.2.0/tests/test_statistics.py +383 -0
- logsuite-0.2.0/tests/test_sums_avg_report.py +123 -0
- logsuite-0.2.0/tests/test_thickness_accuracy.py +1198 -0
logsuite-0.2.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/logsuite/)
|
|
50
|
+
[](https://www.python.org/downloads/)
|
|
51
|
+
[](https://github.com/kkollsga/logsuite/actions)
|
|
52
|
+
[](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
|