Skip to main content

Petrophysical well log analysis with depth-weighted statistics, hierarchical filtering, and template-driven visualization

Project description

logSuite

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.

PyPI version Python CI 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

Learning Path

Quick Reference


Installation

pip install logsuite

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


1-Minute Tutorial

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

from logsuite 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 for a complete walkthrough.


5-Minute Quick Start

Step 1: Load Your Data

from logsuite 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

# 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

# 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

# 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

from logsuite 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

# 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 to understand the library's design patterns, or jump to 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")
# 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):

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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

from logsuite 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:

# 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

from logsuite 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.

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.

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

Colors come from the property's color mapping:

facies = well.get_property('Facies')
facies.colors = {
    0: 'yellow',      # Sand
    1: 'gray',        # Shale
    2: 'lightblue'    # Limestone
}

3. Depth Tracks - Show depth axis

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

Styling Log Curves

Line Styles

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

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

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

# 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

Marker Configuration

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:

# 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:

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:

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:

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 for all options.

Fill Between Two Curves

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):

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:

# 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:

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:

# 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

view = well.WellView(depth_range=[2800, 3000], template=template)
view.show()

Option 2: Store in manager (recommended for multi-well projects)

# 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

template = Template.load("reservoir_template.json")
view = well.WellView(depth_range=[2800, 3000], template=template)

Template Management

# 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

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:

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

# 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:

from logsuite 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

from logsuite 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

# 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:

# 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:

# 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:

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:

# 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:

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:

# 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:

# 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:

# 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

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:

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:

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:

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:

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:

from logsuite 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

from logsuite 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:

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

# 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

# Multi-well reservoir quality
plot = manager.Crossplot(
    x="PHIE",
    y="SW",
    shape="well",              # Different marker per well
    color="VSH",               # Color by shale volume
    size="PERM",               # Size by permeability
    colortemplate="RdYlGn_r",  # Red=shaly, Green=clean
    title="Reservoir Quality Classification"
)

# Add cutoff lines
plot.add_regression("linear", line_color="red", show_equation=False)
plot.show()

# Identify sweet spots: PHIE > 0.15 and SW < 0.4

Lithology Identification

# Density-Neutron crossplot for lithology
plot = well.Crossplot(
    x="RHOB",
    y="NPHI",
    color="GR",                # Color by gamma ray
    colortemplate="viridis",
    color_range=[0, 150],
    title="Density-Neutron Lithology Plot"
)

# Add lithology lines
plot.add_regression("linear", line_color="yellow", name="Sandstone")
plot.add_regression("polynomial", degree=2, line_color="gray", name="Shale")
plot.show()

Best Practices

  1. Choose appropriate scales: Use log scales for permeability, resistivity
  2. Color consistency: Specify color_range to keep colors consistent across plots
  3. Multiple regressions: Try different types and compare R² values
  4. Depth filtering: Focus on specific intervals with depth_range
  5. Save high-res: Use dpi=300 for publication-quality images

Quick Reference

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

# With color and size
plot = well.Crossplot(x="PHIE", y="SW", color="depth", size="PERM")
plot.show()

# Multi-well
plot = manager.Crossplot(x="PHIE", y="SW", shape="well")
plot.show()

# With regression
plot = well.Crossplot(x="RHOB", y="NPHI")
plot.add_regression("linear", line_color="red")
plot.show()

# Standalone regression
from logsuite import LinearRegression
reg = LinearRegression()
reg.fit(x, y)
predictions = reg([10, 20, 30])

For comprehensive examples and API details, see:


Style & Marker Reference

Line Styles

Style Name Code Example Usage
"solid" "-" ───── Default, primary curves
"dashed" "--" ─ ─ ─ Secondary curves
"dotted" ":" ····· Tertiary curves
"dashdot" "-." ─·─·─ Alternate curves
"none" "" (none) Markers only

Markers

Basic Shapes

Name Code Symbol Usage
"circle" "o" General purpose, most common
"square" "s" Grid data, regular samples
"diamond" "D" Special points, core data
"star" "*" Important points
"plus" "+" + Crosshairs, reference points
"cross" "x" × Outliers, rejected points

Triangles

Name Code Symbol Usage
"triangle_up" "^" Increasing trend
"triangle_down" "v" Decreasing trend
"triangle_left" "<" Directional indicators
"triangle_right" ">" Directional indicators

Special

Name Code Symbol Usage
"pentagon" "p" Alternative shape
"hexagon" "h" Honeycomb patterns
"point" "." · Dense data, minimal marker
"pixel" "," , Very dense data
"vline" `" "`
"hline" "_" Horizontal emphasis

Color Names

Basic colors: "red", "blue", "green", "yellow", "orange", "purple", "pink", "brown", "gray", "black", "white"

Light colors: "lightblue", "lightgreen", "lightcoral", "lightgray", "lightyellow"

Dark colors: "darkblue", "darkgreen", "darkred", "darkgray"

Advanced: Use hex codes ("#1f77b4") or RGB tuples ((0.2, 0.5, 0.8)) for precise colors.


Colormap Reference

Sequential (Light to Dark)

Perfect for showing magnitude or intensity:

Colormap Description Use Case
"viridis" Yellow-green-blue (perceptually uniform) Recommended default
"plasma" Purple-pink-yellow High contrast
"inferno" Black-purple-yellow Dark backgrounds
"magma" Black-purple-white Maximum contrast
"cividis" Blue-yellow (colorblind-safe) Accessibility

Diverging (Low-Mid-High)

Perfect for data with a meaningful center (e.g., 0, neutral point):

Colormap Description Use Case
"RdYlGn" Red-Yellow-Green Good/bad (e.g., quality)
"RdBu" Red-Blue Hot/cold, positive/negative
"PiYG" Pink-Yellow-Green Alternative diverging
"BrBG" Brown-Blue-Green Earth tones

Qualitative

For categorical data (use discrete tracks instead):

Colormap Description
"tab10" 10 distinct colors
"tab20" 20 distinct colors
"Paired" Paired colors

Classic (Not Recommended)

Colormap Issue
"jet" Not perceptually uniform, creates false boundaries
"rainbow" Similar issues to jet

💡 Recommendation: Use "viridis" for general purposes. Use "RdYlGn" for diverging data. Avoid "jet".


Advanced Topics

Formation Tops Setup

Formation tops create discrete zones that start at each top and extend to the next:

import pandas as pd

# Create tops DataFrame
tops_df = pd.DataFrame({
    'Well': ['12/3-4 A', '12/3-4 A', '12/3-4 A'],
    'Surface': ['Top_Brent', 'Top_Statfjord', 'Top_Cook'],
    'MD': [2850.0, 3100.0, 3400.0]
})

# Load tops
manager.load_tops(
    tops_df,
    property_name='Zone',      # Name for discrete property
    source_name='Tops',        # Source name
    well_col='Well',           # Column with well names
    discrete_col='Surface',    # Column with formation names
    depth_col='MD'             # Column with depths
)

# How it works:
# - Top_Brent applies from 2850m to 3100m
# - Top_Statfjord applies from 3100m to 3400m
# - Top_Cook applies from 3400m to bottom of log

Discrete Properties & Labels

# Create or modify discrete property
ntg = well.get_property('NTG_Flag')
ntg.type = 'discrete'
ntg.labels = {0: 'NonNet', 1: 'Net'}

# Use in filtering
stats = well.PHIE.filter('NTG_Flag').sums_avg()
# Returns: {'NonNet': {...}, 'Net': {...}}

# Add colors for visualization
ntg.colors = {0: 'gray', 1: 'yellow'}

Understanding Statistics

Each statistics group provides comprehensive information:

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

# Example output structure:
{
  'Top_Brent': {
    'mean': 0.182,              # Depth-weighted average
    'sum': 45.5,                # Sum (for flags: net thickness)
    'std_dev': 0.044,           # Standard deviation
    'percentile': {
      'p10': 0.09,              # 10th percentile (pessimistic)
      'p50': 0.18,              # Median
      'p90': 0.24               # 90th percentile (optimistic)
    },
    'range': {'min': 0.05, 'max': 0.28},
    'depth_range': {'min': 2850.0, 'max': 3100.0},
    'samples': 250,             # Number of valid measurements
    'thickness': 250.0,         # Interval thickness
    'gross_thickness': 555.0,   # Total across all zones
    'thickness_fraction': 0.45, # Fraction of total
    'calculation': 'weighted'   # Method used
  }
}

Export Options

To DataFrame:

# All properties (default: errors if depths don't match exactly)
df = well.data()

# Specific properties
df = well.data(include=['PHIE', 'SW', 'PERM'])

# Interpolate to common depth grid if depths don't align
df = well.data(merge_method='resample')

# Use labels for discrete properties
df = well.data(discrete_labels=True)

To LAS:

# Export all properties
well.export_to_las('output.las')

# Specific properties
well.export_to_las('output.las', include=['PHIE', 'SW'])

# Use original LAS as template (preserves headers)
well.export_to_las('output.las', use_template=True)

# Export each source separately
well.export_sources('output_folder/')
# Creates: Petrophysics.las, CoreData.las, computed.las

Managing Sources

# List sources
print(well.sources)  # ['Petrophysics', 'CoreData']

# Access through source
phie_log = well.Petrophysics.PHIE
phie_core = well.CoreData.CorePHIE

# Rename source
well.rename_source('CoreData', 'Core_Analysis')

# Remove source (deletes all properties)
well.remove_source('Core_Analysis')

Adding External Data

import pandas as pd

# Create DataFrame
external_df = pd.DataFrame({
    'DEPT': [2800, 2801, 2802],
    'CorePHIE': [0.20, 0.22, 0.19],
    'CorePERM': [150, 200, 120]
})

# Add to well
well.add_dataframe(
    external_df,
    source_name='CoreData',
    unit_mappings={'CorePHIE': 'v/v', 'CorePERM': 'mD'},
    type_mappings={'CorePHIE': 'continuous', 'CorePERM': 'continuous'}
)

Sampled Data (Core Plugs)

Core plugs are point samples requiring arithmetic (not depth-weighted) statistics:

# Load as sampled
manager.load_las('core_plugs.las', sampled=True)

# Or mark properties as sampled
well.CorePHIE.type = 'sampled'

# Statistics use arithmetic mean
stats = well.CorePHIE.filter('Zone').sums_avg()
# → {'calculation': 'arithmetic'} (each plug counts equally)

Managing Wells

# List wells
print(manager.wells)  # ['well_12_3_4_A', 'well_12_3_4_B']

# Access by name
well = manager.well_12_3_4_A              # Sanitized name (attribute)
well = manager.get_well('12/3-4 A')       # Original name
well = manager.get_well('12_3_4_A')       # Sanitized name
well = manager.get_well('well_12_3_4_A')  # With prefix

# Add well
well = manager.add_well('12/3-4 C')

# Remove well
manager.remove_well('12_3_4_A')

Property Inspection

# Print property (auto-clips large arrays)
print(well.PHIE)
# [PHIE] (1001 samples)
# depth: [2800.00, 2801.00, ..., 3800.00]
# values (v/v): [0.180, 0.185, ..., 0.210]

# Print filtered property
filtered = well.PHIE.filter('Zone')
print(filtered)
# [PHIE] (1001 samples)
# Filters: Zone: [Top_Brent, Top_Brent, ...]

# Print manager-level property
print(manager.PHIE)
# [PHIE] across 3 well(s):
# Well: well_12_3_4_A
# [PHIE] (1001 samples)
# ...

API Reference

Main Classes

from logsuite import WellDataManager, Well, Property, LasFile

WellDataManager - Manages multiple wells

  • load_las(filepath, sampled=False) - Load LAS file
  • load_tops(df, well_col, discrete_col, depth_col) - Load formation tops
  • add_well(name) - Add empty well
  • get_well(name) - Get well by name
  • remove_well(name) - Remove well
  • save(directory) - Save project
  • load(directory) - Load project
  • add_template(template) - Store template (uses template.name)
  • set_template(name, template) - Store template with custom name
  • get_template(name) - Retrieve template
  • list_templates() - List template names
  • Crossplot(x, y, wells=None, shape="well", ...) - Create multi-well crossplot

Well - Individual wellbore

  • get_property(name, source=None) - Get property
  • add_dataframe(df, source_name, ...) - Add external data
  • data(include=None, exclude=None) - Export to DataFrame
  • export_to_las(filepath, ...) - Export to LAS
  • export_sources(directory) - Export each source
  • rename_source(old, new) - Rename source
  • remove_source(name) - Remove source
  • WellView(depth_range=None, tops=None, template, ...) - Create log visualization
  • Crossplot(x, y, color=None, size=None, shape=None, ...) - Create crossplot

Property - Single measurement or computed value

  • filter(discrete_property) - Filter by discrete property
  • sums_avg(arithmetic=False) - Compute statistics
  • resample(reference_property) - Resample to new depth grid
  • Attributes: name, depth, values, unit, type, labels, colors

Visualization Classes

from logsuite import Template, WellView, Crossplot

Template - Display layout configuration

  • add_track(track_type, logs, fill, tops, ...) - Add track
  • add_tops(property_name, tops_dict, ...) - Add formation tops
  • edit_track(index, **kwargs) - Edit track
  • remove_track(index) - Remove track
  • get_track(index) - Get track config
  • list_tracks() - List all tracks
  • save(filepath) - Save to JSON
  • load(filepath) - Load from JSON (classmethod)
  • to_dict(), from_dict(data) - Dict conversion

WellView - Well log display

  • plot() - Create matplotlib figure
  • show() - Display in Jupyter
  • save(filepath, dpi) - Save to file
  • close() - Close figure
  • add_track(...) - Add temporary track
  • add_tops(...) - Add temporary tops

Crossplot - Scatter plot with regression analysis

  • plot() - Create matplotlib figure
  • show() - Display plot
  • save(filepath, dpi) - Save to file
  • close() - Close figure
  • add_regression(type, **kwargs) - Add regression line
  • remove_regression(name) - Remove regression
  • Attributes: regressions, fig, ax

Regression Classes

from logsuite import (
    LinearRegression,
    PolynomialRegression,
    ExponentialRegression,
    LogarithmicRegression,
    PowerRegression
)

All regression classes share the same interface:

  • fit(x, y) - Fit regression model to data
  • predict(x) - Predict y values for given x
  • __call__(x) - Alternative prediction syntax: reg([1, 2, 3])
  • equation() - Get equation string (e.g., "y = 2.5x + 1.3")
  • Attributes: r_squared, rmse, x_data, y_data

PolynomialRegression - Additional parameter:

  • __init__(degree=2) - Specify polynomial degree

Statistics Functions

from logsuite import compute_intervals, mean, sum, std, percentile

These are low-level functions used internally. Most users should use the high-level filtering API (property.filter().sums_avg()).

Exceptions

from logsuite import (
    DepthAlignmentError,
    PropertyNotFoundError,
    PropertyTypeError
)
  • DepthAlignmentError - Raised when combining properties with different depth grids
  • PropertyNotFoundError - Raised when accessing non-existent property
  • PropertyTypeError - Raised when property has wrong type (e.g., filtering by continuous property)

Common Patterns

Copy-paste examples for common tasks:

Load and Analyze

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

Chain Multiple Filters

stats = well.PHIE.filter('Zone').filter('Facies').filter('NTG_Flag').sums_avg()

Multi-Well Statistics

# P50 by zone across all wells
p50 = manager.PHIE.filter('Zone').percentile(50)

# All statistics
means = manager.PHIE.filter('Zone').mean()
stds = manager.PHIE.filter('Zone').std()

Create Computed Properties

well.HC_Volume = well.PHIE * (1 - well.SW)
well.Reservoir = (well.PHIE > 0.15) & (well.SW < 0.35)

Broadcast Across Wells

manager.PHIE_percent = manager.PHIE * 100
manager.Reservoir = (manager.PHIE > 0.15) & (manager.SW < 0.35)

Quick Visualization

# With depth range
view = well.WellView(depth_range=[2800, 3000])
view.show()

# Auto-calculate from tops
view = well.WellView(tops=['Top_Brent', 'Top_Statfjord'])
view.show()

Build Custom Template

template = Template("custom")
template.add_track(
    track_type="continuous",
    logs=[{"name": "GR", "x_range": [0, 150], "color": "green"}],
    title="Gamma Ray"
)
manager.add_template(template)  # Stored as "custom"
view = well.WellView(template="custom")
view.save("log.png", dpi=300)

Crossplots

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

# With color and regression
plot = well.Crossplot(x="PHIE", y="SW", color="depth")
plot.add_regression("linear", line_color="red")
plot.show()

# Multi-well
plot = manager.Crossplot(x="PHIE", y="SW", shape="well")
plot.show()

Regression Analysis

# With crossplot
plot = well.Crossplot(x="RHOB", y="NPHI")
plot.add_regression("linear")
reg = plot.regressions["linear"]
predictions = reg([2.3, 2.4, 2.5])

# Standalone
from logsuite import LinearRegression
reg = LinearRegression()
reg.fit(x_data, y_data)
print(reg.equation())
y_pred = reg(new_x_values)

Save and Load Projects

manager.save('project/')
manager = WellDataManager('project/')

Troubleshooting

DepthAlignmentError

Problem: Properties have different depth grids

result = well.PHIE + well.CorePHIE  # Error!

Solution: Explicitly resample

core_resampled = well.CorePHIE.resample(well.PHIE)
result = well.PHIE + core_resampled  # Works!

PropertyNotFoundError

Problem: Property doesn't exist

phie = well.PHIE_TOTAL  # Error if property doesn't exist

Solution: Check available properties

print(well.properties)  # List all
print(well.sources)     # Check sources

# Or handle gracefully
try:
    phie = well.get_property('PHIE_TOTAL')
except PropertyNotFoundError:
    phie = well.PHIE  # Use fallback

PropertyTypeError

Problem: Filtering by non-discrete property

stats = well.PHIE.filter('PERM').sums_avg()  # Error!

Solution: Mark as discrete

perm = well.get_property('PERM')
perm.type = 'discrete'
perm.labels = {0: 'Low', 1: 'Medium', 2: 'High'}
stats = well.PHIE.filter('PERM').sums_avg()  # Works!

Missing Statistics for Some Zones

Problem: No valid data in some zones

stats = well.PHIE.filter('Zone').sums_avg()
# Some zones missing if all PHIE values are NaN

Solution: Check raw data

print(well.PHIE.values)  # Look for NaN
print(well.Zone.values)  # Check distribution

# Filter NaN values
import numpy as np
valid_mask = ~np.isnan(well.PHIE.values)

Template Not Found

Problem: Template doesn't exist

view = well.WellView(template="missing")  # Error!

Solution: Check available templates

print(manager.list_templates())  # ['reservoir', 'qc']

# Or pass template directly
template = Template("custom")
view = well.WellView(template=template)

Visualization Not Showing

Problem: Display doesn't appear in Jupyter

view = well.WellView(template="reservoir")
# Nothing shows

Solution: Call show() explicitly

view = well.WellView(template="reservoir")
view.show()  # Required in Jupyter

Markers Not Appearing

Problem: Markers not visible in log display

logs=[{"name": "GR", "marker": "circle"}]
# No markers show

Solution: Check marker configuration

# Ensure marker size is visible
logs=[{"name": "GR", "marker": "circle", "marker_size": 6}]

# If line is very thick, markers might be hidden
logs=[{
    "name": "GR",
    "marker": "circle",
    "marker_size": 8,           # Larger markers
    "marker_outline_color": "red",  # Distinct color
    "marker_fill": "yellow"     # Filled markers stand out
}]

# For markers only, use style="none"
logs=[{
    "name": "CORE_PHIE",
    "style": "none",            # Remove line
    "marker": "diamond",
    "marker_size": 10
}]

Tops Parameter Error

Problem: No formation tops loaded

view = well.WellView(tops=['Top_Brent', 'Top_Statfjord'])
# ValueError: No formation tops have been loaded

Solution: Add tops to template or view first

# Option 1: Add tops to template
template = Template("reservoir")
template.add_tops(property_name='Zone')
view = well.WellView(tops=['Top_Brent', 'Top_Statfjord'], template=template)

# Option 2: Add tops to view
view = well.WellView(template=template)
view.add_tops(property_name='Zone')
# Note: Can't use tops parameter if tops aren't in template

# Option 3: Use depth_range instead
view = well.WellView(depth_range=[2800, 3000], template=template)

Problem: Specified tops not found

view = well.WellView(tops=['Top_Missing'])
# ValueError: Formation tops not found: ['Top_Missing']

Solution: Check available tops

# Load and check tops
manager.load_tops(tops_df, well_col='Well', discrete_col='Surface', depth_col='MD')

# Check what tops are available
zone = well.get_property('Zone')
print(zone.labels)  # {0: 'Top_Brent', 1: 'Top_Statfjord', ...}

# Use correct names
view = well.WellView(tops=['Top_Brent', 'Top_Statfjord'], template=template)

Performance

All operations use vectorized numpy for maximum speed:

  • 100M+ samples/second throughput
  • Typical well logs (1k-10k samples) process in < 1ms
  • Filtered statistics (2 filters, 10 wells): ~9ms
  • Manager-level operations optimized with property caching
  • I/O bottleneck eliminated with lazy loading

Requirements

  • Python >= 3.9
  • numpy >= 1.20.0
  • pandas >= 1.3.0
  • scipy >= 1.7.0
  • matplotlib >= 3.5.0

Contributing

Contributions welcome! Please submit a Pull Request.


License

MIT License


Need Help?

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

logsuite-0.2.0.tar.gz (269.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

logsuite-0.2.0-py3-none-any.whl (172.4 kB view details)

Uploaded Python 3

File details

Details for the file logsuite-0.2.0.tar.gz.

File metadata

  • Download URL: logsuite-0.2.0.tar.gz
  • Upload date:
  • Size: 269.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for logsuite-0.2.0.tar.gz
Algorithm Hash digest
SHA256 eac809cddf6294d448e0e3c8c2a07349158c40bece60c85e54d610a636f080e4
MD5 e7a7badde437b120d359a409b39afba9
BLAKE2b-256 8b631152faa09829f9fd00875b9cf85c5b5f88317e0a6a9bef26ac4ba0b916a3

See more details on using hashes here.

File details

Details for the file logsuite-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: logsuite-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 172.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for logsuite-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7b80692698a04b8254eee91cf4a841d1fa40f1bad06e4d692bfce64a87129b3d
MD5 9a7f1957ed2d7515ed70c004ae89c4c3
BLAKE2b-256 f2ec62e71ea8f10fcfd72ae83afe2b5d5eee141178cf89236944864af3732b55

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page