well-log-toolkit 0.1.152
Fast LAS file processing with lazy loading and filtering for well log analysis
License: MIT
Requires Python: >=3.9
Keywords: well-log, las-file, petrophysics, geoscience, oil-and-gas, petroleum-engineering

# Well Log Toolkit

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.

[![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

## Key Features

- **🚀 Lazy Loading** - Parse headers instantly, load data on demand
- **🧮 Numpy-Style Operations** - `well.HC_Volume = well.PHIE * (1 - well.SW)`
- **🔍 Hierarchical Filtering** - Chain filters: `well.PHIE.filter('Zone').filter('Facies').sums_avg()`
- **⚖️ Depth-Weighted Statistics** - Proper averaging for irregular sampling
- **📊 Multi-Well Analytics** - Cross-well statistics: `manager.PHIE.filter('Zone').percentile(50)`
- **🎨 Professional Visualization** - Create customizable well log displays with templates
- **📊 Interactive Crossplots** - Beautiful scatter plots with color/size/shape mapping by property
- **📈 Regression Analysis** - 5 regression types (linear, polynomial, exponential, log, power)
- **💾 Project Persistence** - Save/load entire projects with metadata and templates

---

## Table of Contents

### Getting Started
- [Installation](#installation)
- [1-Minute Tutorial](#1-minute-tutorial)
- [5-Minute Quick Start](#5-minute-quick-start)

### Learning Path
- [Core Concepts](#core-concepts) - Essential patterns and workflows
- [Visualization Guide](#visualization-guide) - Creating well log displays
- [Crossplot & Regression Guide](#crossplot--regression-guide) - Data analysis and trend visualization
- [Advanced Topics](#advanced-topics) - Deep dives into specific features

### Quick Reference
- [Style & Marker Reference](#style--marker-reference) - Line styles, markers, colors
- [Colormap Reference](#colormap-reference) - Available colormaps
- [API Reference](#api-reference) - Classes, methods, exceptions
- [Common Patterns](#common-patterns) - Copy-paste examples
- [Troubleshooting](#troubleshooting) - Solutions to common issues

---

## Installation

```bash
pip install well-log-toolkit
```

**Requirements:** Python 3.9+, numpy, pandas, scipy, matplotlib

---

## 1-Minute Tutorial

Load LAS files, filter by zones, and compute statistics:

```python
from well_log_toolkit import WellDataManager

# Load and analyze
manager = WellDataManager()
manager.load_las('well.las')

well = manager.well_12_3_4_A
stats = well.PHIE.filter('Zone').sums_avg()

print(stats['Top_Brent']['mean'])  # → 0.182 (depth-weighted)
```

**That's it!** Three lines to go from LAS file to zonal statistics.

> **New to this?** Continue to [5-Minute Quick Start](#5-minute-quick-start) for a complete walkthrough.

---

## 5-Minute Quick Start

### Step 1: Load Your Data

```python
from well_log_toolkit import WellDataManager
import pandas as pd

# Create manager and load LAS files
manager = WellDataManager()
manager.load_las('well_A.las')
manager.load_las('well_B.las')

# Load formation tops from DataFrame
tops_df = pd.DataFrame({
    'Well': ['12/3-4 A', '12/3-4 A', '12/3-4 B'],
    'Surface': ['Top_Brent', 'Top_Statfjord', 'Top_Brent'],
    'MD': [2850.0, 3100.0, 2900.0]
})

manager.load_tops(tops_df, well_col='Well', discrete_col='Surface', depth_col='MD')
```

### Step 2: Access Wells and Properties

```python
# Access well (special characters auto-sanitized)
well = manager.well_12_3_4_A

# Access properties directly
phie = well.PHIE
sw = well.SW

# List everything
print(well.properties)  # ['PHIE', 'SW', 'PERM', 'Zone', ...]
print(well.sources)     # ['Petrophysics', 'Imported_Tops']
```

### Step 3: Compute Statistics

```python
# Single filter - group by Zone
stats = well.PHIE.filter('Zone').sums_avg()
# → {'Top_Brent': {'mean': 0.182, 'thickness': 250.0, ...}, ...}

# Chain filters - hierarchical grouping
stats = well.PHIE.filter('Zone').filter('Facies').sums_avg()
# → {'Top_Brent': {'Sandstone': {...}, 'Shale': {...}}, ...}
```

> **💡 Key Insight:** Statistics are **depth-weighted** by default, accounting for irregular sampling.

### Step 4: Create Computed Properties

```python
# Mathematical expressions (numpy-style)
well.HC_Volume = well.PHIE * (1 - well.SW)
well.PHIE_percent = well.PHIE * 100

# Comparison operations (creates discrete flags)
well.Reservoir = (well.PHIE > 0.15) & (well.SW < 0.35)

# Apply to all wells at once
manager.PHIE_percent = manager.PHIE * 100
```

### Step 5: Visualize Well Logs

```python
from well_log_toolkit import Template

# Create template
template = Template("basic")

# Add GR track
template.add_track(
    track_type="continuous",
    logs=[{"name": "GR", "x_range": [0, 150], "color": "green"}],
    title="Gamma Ray"
)

# Add depth track
template.add_track(track_type="depth", width=0.3)

# Display
view = well.WellView(depth_range=[2800, 3000], template=template)
view.show()
view.save("well_log.png", dpi=300)
```

### Step 6: Save Your Work

```python
# Save entire project
manager.save('my_project/')

# Load later
manager = WellDataManager('my_project/')
```

**Done!** You've learned the core workflow in 5 minutes.

> **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.

---

## Core Concepts

### Understanding Well Log Data

Well log data consists of measurements taken at various depths. This library organizes data into three key components:

1. **Wells** - Individual wellbores (e.g., "12/3-4 A")
2. **Properties** - Measurements or computed values (e.g., PHIE, SW, GR)
3. **Sources** - Origin of data (e.g., "Petrophysics", "CoreData", "computed")

```python
# Access structure
well = manager.well_12_3_4_A
print(well.sources)     # ['Petrophysics', 'CoreData']
print(well.properties)  # ['PHIE', 'SW', 'GR', ...]

# Get property
phie = well.PHIE  # Shorthand
phie = well.get_property('PHIE')  # Explicit
phie = well.Petrophysics.PHIE  # From specific source
```

### Property Types

Properties can be **continuous** (numeric measurements), **discrete** (categories), or **sampled** (point measurements like core plugs):

```python
# Continuous (default) - log curves
well.PHIE.type  # → 'continuous'

# Discrete - zones, facies, flags
zone = well.get_property('Zone')
zone.type = 'discrete'
zone.labels = {0: 'Top_Brent', 1: 'Top_Statfjord', 2: 'Top_Cook'}

# Sampled - core plugs (arithmetic mean instead of depth-weighted)
core_phie = well.get_property('CorePHIE')
core_phie.type = 'sampled'
```

### Hierarchical Filtering

Filter properties by discrete logs to compute grouped statistics:

```python
# Single filter
stats = well.PHIE.filter('Zone').sums_avg()
# {
#   'Top_Brent': {'mean': 0.21, 'thickness': 150.0, ...},
#   'Top_Statfjord': {'mean': 0.17, 'thickness': 180.0, ...}
# }

# Chain multiple filters for hierarchical grouping
stats = well.PHIE.filter('Zone').filter('Facies').sums_avg()
# {
#   'Top_Brent': {
#     'Sandstone': {'mean': 0.23, 'thickness': 120.0, ...},
#     'Shale': {'mean': 0.08, 'thickness': 30.0, ...}
#   },
#   'Top_Statfjord': {...}
# }
```

**Statistics include:**
- `mean`, `sum`, `std_dev` - Depth-weighted by default
- `percentile` - p10, p50, p90 values
- `thickness` - Depth interval thickness
- `samples` - Number of valid measurements
- `range`, `depth_range` - Min/max values and depths

### Custom Interval Filtering

Define custom depth intervals without needing a discrete property in the well:

```python
# Define intervals with name, top, and base
intervals = [
    {"name": "Zone_A", "top": 2500, "base": 2650},
    {"name": "Zone_B", "top": 2650, "base": 2800}
]

# Use with sums_avg or discrete_summary
stats = well.PHIE.filter_intervals(intervals).sums_avg()
# → {'Zone_A': {'mean': 0.18, ...}, 'Zone_B': {'mean': 0.21, ...}}

facies_stats = well.Facies.filter_intervals(intervals).discrete_summary()
```

**Overlapping intervals** are supported - each interval is calculated independently:

```python
# These intervals overlap at 2600-2700m
intervals = [
    {"name": "Full_Reservoir", "top": 2500, "base": 2800},
    {"name": "Upper_Section", "top": 2500, "base": 2700}
]
# Depths 2500-2700 are counted in BOTH zones
stats = well.PHIE.filter_intervals(intervals).sums_avg()
```

**Save intervals for reuse:**

```python
# Save intervals to the well
well.PHIE.filter_intervals(intervals, save="Reservoir_Zones")

# Use saved intervals by name
stats = well.PHIE.filter_intervals("Reservoir_Zones").sums_avg()

# List saved intervals
print(well.saved_intervals)  # ['Reservoir_Zones']

# Retrieve intervals
intervals = well.get_intervals("Reservoir_Zones")
```

**Save different intervals for multiple wells:**

```python
# Define well-specific intervals
manager.well_A.PHIE.filter_intervals({
    "Well_A": [{"name": "Zone_A", "top": 2500, "base": 2700}],
    "Well_B": [{"name": "Zone_A", "top": 2600, "base": 2800}]
}, save="My_Zones")

# Both wells now have "My_Zones" saved with their respective intervals
```

**Chain with other filters:**

```python
# Combine custom intervals with property filters
stats = well.PHIE.filter_intervals(intervals).filter("NetFlag").sums_avg()
# → {'Zone_A': {'Net': {...}, 'NonNet': {...}}, 'Zone_B': {...}}
```

> **💡 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.

### Property Operations

Create computed properties using natural mathematical syntax:

```python
# Arithmetic operations (requires matching depth grids)
well.HC_Volume = well.PHIE * (1 - well.SW)
well.Porosity_Avg = (well.PHIE + well.PHIT) / 2

# Comparison operations (auto-creates discrete properties)
well.High_Poro = well.PHIE > 0.15
well.Reservoir = (well.PHIE > 0.15) & (well.SW < 0.35)

# Use computed properties in filtering
stats = well.PHIE.filter('Reservoir').sums_avg()
# → {False: {...}, True: {...}}
```

> **💡 Pro Tip:** Computed properties are stored in the `'computed'` source and can be exported to LAS files.

### Depth Alignment

Operations require matching depth grids (like numpy arrays) to prevent silent interpolation errors:

```python
# This fails if depths don't match
result = well.PHIE + well.CorePHIE  # DepthAlignmentError

# Explicit resampling required
core_resampled = well.CorePHIE.resample(well.PHIE)
result = well.PHIE + core_resampled  # ✓ Works
```

### Multi-Well Analytics

Compute statistics across all wells in a single call:

```python
# Single statistic across all wells
p50 = manager.PHIE.percentile(50)
# → {'well_A': 0.182, 'well_B': 0.195, 'well_C': 0.173}

# With filtering - grouped by filter values per well
stats = manager.PHIE.filter('Zone').percentile(50)
# → {
#   'well_A': {'Top_Brent': 0.21, 'Top_Statfjord': 0.15},
#   'well_B': {'Top_Brent': 0.19, 'Top_Statfjord': 0.17}
# }

# Chain filters for hierarchical grouping
stats = manager.PHIE.filter('Zone').filter('Facies').mean()

# All statistics: min, max, mean, median, std, percentile
```

**Ambiguous properties** (existing in multiple sources) automatically nest by source:

```python
# If well_A has PHIE in both 'log' and 'core' sources:
p50 = manager.PHIE.percentile(50)
# → {'well_A': {'log': 0.182, 'core': 0.205}, 'well_B': 0.195}
```

### Manager Broadcasting

Apply operations to all wells at once:

```python
# Broadcast to all wells with PHIE
manager.PHIE_percent = manager.PHIE * 100

# Broadcast complex operations
manager.HC_Volume = manager.PHIE * (1 - manager.SW)
# ✓ Created property 'HC_Volume' in 12 well(s)
# ⚠ Skipped 3 well(s) without property 'PHIE' or 'SW'
```

### Depth-Weighted vs Arithmetic Statistics

Standard arithmetic mean fails with irregular sampling:

```python
# Example: NTG flag at depths 1500m, 1501m, 1505m with values 0, 1, 0
# Arithmetic mean: (0+1+0)/3 = 0.33 ❌ (treats all samples equally)
# Weighted mean: accounts for 2.5m interval at 1501m = 0.50 ✓

# Compare both methods
stats = well.NTG.filter('Zone').sums_avg(arithmetic=True)
# Returns: {'mean': {'weighted': 0.50, 'arithmetic': 0.33}, ...}
```

> **✨ Key Insight:** Depth-weighted statistics properly handle irregular sample spacing by accounting for depth intervals.

### Project Persistence

Save and restore entire projects:

```python
# Save project structure
manager.save('my_project/')
# Creates: my_project/well_12_3_4_A/Petrophysics.las, templates/*.json, ...

# Load project (restores everything)
manager = WellDataManager('my_project/')
```

---

## Visualization Guide

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.

### Quick Start

```python
from well_log_toolkit import WellDataManager

# Load data
manager = WellDataManager()
manager.load_las("well.las")
well = manager.well_36_7_5_A

# Simple display with default template
view = well.WellView(depth_range=[2800, 3000])
view.show()  # Displays inline in Jupyter

# Save to file
view.save("well_log.png", dpi=300)
```

#### Auto-Calculate Depth Range from Tops

Instead of manually specifying depth ranges, let WellView automatically calculate the range from formation tops:

```python
# Load formation tops
manager.load_tops(tops_df, well_col='Well', discrete_col='Surface', depth_col='MD')

# Add tops to template
template = Template("reservoir")
template.add_tops(property_name='Zone')

# Auto-calculate depth range from specific tops
view = well.WellView(
    tops=['Top_Brent', 'Top_Statfjord'],  # Specify which tops to show
    template=template
)
view.show()
# Automatically shows Top_Brent to Top_Statfjord with 5% padding (min 50m range)
```

**How it works:**
- Finds the minimum and maximum depths of specified tops
- Adds 5% padding above and below
- Ensures minimum range of 50 meters
- Perfect for focusing on specific intervals without manual depth calculations

### Building Templates

Templates define the layout and styling of well log displays. Think of a template as a blueprint that can be reused across multiple wells.

#### Basic Template Structure

```python
from well_log_toolkit import Template

# Create template
template = Template("reservoir")

# Add tracks (order matters - left to right)
template.add_track(track_type="continuous", logs=[...], title="GR")
template.add_track(track_type="continuous", logs=[...], title="Resistivity")
template.add_track(track_type="discrete", logs=[...], title="Facies")
template.add_track(track_type="depth", width=0.3, title="Depth")

# Add to project (saves with manager.save())
manager.add_template(template)  # Uses template name "reservoir"

# Or save standalone file
template.save("reservoir_template.json")
```

#### Track Types Explained

**1. Continuous Tracks** - For numeric log curves

Shows one or more curves with configurable scales, styles, fills, and markers.

```python
template.add_track(
    track_type="continuous",
    logs=[
        {
            "name": "GR",              # Property name
            "x_range": [0, 150],       # Scale limits [left, right]
            "color": "green",          # Line color
            "style": "solid",          # Line style (solid/dashed/dotted/none)
            "thickness": 1.5,          # Line width
            "alpha": 0.8               # Transparency (0-1)
        }
    ],
    title="Gamma Ray (API)",
    log_scale=False                    # Use logarithmic scale?
)
```

**2. Discrete Tracks** - For categorical data

Displays colored bands for facies, zones, or other categorical properties.

```python
template.add_track(
    track_type="discrete",
    logs=[{"name": "Facies"}],
    title="Lithofacies"
)
```

Colors come from the property's color mapping:
```python
facies = well.get_property('Facies')
facies.colors = {
    0: 'yellow',      # Sand
    1: 'gray',        # Shale
    2: 'lightblue'    # Limestone
}
```

**3. Depth Tracks** - Show depth axis

```python
template.add_track(
    track_type="depth",
    width=0.3,                         # Narrow width
    title="MD (m)"
)
```

### Styling Log Curves

#### Line Styles

```python
logs=[
    {"name": "GR", "style": "solid"},      # ─────
    {"name": "CALI", "style": "dashed"},   # ─ ─ ─
    {"name": "SP", "style": "dotted"},     # ·····
    {"name": "TEMP", "style": "dashdot"},  # ─·─·─
    {"name": "POINTS", "style": "none"}    # (markers only)
]
```

**Supported styles:** `"solid"` (`"-"`), `"dashed"` (`"--"`), `"dotted"` (`":"`), `"dashdot"` (`"-."`), `"none"` (`""`)

#### Colors

```python
logs=[
    {"name": "RHOB", "color": "red"},           # Color names
    {"name": "NPHI", "color": "#1f77b4"},       # Hex codes
    {"name": "GR", "color": (0.2, 0.5, 0.8)}    # RGB tuples
]
```

#### Thickness and Transparency

```python
logs=[
    {"name": "ILD", "thickness": 2.0, "alpha": 1.0},    # Thick, opaque
    {"name": "ILM", "thickness": 1.0, "alpha": 0.6}     # Thin, transparent
]
```

### Markers for Data Points

Display markers at each data point to show actual measurement locations. Useful for sparse data like core plugs, pressure tests, or sample points.

#### Basic Markers

```python
# Markers with line
logs=[{
    "name": "PERM",
    "x_range": [0.1, 1000],
    "color": "green",
    "style": "solid",           # Show connecting line
    "marker": "circle",         # Add circular markers
    "marker_size": 4,           # Marker size
    "marker_fill": "lightgreen" # Fill color (optional)
}]

# Markers only (no line)
logs=[{
    "name": "CORE_PHIE",
    "x_range": [0, 0.4],
    "color": "blue",
    "style": "none",            # No connecting line
    "marker": "diamond",        # Diamond markers
    "marker_size": 8,
    "marker_outline_color": "darkblue",
    "marker_fill": "yellow"
}]
```

#### Marker Types

**Common markers:**
- `"circle"` (○), `"square"` (□), `"diamond"` (◇)
- `"triangle_up"` (△), `"triangle_down"` (▽)
- `"star"` (★), `"plus"` (+), `"cross"` (×)

**All supported markers:** See [Style & Marker Reference](#style--marker-reference)

#### Marker Configuration

```python
logs=[{
    "name": "SAMPLE_POINTS",
    "marker": "circle",                    # Marker shape
    "marker_size": 6,                      # Size (default: 6)
    "marker_outline_color": "red",         # Edge color (defaults to line color)
    "marker_fill": "yellow",               # Fill color (optional, default: unfilled)
    "marker_interval": 5,                  # Show every 5th marker (default: 1)
}]
```

**Marker interval** is useful for dense data - showing every nth marker reduces clutter:
```python
# Show every 10th marker on a high-resolution log
{"name": "GR", "marker": "point", "marker_interval": 10}
```

### Fill Patterns

Fills highlight areas between curves or track edges. Useful for showing porosity, crossover, or lithology.

#### Solid Color Fill

Fill between a curve and a fixed value:

```python
template.add_track(
    track_type="continuous",
    logs=[{"name": "PHIE", "x_range": [0.45, 0], "color": "blue"}],
    fill={
        "left": "PHIE",         # Curve name
        "right": 0,             # Fixed value
        "color": "lightblue",
        "alpha": 0.5
    }
)
```

Fill between track edge and curve:

```python
fill={
    "left": "track_edge",       # Left edge of track
    "right": "GR",              # GR curve
    "color": "lightgreen",
    "alpha": 0.3
}
```

#### Colormap Fill

Create horizontal color bands where each depth interval is colored based on curve values:

```python
template.add_track(
    track_type="continuous",
    logs=[{"name": "GR", "x_range": [0, 150], "color": "black"}],
    fill={
        "left": "track_edge",
        "right": "GR",
        "colormap": "viridis",          # Colormap name
        "color_range": [20, 150],       # GR values map to colors
        "alpha": 0.7
    },
    title="Gamma Ray"
)
# Low GR (20) → dark purple, High GR (150) → bright yellow
```

**Popular colormaps:**
- `"viridis"` - Perceptually uniform (recommended)
- `"inferno"`, `"plasma"` - Dark to bright
- `"RdYlGn"` - Red-Yellow-Green (diverging)
- `"jet"` - Rainbow (not recommended for scientific use)

See [Colormap Reference](#colormap-reference) for all options.

#### Fill Between Two Curves

```python
template.add_track(
    track_type="continuous",
    logs=[
        {"name": "RHOB", "x_range": [1.95, 2.95], "color": "red"},
        {"name": "NPHI", "x_range": [0.45, -0.15], "color": "blue"}
    ],
    fill={
        "left": "RHOB",
        "right": "NPHI",
        "colormap": "RdYlGn",
        "colormap_curve": "NPHI",       # Use NPHI values for colors
        "color_range": [-0.15, 0.45],
        "alpha": 0.6
    },
    title="Density-Neutron Crossover"
)
```

#### Multiple Fills

Apply multiple fills to a single track (drawn in order):

```python
template.add_track(
    track_type="continuous",
    logs=[
        {"name": "PHIE", "x_range": [0.45, 0], "color": "blue"},
        {"name": "SW", "x_range": [0, 1], "color": "red"}
    ],
    fill=[
        # Fill 1: PHIE to zero
        {
            "left": "PHIE",
            "right": 0,
            "color": "lightblue",
            "alpha": 0.3
        },
        # Fill 2: SW to one
        {
            "left": "SW",
            "right": 1,
            "color": "lightcoral",
            "alpha": 0.3
        }
    ]
)
```

### Formation Tops

Add horizontal lines marking formation boundaries across all tracks:

```python
# Add tops to template (applies to all wells using this template)
template.add_tops(property_name='Zone')

# Or add tops to specific view (only this display)
view = well.WellView(template=template)
view.add_tops(property_name='Zone')
view.show()

# Or provide tops manually
view.add_tops(
    tops_dict={
        2850.0: 'Top Brent',
        3100.0: 'Top Statfjord',
        3400.0: 'Base Statfjord'
    },
    colors={
        2850.0: 'yellow',
        3100.0: 'orange',
        3400.0: 'brown'
    }
)
```

Tops can also be added to individual tracks:

```python
template.add_track(
    track_type="discrete",
    logs=[{"name": "Facies"}],
    tops={
        "name": "Zone",                    # Property containing tops
        "line_style": "--",                # Dashed lines
        "line_width": 2.0,                 # Line thickness
        "title_size": 9,                   # Label font size
        "title_weight": "bold",            # Font weight
        "title_orientation": "right",      # Label position (left/center/right)
        "line_offset": 0.0                 # Horizontal offset
    }
)
```

### Logarithmic Scales

Use logarithmic scales for resistivity, permeability, or other exponential data:

```python
# Track-level log scale (applies to all logs in track)
template.add_track(
    track_type="continuous",
    logs=[
        {"name": "ILD", "x_range": [0.2, 2000], "color": "red"},
        {"name": "ILM", "x_range": [0.2, 2000], "color": "green"}
    ],
    title="Resistivity",
    log_scale=True                         # Logarithmic x-axis
)

# Per-log scale override
template.add_track(
    track_type="continuous",
    logs=[
        {"name": "ILD", "x_range": [0.2, 2000], "color": "red"},      # Uses track log_scale
        {"name": "GR", "x_range": [0, 150], "scale": "linear", "color": "green"}  # Override
    ],
    log_scale=True                         # Default for track
)
```

### Using Templates

**Option 1: Pass template directly**
```python
view = well.WellView(depth_range=[2800, 3000], template=template)
view.show()
```

**Option 2: Store in manager (recommended for multi-well projects)**
```python
# Store template in manager (uses template.name automatically)
manager.add_template(template)

# Use by name in any well
view = well.WellView(depth_range=[2800, 3000], template="reservoir")
view.show()

# List all templates
print(manager.list_templates())  # ['reservoir', 'qc', 'basic']

# Templates save with projects
manager.save("my_project/")
# Creates: my_project/templates/reservoir.json
```

**Option 3: Load from file**
```python
template = Template.load("reservoir_template.json")
view = well.WellView(depth_range=[2800, 3000], template=template)
```

### Template Management

```python
# Retrieve template
template = manager.get_template("reservoir")

# List all templates
templates = manager.list_templates()

# View tracks in template
df = template.list_tracks()
print(df)
#    Index       Type           Logs         Title  Width
# 0      0 continuous          [GR]    Gamma Ray    1.0
# 1      1 continuous  [PHIE, SW]    Porosity    1.0
# 2      2      depth            []        Depth    0.3

# Edit track
template.edit_track(0, title="New Title")

# Remove track
template.remove_track(2)

# Add new track
template.add_track(track_type="continuous", logs=[{"name": "RT"}])

# Save changes
manager.add_template(template)          # Update in manager (uses template.name)
template.save("updated_template.json")  # Save to file
```

### Customization

#### Figure Settings

```python
view = well.WellView(
    depth_range=[2800, 3000],
    template="reservoir",
    figsize=(12, 10),              # Width x height in inches
    dpi=100                        # Resolution (default: 100)
)
```

#### Track Widths

Control relative track widths:

```python
template.add_track(track_type="continuous", logs=[...], width=1.0)   # Normal
template.add_track(track_type="discrete", logs=[...], width=1.5)     # 50% wider
template.add_track(track_type="depth", width=0.3)                    # Narrow
```

#### Export Options

```python
# PNG for presentations (raster)
view.save("well_log.png", dpi=300)

# PDF for publications (vector)
view.save("well_log.pdf")

# SVG for editing in Illustrator/Inkscape (vector)
view.save("well_log.svg")
```

### Complete Example

A comprehensive template showcasing all features:

```python
from well_log_toolkit import WellDataManager, Template

# Setup
manager = WellDataManager()
manager.load_las("well.las")
well = manager.well_36_7_5_A

# Create template
template = Template("comprehensive")

# Track 1: GR with colormap and markers
template.add_track(
    track_type="continuous",
    logs=[{
        "name": "GR",
        "x_range": [0, 150],
        "color": "black",
        "marker": "point",
        "marker_interval": 20  # Show every 20th sample
    }],
    fill={
        "left": "track_edge",
        "right": "GR",
        "colormap": "viridis",
        "color_range": [20, 150],
        "alpha": 0.7
    },
    title="Gamma Ray (API)"
)

# Track 2: Resistivity (log scale)
template.add_track(
    track_type="continuous",
    logs=[
        {"name": "ILD", "x_range": [0.2, 2000], "color": "red", "thickness": 1.5},
        {"name": "ILM", "x_range": [0.2, 2000], "color": "green"}
    ],
    title="Resistivity (ohmm)",
    log_scale=True
)

# Track 3: Density-Neutron with crossover
template.add_track(
    track_type="continuous",
    logs=[
        {"name": "RHOB", "x_range": [1.95, 2.95], "color": "red"},
        {"name": "NPHI", "x_range": [0.45, -0.15], "color": "blue"}
    ],
    fill={
        "left": "RHOB",
        "right": "NPHI",
        "colormap": "RdYlGn",
        "alpha": 0.5
    },
    title="Density-Neutron"
)

# Track 4: Porosity & Saturation
template.add_track(
    track_type="continuous",
    logs=[
        {"name": "PHIE", "x_range": [0.45, 0], "color": "blue"},
        {"name": "SW", "x_range": [0, 1], "color": "red"}
    ],
    fill={
        "left": "PHIE",
        "right": 0,
        "color": "lightblue",
        "alpha": 0.5
    },
    title="PHIE & SW"
)

# Track 5: Core data (markers only, no lines)
template.add_track(
    track_type="continuous",
    logs=[{
        "name": "CorePHIE",
        "x_range": [0, 0.4],
        "color": "darkblue",
        "style": "none",           # No connecting line
        "marker": "diamond",
        "marker_size": 8,
        "marker_outline_color": "darkblue",
        "marker_fill": "yellow"
    }],
    title="Core Porosity"
)

# Track 6: Facies with tops
template.add_track(
    track_type="discrete",
    logs=[{"name": "Facies"}],
    title="Lithofacies"
)

# Track 7: Depth
template.add_track(track_type="depth", width=0.3, title="MD (m)")

# Add formation tops spanning all tracks
template.add_tops(property_name='Zone')

# Add to project and display
manager.add_template(template)
view = well.WellView(depth_range=[2800, 3200], template="comprehensive")
view.save("comprehensive_log.png", dpi=300)
```

---

## Crossplot & Regression Guide

Create beautiful, publication-quality crossplots for petrophysical analysis with sophisticated color/size/shape mapping and built-in regression analysis.

### Quick Start

```python
from well_log_toolkit import WellDataManager

manager = WellDataManager()
manager.load_las("well.las")
well = manager.well_36_7_5_A

# Simple crossplot
plot = well.Crossplot(x="RHOB", y="NPHI")
plot.show()
```

That's it! One line to create a scatter plot from any two properties.

### Basic Crossplots

#### Single Well Analysis

```python
# Density vs Neutron Porosity
plot = well.Crossplot(
    x="RHOB",
    y="NPHI",
    title="Density-Neutron Crossplot"
)
plot.show()

# Save high-resolution image
plot.save("density_neutron.png", dpi=300)
```

#### Multi-Well Comparison

Compare multiple wells on the same plot:

```python
# All wells with different markers
plot = manager.Crossplot(
    x="PHIE",
    y="SW",
    shape="well",  # Different marker shape per well
    title="Multi-Well Porosity vs Saturation"
)
plot.show()

# Specific wells only
plot = manager.Crossplot(
    x="RHOB",
    y="NPHI",
    wells=["Well_A", "Well_B", "Well_C"],
    shape="well"
)
plot.show()
```

### Advanced Mapping

#### Color by Property or Depth

Visualize a third dimension using color:

```python
# Color by depth
plot = well.Crossplot(
    x="PHIE",
    y="SW",
    color="depth",
    colortemplate="viridis",
    color_range=[2000, 2500],  # Depth range in meters
    title="Porosity vs SW (colored by depth)"
)
plot.show()

# Color by shale volume
plot = well.Crossplot(
    x="PHIE",
    y="PERM",
    color="VSH",
    colortemplate="RdYlGn_r",  # Red=high shale, Green=low shale
    title="Porosity-Permeability (colored by VSH)"
)
plot.show()
```

**Available colormaps:** `"viridis"`, `"plasma"`, `"coolwarm"`, `"RdYlGn"`, `"jet"`, and 100+ more matplotlib colormaps.

#### Size by Property

Make marker size represent a fourth dimension:

```python
plot = well.Crossplot(
    x="PHIE",
    y="SW",
    size="PERM",              # Bigger markers = higher permeability
    size_range=(20, 200),     # Min/max marker sizes
    color="depth",
    colortemplate="viridis",
    title="Porosity vs SW (sized by PERM)"
)
plot.show()
```

#### Shape by Category

Use different marker shapes for different groups:

```python
# Different shapes for different facies
plot = well.Crossplot(
    x="PHIE",
    y="PERM",
    shape="Facies",           # Different marker per facies type
    color="depth",
    title="Porosity-Permeability by Facies"
)
plot.show()

# Multi-well: different shapes per well
plot = manager.Crossplot(
    x="PHIE",
    y="SW",
    shape="well",             # Circle, square, triangle, etc.
    color="VSH",
    size="PERM"
)
plot.show()
```

#### All Features Combined

Combine color, size, and shape for comprehensive visualization:

```python
plot = manager.Crossplot(
    x="PHIE",
    y="SW",
    wells=["Well_A", "Well_B"],    # Specific wells
    shape="well",                   # Different marker per well
    color="depth",                  # Color by depth
    size="PERM",                    # Size by permeability
    colortemplate="viridis",
    color_range=[2000, 2500],
    size_range=(30, 200),
    title="Multi-Dimensional Analysis",
    figsize=(12, 10),
    dpi=150
)
plot.show()
```

#### Multi-Layer Crossplots

Combine different data types (Core vs Sidewall, different property pairs) in a single plot with automatic shape/color encoding:

```python
# Compare Core and Sidewall data with regression by well
plot = manager.Crossplot(
    layers={
        "Core": ['CorePor', 'CorePerm'],
        "Sidewall": ["SidewallPor", "SidewallPerm"]
    },
    color="Formation",              # Color by formation
    shape="NetSand",                # Shape by net sand flag
    regression_by_color="exponential-polynomial",  # Separate trend per formation
    y_log=True,                     # Log scale for permeability
    title="Core vs Sidewall Analysis"
)
plot.show()

# Simpler version - automatic defaults
manager.Crossplot(
    layers={
        "Core": ['CorePor', 'CorePerm'],
        "Sidewall": ["SidewallPor", "SidewallPerm"]
    },
    y_log=True
).show()
# Automatically uses shape="label" (different markers per layer)
# and color="well" (different colors per well)
```

**How it works:**

- `layers` dict maps labels to [x, y] property pairs
- Each layer gets combined in one plot with unified axes
- Shape defaults to `"label"` (Core gets circles, Sidewall gets squares)
- Color defaults to `"well"` for multi-well visualization
- Perfect for comparing different measurement types (Core plugs vs Formation tests)

### Logarithmic Scales

Perfect for permeability and resistivity data:

```python
# Log scale on x-axis (permeability)
plot = well.Crossplot(
    x="PERM",
    y="PHIE",
    x_log=True,
    title="Porosity-Permeability (log scale)"
)
plot.show()

# Log-log plot
plot = well.Crossplot(
    x="PERM",
    y="Pressure",
    x_log=True,
    y_log=True,
    title="Log-Log Analysis"
)
plot.show()
```

### Depth Filtering

Focus on specific intervals:

```python
# Reservoir zone only
plot = well.Crossplot(
    x="PHIE",
    y="SW",
    depth_range=(2000, 2500),
    color="VSH",
    title="Reservoir Zone Analysis (2000-2500m)"
)
plot.show()
```

### Regression Analysis

Add trend lines to identify relationships between properties.

#### Linear Regression

```python
plot = well.Crossplot(x="RHOB", y="NPHI", title="Density-Neutron")

# Add linear regression
plot.add_regression("linear", line_color="red", line_width=2)
plot.show()

# Access regression results
reg = plot.regressions["linear"]
print(reg.equation())      # y = -0.2956x + 0.9305
print(f"R² = {reg.r_squared:.4f}")  # R² = 0.8147
print(f"RMSE = {reg.rmse:.4f}")     # RMSE = 0.0208
```

#### Multiple Regression Types

Compare different regression models:

```python
plot = well.Crossplot(x="PHIE", y="SW", title="Porosity vs Saturation")

# Add multiple regressions
plot.add_regression("linear", line_color="red")
plot.add_regression("polynomial", degree=2, line_color="blue")
plot.add_regression("exponential", line_color="green")

plot.show()

# Compare R² values
for name, reg in plot.regressions.items():
    print(f"{name}: R² = {reg.r_squared:.4f}")
# linear: R² = 0.0144
# polynomial: R² = 0.0155
# exponential: R² = 0.0201  ← Best fit
```

#### Available Regression Types

| Type | Equation | Use Case | Example |
|------|----------|----------|---------|
| `"linear"` | y = ax + b | Straight trends | Density-Porosity |
| `"polynomial"` | y = aₙxⁿ + ... + a₁x + a₀ | Curved relationships | Sonic-Porosity |
| `"exponential"` | y = ae^(bx) | Exponential growth | Production decline |
| `"logarithmic"` | y = a·ln(x) + b | Diminishing returns | Time-dependent |
| `"power"` | y = ax^b | Power law | Porosity-Permeability |

#### Polynomial Regression

Fit higher-order polynomials for curved relationships:

```python
plot = well.Crossplot(x="DT", y="PHIE")

# Quadratic (degree 2)
plot.add_regression("polynomial", degree=2, line_color="blue")

# Cubic (degree 3)
plot.add_regression("polynomial", degree=3, line_color="green", name="cubic")

plot.show()
```

#### Regression Customization

Control regression line appearance:

```python
plot.add_regression(
    "linear",
    name="best_fit",           # Custom name
    line_color="red",           # Line color
    line_width=2,               # Line thickness
    line_style="--",            # Dashed: "--", dotted: ":", solid: "-"
    line_alpha=0.8,             # Transparency (0-1)
    show_equation=True,         # Show equation in legend
    show_r2=True                # Show R² value
)
```

#### Using Regression for Predictions

Extract regression objects and use them for calculations:

```python
plot = well.Crossplot(x="RHOB", y="NPHI")
plot.add_regression("linear")

# Get regression object
reg = plot.regressions["linear"]

# Predict values
density_values = [2.3, 2.4, 2.5, 2.6]
predicted_nphi = reg(density_values)
print(predicted_nphi)  # [0.249, 0.220, 0.191, 0.161]

# Or use predict method
predicted_nphi = reg.predict(density_values)

# Get statistics
print(f"Equation: {reg.equation()}")
print(f"R²: {reg.r_squared:.4f}")
print(f"RMSE: {reg.rmse:.4f}")
```

### Standalone Regression Classes

Use regression classes independently for data analysis:

```python
from well_log_toolkit import LinearRegression, PolynomialRegression
import numpy as np

# Prepare data
x_data = np.array([2.2, 2.3, 2.4, 2.5, 2.6])
y_data = np.array([0.28, 0.25, 0.22, 0.19, 0.16])

# Fit linear regression
reg = LinearRegression()
reg.fit(x_data, y_data)

# Get results
print(reg.equation())           # y = -0.3000x + 0.9400
print(f"R² = {reg.r_squared}")  # R² = 1.0000
print(f"RMSE = {reg.rmse}")     # RMSE = 0.0000

# Make predictions
new_densities = [2.35, 2.45, 2.55]
predicted = reg(new_densities)
print(predicted)  # [0.235, 0.205, 0.175]

# Try polynomial
poly = PolynomialRegression(degree=2)
poly.fit(x_data, y_data)
print(poly.equation())
```

#### All Regression Classes

```python
from well_log_toolkit import (
    LinearRegression,          # y = ax + b
    PolynomialRegression,      # y = aₙxⁿ + ... + a₀
    ExponentialRegression,     # y = ae^(bx)
    LogarithmicRegression,     # y = a·ln(x) + b
    PowerRegression            # y = ax^b
)

# Each has the same interface
reg = LinearRegression()
reg.fit(x, y)
y_pred = reg.predict(x_new)
print(reg.equation())
print(reg.r_squared)
print(reg.rmse)
```

### Customization Options

Fine-tune your crossplot appearance:

```python
plot = well.Crossplot(
    x="RHOB",
    y="NPHI",
    # Plot settings
    title="Custom Crossplot",
    xlabel="Bulk Density (g/cc)",
    ylabel="Neutron Porosity (v/v)",
    figsize=(12, 10),           # Figure size (width, height)
    dpi=150,                    # Resolution

    # Marker settings
    marker="D",                 # Diamond markers
    marker_size=80,             # Larger markers
    marker_alpha=0.7,           # 70% opaque
    edge_color="darkblue",      # Marker outline color
    edge_width=1.5,             # Outline thickness

    # Grid settings
    grid=True,
    grid_alpha=0.3,             # Subtle grid

    # Display options
    show_colorbar=True,         # Show colorbar
    show_legend=True            # Show legend
)
plot.show()
```

**Marker styles:** `"o"` (circle), `"s"` (square), `"^"` (triangle), `"D"` (diamond), `"v"` (inverted triangle), `"*"` (star), `"+"` (plus), `"x"` (cross)

### Practical Examples

#### Porosity-Permeability Analysis

```python
# Classic log-scale relationship
plot = well.Crossplot(
    x="PHIE",
    y="PERM",
    y_log=True,                 # Log scale for permeability
    color="depth",
    colortemplate="viridis",
    title="Porosity-Permeability Transform"
)

# Add power law regression (typical for poro-perm)
plot.add_regression("power", line_color="red", line_width=2)
plot.show()

# Use regression for permeability prediction
power_reg = plot.regressions["power"]
print(power_reg.equation())    # y = 2.5*x^3.2

# Predict permeability from porosity
porosities = [0.10, 0.15, 0.20, 0.25, 0.30]
perms = power_reg(porosities)
print(perms)  # [0.003, 0.025, 0.100, 0.275, 0.562] mD
```

#### Reservoir Quality Classification

```p