well-log-toolkit 0.1.79__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- well_log_toolkit-0.1.79/PKG-INFO +1291 -0
- well_log_toolkit-0.1.79/README.md +1253 -0
- well_log_toolkit-0.1.79/pyproject.toml +108 -0
- well_log_toolkit-0.1.79/setup.cfg +4 -0
- well_log_toolkit-0.1.79/well_log_toolkit/__init__.py +125 -0
- well_log_toolkit-0.1.79/well_log_toolkit/exceptions.py +48 -0
- well_log_toolkit-0.1.79/well_log_toolkit/las_file.py +988 -0
- well_log_toolkit-0.1.79/well_log_toolkit/manager.py +2368 -0
- well_log_toolkit-0.1.79/well_log_toolkit/operations.py +494 -0
- well_log_toolkit-0.1.79/well_log_toolkit/property.py +1858 -0
- well_log_toolkit-0.1.79/well_log_toolkit/statistics.py +513 -0
- well_log_toolkit-0.1.79/well_log_toolkit/utils.py +225 -0
- well_log_toolkit-0.1.79/well_log_toolkit/visualization.py +1756 -0
- well_log_toolkit-0.1.79/well_log_toolkit/well.py +2125 -0
- well_log_toolkit-0.1.79/well_log_toolkit.egg-info/PKG-INFO +1291 -0
- well_log_toolkit-0.1.79/well_log_toolkit.egg-info/SOURCES.txt +17 -0
- well_log_toolkit-0.1.79/well_log_toolkit.egg-info/dependency_links.txt +1 -0
- well_log_toolkit-0.1.79/well_log_toolkit.egg-info/requires.txt +11 -0
- well_log_toolkit-0.1.79/well_log_toolkit.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,1291 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: well-log-toolkit
|
|
3
|
+
Version: 0.1.79
|
|
4
|
+
Summary: Fast LAS file processing with lazy loading and filtering for well log analysis
|
|
5
|
+
Author-email: Kristian dF Kollsgård <kkollsg@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kkollsga/well_log_toolkit
|
|
8
|
+
Project-URL: Documentation, https://github.com/kkollsga/well_log_toolkit#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/kkollsga/well_log_toolkit
|
|
10
|
+
Project-URL: Issues, https://github.com/kkollsga/well_log_toolkit/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/kkollsga/well_log_toolkit/releases
|
|
12
|
+
Keywords: well-log,las-file,petrophysics,geoscience,oil-and-gas,petroleum-engineering
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
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.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Operating System :: OS Independent
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
Requires-Dist: numpy>=1.20.0
|
|
29
|
+
Requires-Dist: pandas>=1.3.0
|
|
30
|
+
Requires-Dist: scipy>=1.7.0
|
|
31
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
37
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
38
|
+
|
|
39
|
+
# Well Log Toolkit
|
|
40
|
+
|
|
41
|
+
Fast, intuitive Python library for petrophysical well log analysis. Load LAS files, filter by zones, and compute depth-weighted statistics in just a few lines.
|
|
42
|
+
|
|
43
|
+
## Key Features
|
|
44
|
+
|
|
45
|
+
- **Lazy Loading** - Parse headers instantly, load data on demand
|
|
46
|
+
- **Numpy-Style Operations** - `well.HC_Volume = well.PHIE * (1 - well.SW)`
|
|
47
|
+
- **Hierarchical Filtering** - Chain filters: `well.PHIE.filter('Zone').filter('Facies').sums_avg()`
|
|
48
|
+
- **Depth-Weighted Statistics** - Proper averaging for irregular sampling
|
|
49
|
+
- **Multi-Well Statistics** - Cross-well analytics: `manager.PHIE.filter('Zone').percentile(50)`
|
|
50
|
+
- **Multi-Well Management** - Broadcast operations: `manager.PHIE_percent = manager.PHIE * 100`
|
|
51
|
+
- **Well Log Visualization** - Create publication-quality log displays in Jupyter Lab
|
|
52
|
+
- **Project Persistence** - Save/load entire projects with metadata and templates
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install well-log-toolkit
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Table of Contents
|
|
61
|
+
|
|
62
|
+
- [1-Minute Tutorial](#1-minute-tutorial) - Get started immediately
|
|
63
|
+
- [Quick Start](#quick-start) - Core workflow in 5 minutes
|
|
64
|
+
- [Core Concepts](#core-concepts) - Essential patterns
|
|
65
|
+
- [Visualization](#visualization) - Create well log displays
|
|
66
|
+
- [Full Documentation](#documentation) - Complete guides
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 1-Minute Tutorial
|
|
71
|
+
|
|
72
|
+
Load LAS files, filter by zones, and compute statistics:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from well_log_toolkit import WellDataManager
|
|
76
|
+
|
|
77
|
+
# Load and analyze
|
|
78
|
+
manager = WellDataManager()
|
|
79
|
+
manager.load_las('well.las')
|
|
80
|
+
|
|
81
|
+
well = manager.well_12_3_4_A
|
|
82
|
+
stats = well.PHIE.filter('Zone').sums_avg()
|
|
83
|
+
|
|
84
|
+
print(stats['Top_Brent']['mean']) # → 0.182 (depth-weighted)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**That's it!** Three lines to go from LAS file to zonal statistics.
|
|
88
|
+
|
|
89
|
+
> **New to this?** Continue to [Quick Start](#quick-start) for a complete 5-minute walkthrough.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Quick Start
|
|
94
|
+
|
|
95
|
+
### 1. Load Data
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from well_log_toolkit import WellDataManager
|
|
99
|
+
import pandas as pd
|
|
100
|
+
|
|
101
|
+
# Load LAS files
|
|
102
|
+
manager = WellDataManager()
|
|
103
|
+
manager.load_las('well_A.las')
|
|
104
|
+
manager.load_las('well_B.las')
|
|
105
|
+
|
|
106
|
+
# Load formation tops from DataFrame
|
|
107
|
+
tops_df = pd.DataFrame({
|
|
108
|
+
'Well': ['12/3-4 A', '12/3-4 A', '12/3-4 B'],
|
|
109
|
+
'Surface': ['Top_Brent', 'Top_Statfjord', 'Top_Brent'],
|
|
110
|
+
'MD': [2850.0, 3100.0, 2900.0]
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
manager.load_tops(tops_df, well_col='Well', discrete_col='Surface', depth_col='MD')
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 2. Access Wells and Properties
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
# Access well (special characters auto-sanitized)
|
|
120
|
+
well = manager.well_12_3_4_A
|
|
121
|
+
|
|
122
|
+
# Access properties directly
|
|
123
|
+
phie = well.PHIE
|
|
124
|
+
sw = well.SW
|
|
125
|
+
|
|
126
|
+
# List everything
|
|
127
|
+
print(well.properties) # ['PHIE', 'SW', 'PERM', 'Zone', ...]
|
|
128
|
+
print(well.sources) # ['Petrophysics', 'Imported_Tops']
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 3. Compute Statistics with Filtering
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# Single filter - group by Zone
|
|
135
|
+
stats = well.PHIE.filter('Zone').sums_avg()
|
|
136
|
+
# → {'Top_Brent': {'mean': 0.182, 'thickness': 250.0, ...}, 'Top_Statfjord': {...}}
|
|
137
|
+
|
|
138
|
+
# Chain filters - hierarchical grouping
|
|
139
|
+
stats = well.PHIE.filter('Zone').filter('Facies').sums_avg()
|
|
140
|
+
# → {'Top_Brent': {'Sandstone': {...}, 'Shale': {...}}, 'Top_Statfjord': {...}}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> **💡 Key:** Statistics are **depth-weighted** by default, accounting for irregular sampling. Add `arithmetic=True` to compare methods.
|
|
144
|
+
|
|
145
|
+
### 4. Create New Properties
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# Mathematical expressions (numpy-style)
|
|
149
|
+
well.HC_Volume = well.PHIE * (1 - well.SW)
|
|
150
|
+
well.PHIE_percent = well.PHIE * 100
|
|
151
|
+
|
|
152
|
+
# Comparison operations (auto-creates discrete flags)
|
|
153
|
+
well.Reservoir = (well.PHIE > 0.15) & (well.SW < 0.35)
|
|
154
|
+
|
|
155
|
+
# Apply to all wells at once
|
|
156
|
+
manager.PHIE_percent = manager.PHIE * 100
|
|
157
|
+
# → Converts PHIE to percent in every well
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
> **⚠️ Important:** Operations require matching depth grids (like numpy). Use `.resample()` to align different grids.
|
|
161
|
+
|
|
162
|
+
### 5. Export Results
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# Export to DataFrame
|
|
166
|
+
df = well.data(include=['PHIE', 'SW', 'HC_Volume'])
|
|
167
|
+
|
|
168
|
+
# Export to LAS file
|
|
169
|
+
well.export_to_las('output.las')
|
|
170
|
+
|
|
171
|
+
# Save entire project
|
|
172
|
+
manager.save('my_project/')
|
|
173
|
+
|
|
174
|
+
# Load project later
|
|
175
|
+
manager = WellDataManager('my_project/')
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Done!** You've learned the core workflow in 5 minutes.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Core Concepts
|
|
183
|
+
|
|
184
|
+
### Hierarchical Filtering
|
|
185
|
+
|
|
186
|
+
Filter properties by discrete logs (zones, facies, flags) to compute grouped statistics:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# Add labels for readable output
|
|
190
|
+
ntg_flag = well.get_property('NTG_Flag')
|
|
191
|
+
ntg_flag.type = 'discrete'
|
|
192
|
+
ntg_flag.labels = {0: 'NonNet', 1: 'Net'}
|
|
193
|
+
|
|
194
|
+
# Chain filters for hierarchical grouping
|
|
195
|
+
stats = well.PHIE.filter('Zone').filter('NTG_Flag').sums_avg()
|
|
196
|
+
# {
|
|
197
|
+
# 'Top_Brent': {
|
|
198
|
+
# 'Net': {'mean': 0.21, 'thickness': 150.0, 'samples': 150},
|
|
199
|
+
# 'NonNet': {'mean': 0.08, 'thickness': 100.0, 'samples': 100}
|
|
200
|
+
# },
|
|
201
|
+
# 'Top_Statfjord': {...}
|
|
202
|
+
# }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Each statistics group includes:**
|
|
206
|
+
- `mean`, `sum`, `std_dev` - Depth-weighted by default
|
|
207
|
+
- `percentile` - p10, p50, p90 values
|
|
208
|
+
- `thickness` - Depth interval for this group
|
|
209
|
+
- `samples` - Number of valid measurements
|
|
210
|
+
- `range`, `depth_range` - Min/max values and depths
|
|
211
|
+
|
|
212
|
+
### Property Operations
|
|
213
|
+
|
|
214
|
+
Create computed properties using natural mathematical syntax:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
# Arithmetic (strict depth matching like numpy)
|
|
218
|
+
well.HC_Volume = well.PHIE * (1 - well.SW)
|
|
219
|
+
well.Porosity_Avg = (well.PHIE + well.PHIT) / 2
|
|
220
|
+
|
|
221
|
+
# Comparisons (auto-creates discrete properties)
|
|
222
|
+
well.High_Poro = well.PHIE > 0.15
|
|
223
|
+
well.Reservoir = (well.PHIE > 0.15) & (well.SW < 0.35)
|
|
224
|
+
|
|
225
|
+
# Use computed properties in filtering
|
|
226
|
+
stats = well.PHIE.filter('Reservoir').sums_avg()
|
|
227
|
+
# → {'False': {...}, 'True': {...}}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
> **💡 Pro Tip:** Computed properties are stored in a special `'computed'` source and can be exported to LAS files.
|
|
231
|
+
|
|
232
|
+
### Depth Alignment
|
|
233
|
+
|
|
234
|
+
Operations fail if depth grids don't match (prevents silent interpolation errors):
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
# This fails if depths don't match
|
|
238
|
+
result = well.PHIE + well.CorePHIE # DepthAlignmentError
|
|
239
|
+
|
|
240
|
+
# Explicit resampling required
|
|
241
|
+
core_resampled = well.CorePHIE.resample(well.PHIE)
|
|
242
|
+
result = well.PHIE + core_resampled # ✓ Works
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Manager-Level Statistics
|
|
246
|
+
|
|
247
|
+
Compute statistics across all wells in a single call:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
# Single statistic across all wells
|
|
251
|
+
p50 = manager.PHIE.percentile(50)
|
|
252
|
+
# → {'well_A': 0.182, 'well_B': 0.195, 'well_C': 0.173}
|
|
253
|
+
|
|
254
|
+
# With filtering - grouped by filter values per well
|
|
255
|
+
stats = manager.PHIE.filter('Zone').percentile(50)
|
|
256
|
+
# → {'well_A': {'Top_Brent': 0.21, 'Top_Statfjord': 0.15},
|
|
257
|
+
# 'well_B': {'Top_Brent': 0.19, 'Top_Statfjord': 0.17}}
|
|
258
|
+
|
|
259
|
+
# Chain filters for hierarchical grouping
|
|
260
|
+
stats = manager.PHIE.filter('Zone').filter('Facies').mean()
|
|
261
|
+
|
|
262
|
+
# All statistics available: min, max, mean, median, std, percentile
|
|
263
|
+
stats = manager.PHIE.filter('Zone').min()
|
|
264
|
+
stats = manager.PHIE.filter('Zone').max()
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Ambiguous properties** (existing in multiple sources like log + core) automatically nest by source:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
# If well_A has PHIE in both 'log' and 'core' sources:
|
|
271
|
+
p50 = manager.PHIE.percentile(50)
|
|
272
|
+
# → {'well_A': {'log': 0.182, 'core': 0.205}, 'well_B': 0.195}
|
|
273
|
+
|
|
274
|
+
# With filtering, only sources with the filter property are included:
|
|
275
|
+
stats = manager.PHIE.filter('Zone').percentile(50)
|
|
276
|
+
# → {'well_A': {'log': {'Top_Brent': 0.21, ...}}, 'well_B': {...}}
|
|
277
|
+
# (core source excluded if it lacks 'Zone' property)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
> **💡 Pro Tip:** Use `nested=True` to always show source names: `manager.PHIE.percentile(50, nested=True)`
|
|
281
|
+
|
|
282
|
+
### Manager Broadcasting
|
|
283
|
+
|
|
284
|
+
Apply operations to all wells at once:
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
# Broadcast to all wells with PHIE
|
|
288
|
+
manager.PHIE_percent = manager.PHIE * 100
|
|
289
|
+
|
|
290
|
+
# Broadcast complex operations
|
|
291
|
+
manager.HC_Volume = manager.PHIE * (1 - manager.SW)
|
|
292
|
+
# ✓ Created property 'HC_Volume' in 12 well(s)
|
|
293
|
+
# ⚠ Skipped 3 well(s) without property 'PHIE' or 'SW'
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Wells without required properties are automatically skipped with a warning.
|
|
297
|
+
|
|
298
|
+
### Depth-Weighted Statistics
|
|
299
|
+
|
|
300
|
+
Standard arithmetic mean fails with irregular sampling:
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
# Example: NTG flag
|
|
304
|
+
# Depths: 1500m, 1501m, 1505m
|
|
305
|
+
# Values: 0, 1, 0
|
|
306
|
+
|
|
307
|
+
# Arithmetic mean: (0+1+0)/3 = 0.33 ❌ (treats all samples equally)
|
|
308
|
+
# Weighted mean: accounts for 2.5m interval at depth 1501m = 0.50 ✓
|
|
309
|
+
|
|
310
|
+
# Compare both methods
|
|
311
|
+
stats = well.NTG.filter('Zone').sums_avg(arithmetic=True)
|
|
312
|
+
# Returns: {'mean': {'weighted': 0.50, 'arithmetic': 0.33}, ...}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
> **✨ Key Insight:** Weighted statistics properly handle irregular sample spacing by accounting for depth intervals.
|
|
316
|
+
|
|
317
|
+
### Sampled Data (Core Plugs)
|
|
318
|
+
|
|
319
|
+
Core plug measurements are point samples requiring different treatment:
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
# Load core data as sampled
|
|
323
|
+
manager.load_las('core_plugs.las', sampled=True)
|
|
324
|
+
|
|
325
|
+
# Or mark properties as sampled
|
|
326
|
+
well.CorePHIE.type = 'sampled'
|
|
327
|
+
|
|
328
|
+
# Sampled data uses arithmetic mean (each plug counts equally)
|
|
329
|
+
stats = well.CorePHIE.filter('Zone').sums_avg()
|
|
330
|
+
# → {'Top_Brent': {'mean': 0.205, 'samples': 12, 'calculation': 'arithmetic'}}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Project Persistence
|
|
334
|
+
|
|
335
|
+
Save and restore entire projects:
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
# Save project structure
|
|
339
|
+
manager.save('my_project/')
|
|
340
|
+
# Creates: my_project/well_12_3_4_A/Petrophysics.las, Imported_Tops.las, ...
|
|
341
|
+
|
|
342
|
+
# Load project (restores everything)
|
|
343
|
+
manager = WellDataManager('my_project/')
|
|
344
|
+
|
|
345
|
+
# All wells, properties, labels, and metadata are restored
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Visualization
|
|
351
|
+
|
|
352
|
+
Create publication-quality well log displays optimized for Jupyter Lab. Build customizable templates with multiple tracks showing continuous logs, discrete properties, fills, and formation tops.
|
|
353
|
+
|
|
354
|
+
### Quick Start
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
from well_log_toolkit import WellDataManager, Template
|
|
358
|
+
|
|
359
|
+
# Load data
|
|
360
|
+
manager = WellDataManager()
|
|
361
|
+
manager.load_las("well.las")
|
|
362
|
+
well = manager.well_36_7_5_A
|
|
363
|
+
|
|
364
|
+
# Create a simple display with default template
|
|
365
|
+
view = well.WellView(depth_range=[2800, 3000])
|
|
366
|
+
view.show() # Displays inline in Jupyter
|
|
367
|
+
|
|
368
|
+
# Save to file
|
|
369
|
+
view.save("well_log.png", dpi=300)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Creating Templates
|
|
373
|
+
|
|
374
|
+
Templates define the layout and styling of well log displays:
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
from well_log_toolkit import Template
|
|
378
|
+
|
|
379
|
+
# Create template
|
|
380
|
+
template = Template("reservoir")
|
|
381
|
+
|
|
382
|
+
# Add GR track with colormap fill
|
|
383
|
+
template.add_track(
|
|
384
|
+
track_type="continuous",
|
|
385
|
+
logs=[{"name": "GR", "x_range": [0, 150], "color": "black"}],
|
|
386
|
+
fill={
|
|
387
|
+
"left": "track_edge", # Simplified: track edge
|
|
388
|
+
"right": "GR", # Simplified: curve name
|
|
389
|
+
"colormap": "viridis", # Creates horizontal color bands
|
|
390
|
+
"color_range": [20, 150], # GR values map to colormap
|
|
391
|
+
"alpha": 0.7
|
|
392
|
+
},
|
|
393
|
+
title="Gamma Ray"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Add porosity and saturation track
|
|
397
|
+
template.add_track(
|
|
398
|
+
track_type="continuous",
|
|
399
|
+
logs=[
|
|
400
|
+
{"name": "PHIE", "x_range": [0.45, 0], "color": "blue"},
|
|
401
|
+
{"name": "SW", "x_range": [0, 1], "color": "red"}
|
|
402
|
+
],
|
|
403
|
+
fill={
|
|
404
|
+
"left": "PHIE", # Simplified: curve name
|
|
405
|
+
"right": 0, # Simplified: numeric value
|
|
406
|
+
"color": "lightblue",
|
|
407
|
+
"alpha": 0.5
|
|
408
|
+
},
|
|
409
|
+
title="Porosity & Saturation"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Add resistivity track (logarithmic scale)
|
|
413
|
+
template.add_track(
|
|
414
|
+
track_type="continuous",
|
|
415
|
+
logs=[
|
|
416
|
+
{"name": "ILD", "x_range": [0.2, 2000], "color": "red"},
|
|
417
|
+
{"name": "ILM", "x_range": [0.2, 2000], "color": "green"}
|
|
418
|
+
],
|
|
419
|
+
title="Resistivity"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Add facies track (discrete/categorical)
|
|
423
|
+
template.add_track(
|
|
424
|
+
track_type="discrete",
|
|
425
|
+
logs=[{"name": "Facies"}],
|
|
426
|
+
tops={
|
|
427
|
+
"name": "Well_Tops",
|
|
428
|
+
"line_style": "--",
|
|
429
|
+
"line_width": 2.0,
|
|
430
|
+
"title_size": 9,
|
|
431
|
+
"title_weight": "bold",
|
|
432
|
+
"title_orientation": "right"
|
|
433
|
+
},
|
|
434
|
+
title="Facies"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Add depth track
|
|
438
|
+
template.add_track(track_type="depth", width=0.3, title="Depth")
|
|
439
|
+
|
|
440
|
+
# Save template for reuse
|
|
441
|
+
template.save("reservoir_template.json")
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Using Templates
|
|
445
|
+
|
|
446
|
+
**Option 1: Pass template directly**
|
|
447
|
+
```python
|
|
448
|
+
view = well.WellView(depth_range=[2800, 3000], template=template)
|
|
449
|
+
view.show()
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Option 2: Store in manager (recommended)**
|
|
453
|
+
```python
|
|
454
|
+
# Store template in manager
|
|
455
|
+
manager.set_template("reservoir", template)
|
|
456
|
+
|
|
457
|
+
# Use by name in any well
|
|
458
|
+
view = well.WellView(depth_range=[2800, 3000], template="reservoir")
|
|
459
|
+
view.show()
|
|
460
|
+
|
|
461
|
+
# List all templates
|
|
462
|
+
print(manager.list_templates()) # ['reservoir', 'qc', 'basic']
|
|
463
|
+
|
|
464
|
+
# Templates are saved with projects
|
|
465
|
+
manager.save("my_project/") # Saves to my_project/templates/reservoir.json
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Option 3: Load from file**
|
|
469
|
+
```python
|
|
470
|
+
template = Template.load("reservoir_template.json")
|
|
471
|
+
view = well.WellView(depth_range=[2800, 3000], template=template)
|
|
472
|
+
view.show()
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Track Types
|
|
476
|
+
|
|
477
|
+
**Continuous Tracks** - For log curves (GR, RHOB, NPHI, etc.)
|
|
478
|
+
```python
|
|
479
|
+
template.add_track(
|
|
480
|
+
track_type="continuous",
|
|
481
|
+
logs=[
|
|
482
|
+
{"name": "GR", "x_range": [0, 150], "color": "green", "style": "-"},
|
|
483
|
+
{"name": "CALI", "x_range": [6, 16], "color": "black", "style": "--"}
|
|
484
|
+
],
|
|
485
|
+
title="GR & Caliper"
|
|
486
|
+
)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Discrete Tracks** - For categorical data (facies, zones)
|
|
490
|
+
```python
|
|
491
|
+
template.add_track(
|
|
492
|
+
track_type="discrete",
|
|
493
|
+
logs=[{"name": "Facies"}],
|
|
494
|
+
title="Lithology"
|
|
495
|
+
)
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**Depth Tracks** - Show depth axis
|
|
499
|
+
```python
|
|
500
|
+
template.add_track(track_type="depth", width=0.3)
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Fill Patterns
|
|
504
|
+
|
|
505
|
+
**Solid Color Fill**
|
|
506
|
+
```python
|
|
507
|
+
fill={
|
|
508
|
+
"left": "PHIE", # Simplified: curve name
|
|
509
|
+
"right": 0, # Simplified: numeric value
|
|
510
|
+
"color": "lightblue",
|
|
511
|
+
"alpha": 0.5
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**Colormap Fill** (horizontal bands colored by curve value)
|
|
516
|
+
```python
|
|
517
|
+
fill={
|
|
518
|
+
"left": "track_edge", # Simplified: track edge
|
|
519
|
+
"right": "GR", # Simplified: curve name
|
|
520
|
+
"colormap": "viridis", # or "inferno", "plasma", "RdYlGn"
|
|
521
|
+
"color_range": [20, 150], # GR values map to colors
|
|
522
|
+
"alpha": 0.7
|
|
523
|
+
}
|
|
524
|
+
# Low GR (20) → dark purple, High GR (150) → bright yellow
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Fill Between Two Curves**
|
|
528
|
+
```python
|
|
529
|
+
fill={
|
|
530
|
+
"left": "RHOB", # Simplified: curve name
|
|
531
|
+
"right": "NPHI", # Simplified: curve name
|
|
532
|
+
"colormap": "RdYlGn",
|
|
533
|
+
"colormap_curve": "NPHI", # Use NPHI values for colors
|
|
534
|
+
"color_range": [0.15, 0.35],
|
|
535
|
+
"alpha": 0.6
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Formation Tops
|
|
540
|
+
|
|
541
|
+
Add formation markers to any track:
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
template.add_track(
|
|
545
|
+
track_type="discrete",
|
|
546
|
+
logs=[{"name": "Facies"}],
|
|
547
|
+
tops={
|
|
548
|
+
"name": "Well_Tops", # Property containing tops
|
|
549
|
+
"line_style": "--", # Line style
|
|
550
|
+
"line_width": 2.0, # Line thickness
|
|
551
|
+
"title_size": 9, # Label font size
|
|
552
|
+
"title_weight": "bold", # Font weight
|
|
553
|
+
"title_orientation": "right", # Label position (left/center/right)
|
|
554
|
+
"line_offset": 0.0 # Horizontal offset
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Template Management
|
|
560
|
+
|
|
561
|
+
```python
|
|
562
|
+
# Store template
|
|
563
|
+
manager.set_template("reservoir", template)
|
|
564
|
+
|
|
565
|
+
# Retrieve template
|
|
566
|
+
template = manager.get_template("reservoir")
|
|
567
|
+
|
|
568
|
+
# List all templates
|
|
569
|
+
templates = manager.list_templates() # ['reservoir', 'qc', 'basic']
|
|
570
|
+
|
|
571
|
+
# Remove template
|
|
572
|
+
manager.remove_template("old_template")
|
|
573
|
+
|
|
574
|
+
# Templates save with projects
|
|
575
|
+
manager.save("my_project/")
|
|
576
|
+
# Creates: my_project/templates/*.json
|
|
577
|
+
|
|
578
|
+
# Templates load with projects
|
|
579
|
+
manager.load("my_project/")
|
|
580
|
+
print(manager.list_templates()) # All saved templates restored
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Editing Templates
|
|
584
|
+
|
|
585
|
+
```python
|
|
586
|
+
# Load existing template
|
|
587
|
+
template = manager.get_template("reservoir")
|
|
588
|
+
|
|
589
|
+
# View tracks
|
|
590
|
+
df = template.list_tracks()
|
|
591
|
+
print(df)
|
|
592
|
+
# Index Type Logs Title Width
|
|
593
|
+
# 0 0 continuous [GR] Gamma Ray 1.0
|
|
594
|
+
# 1 1 continuous [PHIE, SW] Porosity 1.0
|
|
595
|
+
# 2 2 depth [] Depth 0.3
|
|
596
|
+
|
|
597
|
+
# Edit track
|
|
598
|
+
template.edit_track(0, title="New Title")
|
|
599
|
+
|
|
600
|
+
# Remove track
|
|
601
|
+
template.remove_track(2)
|
|
602
|
+
|
|
603
|
+
# Add new track
|
|
604
|
+
template.add_track(track_type="continuous", logs=[{"name": "RT"}])
|
|
605
|
+
|
|
606
|
+
# Save changes
|
|
607
|
+
template.save("updated_template.json")
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Customization Options
|
|
611
|
+
|
|
612
|
+
**Log Styling**
|
|
613
|
+
```python
|
|
614
|
+
logs=[{
|
|
615
|
+
"name": "GR",
|
|
616
|
+
"x_range": [0, 150], # X-axis limits [left, right]
|
|
617
|
+
"color": "green", # Line color (name or hex)
|
|
618
|
+
"style": "-", # Line style ("-", "--", "-.", ":")
|
|
619
|
+
"thickness": 1.5, # Line width
|
|
620
|
+
"alpha": 0.8 # Transparency (0-1)
|
|
621
|
+
}]
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**Figure Settings**
|
|
625
|
+
```python
|
|
626
|
+
view = well.WellView(
|
|
627
|
+
depth_range=[2800, 3000],
|
|
628
|
+
template="reservoir",
|
|
629
|
+
figsize=(12, 10), # Width x height in inches
|
|
630
|
+
dpi=100 # Resolution
|
|
631
|
+
)
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
**Export Options**
|
|
635
|
+
```python
|
|
636
|
+
# PNG for presentations
|
|
637
|
+
view.save("well_log.png", dpi=300)
|
|
638
|
+
|
|
639
|
+
# PDF for publications
|
|
640
|
+
view.save("well_log.pdf")
|
|
641
|
+
|
|
642
|
+
# SVG for editing
|
|
643
|
+
view.save("well_log.svg")
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Complete Example
|
|
647
|
+
|
|
648
|
+
```python
|
|
649
|
+
from well_log_toolkit import WellDataManager, Template
|
|
650
|
+
|
|
651
|
+
# Setup
|
|
652
|
+
manager = WellDataManager()
|
|
653
|
+
manager.load_las("well.las")
|
|
654
|
+
|
|
655
|
+
# Create comprehensive template
|
|
656
|
+
template = Template("petrophysics")
|
|
657
|
+
|
|
658
|
+
# Track 1: GR with colormap
|
|
659
|
+
template.add_track(
|
|
660
|
+
track_type="continuous",
|
|
661
|
+
logs=[{"name": "GR", "x_range": [0, 150], "color": "black"}],
|
|
662
|
+
fill={
|
|
663
|
+
"left": "track_edge",
|
|
664
|
+
"right": "GR",
|
|
665
|
+
"colormap": "viridis",
|
|
666
|
+
"color_range": [20, 150],
|
|
667
|
+
"alpha": 0.7
|
|
668
|
+
},
|
|
669
|
+
title="Gamma Ray (API)"
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
# Track 2: Resistivity
|
|
673
|
+
template.add_track(
|
|
674
|
+
track_type="continuous",
|
|
675
|
+
logs=[
|
|
676
|
+
{"name": "ILD", "x_range": [0.2, 2000], "color": "red", "thickness": 1.5},
|
|
677
|
+
{"name": "ILM", "x_range": [0.2, 2000], "color": "green"}
|
|
678
|
+
],
|
|
679
|
+
title="Resistivity (ohmm)"
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
# Track 3: Density-Neutron
|
|
683
|
+
template.add_track(
|
|
684
|
+
track_type="continuous",
|
|
685
|
+
logs=[
|
|
686
|
+
{"name": "RHOB", "x_range": [1.95, 2.95], "color": "red"},
|
|
687
|
+
{"name": "NPHI", "x_range": [0.45, -0.15], "color": "blue"}
|
|
688
|
+
],
|
|
689
|
+
fill={
|
|
690
|
+
"left": "RHOB",
|
|
691
|
+
"right": "NPHI",
|
|
692
|
+
"colormap": "RdYlGn",
|
|
693
|
+
"color_range": [-0.15, 0.45],
|
|
694
|
+
"alpha": 0.5
|
|
695
|
+
},
|
|
696
|
+
title="Density-Neutron"
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# Track 4: Porosity & Saturation
|
|
700
|
+
template.add_track(
|
|
701
|
+
track_type="continuous",
|
|
702
|
+
logs=[
|
|
703
|
+
{"name": "PHIE", "x_range": [0.45, 0], "color": "blue"},
|
|
704
|
+
{"name": "SW", "x_range": [0, 1], "color": "red"}
|
|
705
|
+
],
|
|
706
|
+
fill={
|
|
707
|
+
"left": "PHIE",
|
|
708
|
+
"right": 0,
|
|
709
|
+
"color": "lightblue",
|
|
710
|
+
"alpha": 0.5
|
|
711
|
+
},
|
|
712
|
+
title="PHIE & SW"
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
# Track 5: Facies with formation tops
|
|
716
|
+
template.add_track(
|
|
717
|
+
track_type="discrete",
|
|
718
|
+
logs=[{"name": "Facies"}],
|
|
719
|
+
tops={
|
|
720
|
+
"name": "Well_Tops",
|
|
721
|
+
"line_style": "--",
|
|
722
|
+
"line_width": 2.0,
|
|
723
|
+
"title_size": 9,
|
|
724
|
+
"title_weight": "bold",
|
|
725
|
+
"title_orientation": "right"
|
|
726
|
+
},
|
|
727
|
+
title="Lithofacies"
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# Track 6: Depth
|
|
731
|
+
template.add_track(track_type="depth", width=0.3, title="MD (m)")
|
|
732
|
+
|
|
733
|
+
# Save template and create display
|
|
734
|
+
manager.set_template("petrophysics", template)
|
|
735
|
+
well = manager.well_36_7_5_A
|
|
736
|
+
view = well.WellView(depth_range=[2800, 3200], template="petrophysics")
|
|
737
|
+
view.save("petrophysics_display.png", dpi=300)
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## Documentation
|
|
743
|
+
|
|
744
|
+
### Quick References
|
|
745
|
+
|
|
746
|
+
Jump directly to specific topics:
|
|
747
|
+
|
|
748
|
+
- **[Managing Wells](#managing-wells)** - Add, remove, access wells
|
|
749
|
+
- **[Manager-Level Statistics](#manager-level-statistics)** - Cross-well analytics
|
|
750
|
+
- **[Visualization](#visualization)** - Create well log displays with templates
|
|
751
|
+
- **[Formation Tops](#formation-tops-setup)** - Load and configure formation tops
|
|
752
|
+
- **[Discrete Properties](#discrete-properties--labels)** - Set up labels for readable output
|
|
753
|
+
- **[Statistics Explained](#understanding-statistics-output)** - What each statistic means
|
|
754
|
+
- **[Export Options](#export-options)** - DataFrame and LAS export
|
|
755
|
+
- **[Managing Sources](#managing-sources)** - Organize and rename sources
|
|
756
|
+
- **[Adding Data](#adding-external-data)** - Import from DataFrames
|
|
757
|
+
- **[Property Printing](#property-printing)** - Inspect data visually
|
|
758
|
+
- **[Troubleshooting](#troubleshooting)** - Common issues and solutions
|
|
759
|
+
|
|
760
|
+
### API Reference
|
|
761
|
+
|
|
762
|
+
```python
|
|
763
|
+
# Main classes
|
|
764
|
+
from well_log_toolkit import WellDataManager, Well, Property, LasFile
|
|
765
|
+
|
|
766
|
+
# Visualization
|
|
767
|
+
from well_log_toolkit import Template, WellView
|
|
768
|
+
|
|
769
|
+
# Statistics functions
|
|
770
|
+
from well_log_toolkit import compute_intervals, mean, sum, std, percentile
|
|
771
|
+
|
|
772
|
+
# Exceptions
|
|
773
|
+
from well_log_toolkit import (
|
|
774
|
+
DepthAlignmentError,
|
|
775
|
+
PropertyNotFoundError,
|
|
776
|
+
PropertyTypeError
|
|
777
|
+
)
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### Common Patterns
|
|
781
|
+
|
|
782
|
+
**Load and analyze quickly:**
|
|
783
|
+
```python
|
|
784
|
+
manager = WellDataManager()
|
|
785
|
+
manager.load_las('well.las')
|
|
786
|
+
stats = manager.well_12_3_4_A.PHIE.filter('Zone').sums_avg()
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
**Chain multiple filters:**
|
|
790
|
+
```python
|
|
791
|
+
stats = well.PHIE.filter('Zone').filter('Facies').filter('NTG_Flag').sums_avg()
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
**Multi-well statistics:**
|
|
795
|
+
```python
|
|
796
|
+
# Cross-well P50 by zone
|
|
797
|
+
p50_by_zone = manager.PHIE.filter('Zone').percentile(50)
|
|
798
|
+
|
|
799
|
+
# Compare statistics across wells
|
|
800
|
+
means = manager.PHIE.filter('Zone').mean()
|
|
801
|
+
stds = manager.PHIE.filter('Zone').std()
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
**Create computed properties:**
|
|
805
|
+
```python
|
|
806
|
+
well.HC_Volume = well.PHIE * (1 - well.SW)
|
|
807
|
+
well.Reservoir = (well.PHIE > 0.15) & (well.SW < 0.35)
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
**Broadcast across wells:**
|
|
811
|
+
```python
|
|
812
|
+
manager.PHIE_percent = manager.PHIE * 100
|
|
813
|
+
manager.Reservoir = (manager.PHIE > 0.15) & (manager.SW < 0.35)
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**Visualize well logs:**
|
|
817
|
+
```python
|
|
818
|
+
# Quick display
|
|
819
|
+
view = well.WellView(depth_range=[2800, 3000])
|
|
820
|
+
view.show()
|
|
821
|
+
|
|
822
|
+
# With custom template
|
|
823
|
+
template = Template("reservoir")
|
|
824
|
+
template.add_track(track_type="continuous", logs=[{"name": "GR"}])
|
|
825
|
+
manager.set_template("reservoir", template)
|
|
826
|
+
view = well.WellView(template="reservoir")
|
|
827
|
+
view.save("log.png", dpi=300)
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
**Save and restore projects:**
|
|
831
|
+
```python
|
|
832
|
+
manager.save('project/')
|
|
833
|
+
manager = WellDataManager('project/')
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
---
|
|
837
|
+
|
|
838
|
+
## Detailed Guide
|
|
839
|
+
|
|
840
|
+
### Managing Wells
|
|
841
|
+
|
|
842
|
+
```python
|
|
843
|
+
# List all wells
|
|
844
|
+
print(manager.wells) # ['well_12_3_4_A', 'well_12_3_4_B']
|
|
845
|
+
|
|
846
|
+
# Access by sanitized name (attribute access)
|
|
847
|
+
well = manager.well_12_3_4_A
|
|
848
|
+
|
|
849
|
+
# Access by original name
|
|
850
|
+
well = manager.get_well('12/3-4 A') # Works with original name
|
|
851
|
+
well = manager.get_well('12_3_4_A') # Works with sanitized name
|
|
852
|
+
well = manager.get_well('well_12_3_4_A') # Works with well_ prefix
|
|
853
|
+
|
|
854
|
+
# Add well manually
|
|
855
|
+
well = manager.add_well('12/3-4 C')
|
|
856
|
+
|
|
857
|
+
# Remove well
|
|
858
|
+
manager.remove_well('12_3_4_A')
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### Manager-Level Statistics
|
|
862
|
+
|
|
863
|
+
Compute statistics across multiple wells at once. Results are returned as nested dictionaries with well names as keys:
|
|
864
|
+
|
|
865
|
+
```python
|
|
866
|
+
# Basic statistics - returns value per well
|
|
867
|
+
p50 = manager.PHIE.percentile(50)
|
|
868
|
+
# {'well_A': 0.182, 'well_B': 0.195, 'well_C': 0.173}
|
|
869
|
+
|
|
870
|
+
mean = manager.PHIE.mean()
|
|
871
|
+
std = manager.PHIE.std()
|
|
872
|
+
min_val = manager.PHIE.min()
|
|
873
|
+
max_val = manager.PHIE.max()
|
|
874
|
+
median = manager.PHIE.median()
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
**With filtering** - returns grouped statistics per well:
|
|
878
|
+
|
|
879
|
+
```python
|
|
880
|
+
# Group by one filter
|
|
881
|
+
stats = manager.PHIE.filter('Zone').percentile(50)
|
|
882
|
+
# {
|
|
883
|
+
# 'well_A': {'Top_Brent': 0.21, 'Top_Statfjord': 0.15, 'Top_Cook': 0.18},
|
|
884
|
+
# 'well_B': {'Top_Brent': 0.19, 'Top_Statfjord': 0.17}
|
|
885
|
+
# }
|
|
886
|
+
|
|
887
|
+
# Chain multiple filters for hierarchical grouping
|
|
888
|
+
stats = manager.PHIE.filter('Zone').filter('Facies').mean()
|
|
889
|
+
# {
|
|
890
|
+
# 'well_A': {
|
|
891
|
+
# 'Top_Brent': {'Sandstone': 0.23, 'Shale': 0.08},
|
|
892
|
+
# 'Top_Statfjord': {'Sandstone': 0.19, 'Shale': 0.06}
|
|
893
|
+
# },
|
|
894
|
+
# 'well_B': {...}
|
|
895
|
+
# }
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
**Handling ambiguous properties** - properties existing in multiple sources (e.g., PHIE in both log and core):
|
|
899
|
+
|
|
900
|
+
```python
|
|
901
|
+
# Without filters - nests by source when ambiguous
|
|
902
|
+
p50 = manager.PHIE.percentile(50)
|
|
903
|
+
# {
|
|
904
|
+
# 'well_A': {'log': 0.182, 'core': 0.205}, # Ambiguous in well_A
|
|
905
|
+
# 'well_B': 0.195 # Unique in well_B
|
|
906
|
+
# }
|
|
907
|
+
|
|
908
|
+
# With filters - only includes sources that have the filter property
|
|
909
|
+
stats = manager.PHIE.filter('Zone').percentile(50)
|
|
910
|
+
# {
|
|
911
|
+
# 'well_A': {'log': {'Top_Brent': 0.21, ...}}, # Only log has Zone
|
|
912
|
+
# 'well_B': {'Top_Brent': 0.19, ...} # Unique, no nesting
|
|
913
|
+
# }
|
|
914
|
+
|
|
915
|
+
# Force nesting for all wells (always show source names)
|
|
916
|
+
stats = manager.PHIE.percentile(50, nested=True)
|
|
917
|
+
# {
|
|
918
|
+
# 'well_A': {'log': 0.182, 'core': 0.205},
|
|
919
|
+
# 'well_B': {'log': 0.195} # Now shows source even though unique
|
|
920
|
+
# }
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
**Available statistics:**
|
|
924
|
+
- `min()` - Minimum value
|
|
925
|
+
- `max()` - Maximum value
|
|
926
|
+
- `mean()` - Arithmetic or depth-weighted average
|
|
927
|
+
- `median()` - Median value
|
|
928
|
+
- `std()` - Standard deviation
|
|
929
|
+
- `percentile(p)` - Specified percentile (e.g., 10, 50, 90)
|
|
930
|
+
|
|
931
|
+
All methods support `weighted=True` (default) for depth-weighted calculations.
|
|
932
|
+
|
|
933
|
+
### Formation Tops Setup
|
|
934
|
+
|
|
935
|
+
Formation tops require a specific DataFrame structure:
|
|
936
|
+
|
|
937
|
+
```python
|
|
938
|
+
import pandas as pd
|
|
939
|
+
|
|
940
|
+
# Required columns: Well, Surface (formation name), MD (depth)
|
|
941
|
+
tops_df = pd.DataFrame({
|
|
942
|
+
'Well': ['12/3-4 A', '12/3-4 A', '12/3-4 A'],
|
|
943
|
+
'Surface': ['Top_Brent', 'Top_Statfjord', 'Top_Cook'],
|
|
944
|
+
'MD': [2850.0, 3100.0, 3400.0]
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
manager.load_tops(
|
|
948
|
+
tops_df,
|
|
949
|
+
property_name='Zone', # Name for discrete property (default: 'Well_Tops')
|
|
950
|
+
source_name='Tops', # Source name (default: 'Imported_Tops')
|
|
951
|
+
well_col='Well', # Column with well names
|
|
952
|
+
discrete_col='Surface', # Column with formation names
|
|
953
|
+
depth_col='MD' # Column with depths
|
|
954
|
+
)
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
**How tops work:**
|
|
958
|
+
- Each top marks the **start** of that formation
|
|
959
|
+
- Uses **forward-fill**: Top_Brent applies from 2850m down to 3100m
|
|
960
|
+
- At 3100m, Top_Statfjord takes over and applies down to 3400m
|
|
961
|
+
- Labels are automatically created: `{0: 'Top_Brent', 1: 'Top_Statfjord', 2: 'Top_Cook'}`
|
|
962
|
+
|
|
963
|
+
### Discrete Properties & Labels
|
|
964
|
+
|
|
965
|
+
Labels make discrete properties human-readable:
|
|
966
|
+
|
|
967
|
+
```python
|
|
968
|
+
# Get discrete property
|
|
969
|
+
ntg = well.get_property('NTG_Flag')
|
|
970
|
+
|
|
971
|
+
# Mark as discrete
|
|
972
|
+
ntg.type = 'discrete'
|
|
973
|
+
|
|
974
|
+
# Add labels (maps numeric values to names)
|
|
975
|
+
ntg.labels = {
|
|
976
|
+
0: 'NonNet',
|
|
977
|
+
1: 'Net'
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
# Now statistics use labels instead of numbers
|
|
981
|
+
stats = well.PHIE.filter('NTG_Flag').sums_avg()
|
|
982
|
+
# Returns: {'NonNet': {...}, 'Net': {...}}
|
|
983
|
+
# Instead of: {0: {...}, 1: {...}}
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
**When to use discrete type:**
|
|
987
|
+
- Zones/formations
|
|
988
|
+
- Facies classifications
|
|
989
|
+
- Flags (net/non-net, pay/non-pay)
|
|
990
|
+
- Rock types
|
|
991
|
+
- Any categorical data
|
|
992
|
+
|
|
993
|
+
### Understanding Statistics Output
|
|
994
|
+
|
|
995
|
+
Each statistics group contains:
|
|
996
|
+
|
|
997
|
+
```python
|
|
998
|
+
stats = well.PHIE.filter('Zone').sums_avg()
|
|
999
|
+
|
|
1000
|
+
# Example output for one zone:
|
|
1001
|
+
{
|
|
1002
|
+
'Top_Brent': {
|
|
1003
|
+
# Core statistics (depth-weighted by default)
|
|
1004
|
+
'mean': 0.182, # Average porosity
|
|
1005
|
+
'sum': 45.5, # Sum (useful for flags: sum of NTG = net thickness)
|
|
1006
|
+
'std_dev': 0.044, # Standard deviation
|
|
1007
|
+
|
|
1008
|
+
# Percentiles
|
|
1009
|
+
'percentile': {
|
|
1010
|
+
'p10': 0.09, # 10th percentile (pessimistic)
|
|
1011
|
+
'p50': 0.18, # Median
|
|
1012
|
+
'p90': 0.24 # 90th percentile (optimistic)
|
|
1013
|
+
},
|
|
1014
|
+
|
|
1015
|
+
# Value range
|
|
1016
|
+
'range': {
|
|
1017
|
+
'min': 0.05, # Minimum value
|
|
1018
|
+
'max': 0.28 # Maximum value
|
|
1019
|
+
},
|
|
1020
|
+
|
|
1021
|
+
# Depth information
|
|
1022
|
+
'depth_range': {
|
|
1023
|
+
'min': 2850.0, # Top of zone
|
|
1024
|
+
'max': 3100.0 # Base of zone
|
|
1025
|
+
},
|
|
1026
|
+
|
|
1027
|
+
# Sample information
|
|
1028
|
+
'samples': 250, # Number of non-NaN measurements
|
|
1029
|
+
'thickness': 250.0, # Interval thickness (sum of depth intervals)
|
|
1030
|
+
'gross_thickness': 555.0, # Total thickness across all zones
|
|
1031
|
+
'thickness_fraction': 0.45, # Fraction of total (thickness/gross_thickness)
|
|
1032
|
+
|
|
1033
|
+
# Metadata
|
|
1034
|
+
'calculation': 'weighted' # Method: 'weighted', 'arithmetic', or 'both'
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
**Compare weighted vs arithmetic:**
|
|
1040
|
+
```python
|
|
1041
|
+
stats = well.PHIE.filter('Zone').sums_avg(arithmetic=True)
|
|
1042
|
+
|
|
1043
|
+
# Values become dicts with both methods:
|
|
1044
|
+
{
|
|
1045
|
+
'Top_Brent': {
|
|
1046
|
+
'mean': {'weighted': 0.182, 'arithmetic': 0.179},
|
|
1047
|
+
'sum': {'weighted': 45.5, 'arithmetic': 44.8},
|
|
1048
|
+
# ... other fields also have both methods
|
|
1049
|
+
'calculation': 'both'
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
### Export Options
|
|
1055
|
+
|
|
1056
|
+
**To DataFrame:**
|
|
1057
|
+
```python
|
|
1058
|
+
# All properties
|
|
1059
|
+
df = well.data()
|
|
1060
|
+
|
|
1061
|
+
# Specific properties only
|
|
1062
|
+
df = well.data(include=['PHIE', 'SW', 'PERM'])
|
|
1063
|
+
|
|
1064
|
+
# Exclude properties
|
|
1065
|
+
df = well.data(exclude=['DEPT'])
|
|
1066
|
+
|
|
1067
|
+
# Auto-resample to common depth grid (when properties have different depths)
|
|
1068
|
+
df = well.data(auto_resample=True)
|
|
1069
|
+
|
|
1070
|
+
# Use labels for discrete properties
|
|
1071
|
+
df = well.data(discrete_labels=True)
|
|
1072
|
+
# Zone column shows: 'Top_Brent', 'Top_Statfjord'
|
|
1073
|
+
# Instead of: 0, 1
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
**To LAS file:**
|
|
1077
|
+
```python
|
|
1078
|
+
# Export all properties
|
|
1079
|
+
well.export_to_las('output.las')
|
|
1080
|
+
|
|
1081
|
+
# Specific properties
|
|
1082
|
+
well.export_to_las('output.las', include=['PHIE', 'SW'])
|
|
1083
|
+
|
|
1084
|
+
# Use original LAS as template (preserves header info)
|
|
1085
|
+
well.export_to_las('output.las', use_template=True)
|
|
1086
|
+
|
|
1087
|
+
# Export each source separately
|
|
1088
|
+
well.export_sources('output_folder/')
|
|
1089
|
+
# Creates: Petrophysics.las, Imported_Tops.las, computed.las
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
### Managing Sources
|
|
1093
|
+
|
|
1094
|
+
Sources organize properties by origin (e.g., different LAS files, imported data):
|
|
1095
|
+
|
|
1096
|
+
```python
|
|
1097
|
+
# List all sources
|
|
1098
|
+
print(well.sources) # ['Petrophysics', 'CoreData', 'Imported_Tops']
|
|
1099
|
+
|
|
1100
|
+
# Access properties through source
|
|
1101
|
+
phie = well.Petrophysics.PHIE
|
|
1102
|
+
core_phie = well.CoreData.CorePHIE
|
|
1103
|
+
|
|
1104
|
+
# List properties in a source
|
|
1105
|
+
print(well.Petrophysics.properties) # ['DEPT', 'PHIE', 'SW', 'PERM']
|
|
1106
|
+
|
|
1107
|
+
# Rename source
|
|
1108
|
+
well.rename_source('CoreData', 'Core_Porosity')
|
|
1109
|
+
print(well.sources) # ['Petrophysics', 'Core_Porosity', 'Imported_Tops']
|
|
1110
|
+
|
|
1111
|
+
# Remove source (deletes all its properties)
|
|
1112
|
+
well.remove_source('Core_Porosity')
|
|
1113
|
+
print(well.sources) # ['Petrophysics', 'Imported_Tops']
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
**Changes are saved to disk:**
|
|
1117
|
+
```python
|
|
1118
|
+
manager.save() # Renamed files updated, removed files deleted
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
### Adding External Data
|
|
1122
|
+
|
|
1123
|
+
Load data from pandas DataFrames:
|
|
1124
|
+
|
|
1125
|
+
```python
|
|
1126
|
+
import pandas as pd
|
|
1127
|
+
|
|
1128
|
+
# Create DataFrame with depth column
|
|
1129
|
+
external_df = pd.DataFrame({
|
|
1130
|
+
'DEPT': [2800, 2801, 2802, 2803],
|
|
1131
|
+
'CorePHIE': [0.20, 0.22, 0.19, 0.21],
|
|
1132
|
+
'CorePERM': [150, 200, 120, 180]
|
|
1133
|
+
})
|
|
1134
|
+
|
|
1135
|
+
# Add to well
|
|
1136
|
+
well.add_dataframe(
|
|
1137
|
+
external_df,
|
|
1138
|
+
source_name='CoreData', # Optional, defaults to 'external_df'
|
|
1139
|
+
unit_mappings={ # Optional, specify units
|
|
1140
|
+
'CorePHIE': 'v/v',
|
|
1141
|
+
'CorePERM': 'mD'
|
|
1142
|
+
},
|
|
1143
|
+
type_mappings={ # Optional, specify types
|
|
1144
|
+
'CorePHIE': 'continuous',
|
|
1145
|
+
'CorePERM': 'continuous'
|
|
1146
|
+
},
|
|
1147
|
+
label_mappings={} # Optional, for discrete properties
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
# Access new properties
|
|
1151
|
+
print(well.CoreData.CorePHIE.values)
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
### Property Printing
|
|
1155
|
+
|
|
1156
|
+
Inspect properties directly:
|
|
1157
|
+
|
|
1158
|
+
```python
|
|
1159
|
+
# Print property (numpy-style, auto-clips large arrays)
|
|
1160
|
+
print(well.PHIE)
|
|
1161
|
+
# [PHIE] (1001 samples)
|
|
1162
|
+
# depth: [2800.00, 2801.00, 2802.00, ..., 3798.00, 3799.00, 3800.00]
|
|
1163
|
+
# values (v/v): [0.180, 0.185, 0.192, ..., 0.215, 0.212, 0.210]
|
|
1164
|
+
|
|
1165
|
+
# Print filtered property (shows filter values)
|
|
1166
|
+
filtered = well.PHIE.filter('Zone').filter('NTG_Flag')
|
|
1167
|
+
print(filtered)
|
|
1168
|
+
# [PHIE] (1001 samples)
|
|
1169
|
+
# depth: [2800.00, 2801.00, ..., 3800.00]
|
|
1170
|
+
# values (v/v): [0.180, 0.185, ..., 0.210]
|
|
1171
|
+
#
|
|
1172
|
+
# Filters:
|
|
1173
|
+
# Zone: [Top_Brent, Top_Brent, ..., Top_Statfjord]
|
|
1174
|
+
# NTG_Flag: [NonNet, Net, ..., Net]
|
|
1175
|
+
|
|
1176
|
+
# Print manager-level property (shows all wells)
|
|
1177
|
+
print(manager.PHIE)
|
|
1178
|
+
# [PHIE] across 3 well(s):
|
|
1179
|
+
#
|
|
1180
|
+
# Well: well_12_3_4_A
|
|
1181
|
+
# [PHIE] (1001 samples)
|
|
1182
|
+
# ...
|
|
1183
|
+
#
|
|
1184
|
+
# Well: well_12_3_4_B
|
|
1185
|
+
# [PHIE] (856 samples)
|
|
1186
|
+
# ...
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
### Troubleshooting
|
|
1190
|
+
|
|
1191
|
+
**DepthAlignmentError: Cannot combine properties with different depth grids**
|
|
1192
|
+
|
|
1193
|
+
```python
|
|
1194
|
+
# Problem: Properties have different depths
|
|
1195
|
+
result = well.PHIE + well.CorePHIE # Error!
|
|
1196
|
+
|
|
1197
|
+
# Solution: Explicitly resample
|
|
1198
|
+
core_resampled = well.CorePHIE.resample(well.PHIE)
|
|
1199
|
+
result = well.PHIE + core_resampled # Works!
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
**PropertyNotFoundError: Property not found**
|
|
1203
|
+
|
|
1204
|
+
```python
|
|
1205
|
+
# Problem: Property doesn't exist or wrong name
|
|
1206
|
+
phie = well.PHIE_TOTAL # Error if property doesn't exist
|
|
1207
|
+
|
|
1208
|
+
# Solution: Check available properties
|
|
1209
|
+
print(well.properties) # List all properties
|
|
1210
|
+
print(well.sources) # List all sources
|
|
1211
|
+
|
|
1212
|
+
# Or check if property exists
|
|
1213
|
+
try:
|
|
1214
|
+
phie = well.get_property('PHIE_TOTAL')
|
|
1215
|
+
except PropertyNotFoundError:
|
|
1216
|
+
print("Property not found, using default")
|
|
1217
|
+
phie = well.PHIE
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
**PropertyTypeError: Property must be discrete type**
|
|
1221
|
+
|
|
1222
|
+
```python
|
|
1223
|
+
# Problem: Trying to filter by non-discrete property
|
|
1224
|
+
stats = well.PHIE.filter('PERM').sums_avg() # Error!
|
|
1225
|
+
|
|
1226
|
+
# Solution: Mark property as discrete
|
|
1227
|
+
perm = well.get_property('PERM')
|
|
1228
|
+
perm.type = 'discrete'
|
|
1229
|
+
perm.labels = {0: 'Low', 1: 'Medium', 2: 'High'}
|
|
1230
|
+
stats = well.PHIE.filter('PERM').sums_avg() # Works!
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
**Missing statistics for some zones**
|
|
1234
|
+
|
|
1235
|
+
```python
|
|
1236
|
+
# Problem: No valid data in some zones
|
|
1237
|
+
stats = well.PHIE.filter('Zone').sums_avg()
|
|
1238
|
+
# Some zones might be missing if all PHIE values are NaN in that zone
|
|
1239
|
+
|
|
1240
|
+
# Solution: Check raw data
|
|
1241
|
+
print(well.PHIE.values) # Look for NaN values
|
|
1242
|
+
print(well.Zone.values) # Check zone distribution
|
|
1243
|
+
|
|
1244
|
+
# Or filter to remove NaN
|
|
1245
|
+
valid_mask = ~np.isnan(well.PHIE.values)
|
|
1246
|
+
valid_depths = well.PHIE.depth[valid_mask]
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
**Computed properties not showing up**
|
|
1250
|
+
|
|
1251
|
+
```python
|
|
1252
|
+
# After creating computed properties
|
|
1253
|
+
well.HC_Volume = well.PHIE * (1 - well.SW)
|
|
1254
|
+
|
|
1255
|
+
# Check they exist
|
|
1256
|
+
print(well.sources) # Should include 'computed'
|
|
1257
|
+
print(well.computed.properties) # List computed properties
|
|
1258
|
+
|
|
1259
|
+
# Access directly
|
|
1260
|
+
hc = well.HC_Volume # Works
|
|
1261
|
+
|
|
1262
|
+
# Or through source
|
|
1263
|
+
hc = well.computed.HC_Volume # Also works
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
---
|
|
1267
|
+
|
|
1268
|
+
## Requirements
|
|
1269
|
+
|
|
1270
|
+
- Python >= 3.9
|
|
1271
|
+
- numpy >= 1.20.0
|
|
1272
|
+
- pandas >= 1.3.0
|
|
1273
|
+
- scipy >= 1.7.0
|
|
1274
|
+
- matplotlib >= 3.5.0
|
|
1275
|
+
|
|
1276
|
+
## Performance
|
|
1277
|
+
|
|
1278
|
+
All operations use **vectorized numpy** for maximum speed:
|
|
1279
|
+
- 100M+ samples/second throughput
|
|
1280
|
+
- Typical well logs (1k-10k samples) process in < 1ms
|
|
1281
|
+
- Filtered statistics (2 filters, 10 wells): ~9ms
|
|
1282
|
+
- Manager-level operations optimized with property caching
|
|
1283
|
+
- I/O bottleneck eliminated with lazy loading
|
|
1284
|
+
|
|
1285
|
+
## Contributing
|
|
1286
|
+
|
|
1287
|
+
Contributions welcome! Please submit a Pull Request.
|
|
1288
|
+
|
|
1289
|
+
## License
|
|
1290
|
+
|
|
1291
|
+
MIT License
|