well-log strip plot
Petrophysics — GR, DEN+NEU, RDEP, CALI
Example from the compendium of canonical charts
Python Code
"""Petrophysics — well-log strip plot (GR, DEN+NEU, RDEP, CALI)."""
import tempfile
import os
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
LAS_URL = "https://raw.githubusercontent.com/andymcdgeo/Petrophysics-Python-Series/master/Data/15-9-19_SR_COMP.LAS"
NULL_VAL = -999.25
DECIMATE = 2000
def load_las(url):
import requests
import lasio
r = requests.get(url, verify=False, timeout=60)
r.raise_for_status()
with tempfile.NamedTemporaryFile(suffix=".las", delete=False) as f:
f.write(r.content)
tmp_path = f.name
try:
las = lasio.read(tmp_path)
df = las.df().reset_index()
df.columns = [c.strip().upper() for c in df.columns]
df.replace(NULL_VAL, np.nan, inplace=True)
return df
finally:
os.unlink(tmp_path)
def generate():
try:
print("fetching LAS file …")
df = load_las(LAS_URL)
print(f" loaded {len(df)} rows; columns: {list(df.columns)}")
# Identify depth column
dept_col = next((c for c in df.columns if c in ("DEPT", "DEPTH", "MD")), df.columns[0])
# Map expected curves
def find_col(candidates):
for c in candidates:
if c in df.columns:
return c
return None
gr_col = find_col(["GR", "GR_N"])
den_col = find_col(["DEN", "RHOB", "RHOZ"])
neu_col = find_col(["NEU", "NPHI", "TNPH"])
res_col = find_col(["RDEP", "ILD", "RT", "RD", "LLD"])
cal_col = find_col(["CALI", "CAL", "CALS"])
print(f" curves found — GR:{gr_col} DEN:{den_col} NEU:{neu_col} RES:{res_col} CALI:{cal_col}")
# Decimate
step = max(1, len(df) // DECIMATE)
df = df.iloc[::step].copy()
depth = df[dept_col]
except Exception as e:
print(f" LAS load failed ({e}); using synthetic fallback")
np.random.seed(7)
n = DECIMATE
depth = np.linspace(1500, 3000, n)
df = pd.DataFrame({
"DEPT": depth,
"GR": 30 + 90 * np.abs(np.sin(depth / 120)) + np.random.normal(0, 5, n),
"RHOB": 2.1 + 0.5 * np.random.rand(n),
"NPHI": 0.05 + 0.35 * np.random.rand(n),
"ILD": np.exp(1 + 3 * np.random.rand(n)),
"CALI": 8.5 + 2 * np.random.rand(n),
})
dept_col = "DEPT"
gr_col, den_col, neu_col, res_col, cal_col = "GR", "RHOB", "NPHI", "ILD", "CALI"
depth = df[dept_col]
fig = make_subplots(
rows=1, cols=5,
shared_yaxes=True,
column_widths=[0.22, 0.20, 0.20, 0.22, 0.16],
horizontal_spacing=0.02,
subplot_titles=["GR", "RHOB (Density)", "NPHI (Neutron)", "Resistivity", "Caliper"],
)
# Track 1: GR
if gr_col:
fig.add_trace(go.Scatter(
x=df[gr_col], y=depth, mode="lines",
line=dict(color=GREEN, width=1),
name="GR (API)", showlegend=True,
hovertemplate="GR: %{x:.1f} API<br>Depth: %{y:.1f} m<extra></extra>",
), row=1, col=1)
# Track 2: Density (RHOB) — own track, 1.95–2.95 g/cc range
if den_col:
fig.add_trace(go.Scatter(
x=df[den_col], y=depth, mode="lines",
line=dict(color="#c0392b", width=1.2),
name="RHOB (g/cc)", showlegend=True,
hovertemplate="RHOB: %{x:.3f} g/cc<br>Depth: %{y:.1f}<extra></extra>",
), row=1, col=2)
# Track 3: Neutron porosity (NPHI) — own track
if neu_col:
fig.add_trace(go.Scatter(
x=df[neu_col], y=depth, mode="lines",
line=dict(color="#2980b9", width=1.2),
name="NPHI (v/v)", showlegend=True,
hovertemplate="NPHI: %{x:.3f}<br>Depth: %{y:.1f}<extra></extra>",
), row=1, col=3)
# Track 4: Resistivity (log-x)
if res_col:
fig.add_trace(go.Scatter(
x=df[res_col], y=depth, mode="lines",
line=dict(color=MUTED, width=1),
name="Resistivity (Ω·m)", showlegend=True,
hovertemplate="Res: %{x:.2f} Ω·m<br>Depth: %{y:.1f}<extra></extra>",
), row=1, col=4)
# Track 5: Caliper
if cal_col:
fig.add_trace(go.Scatter(
x=df[cal_col], y=depth, mode="lines",
line=dict(color=TEAL, width=1),
name="Caliper (in)", showlegend=True,
hovertemplate="CALI: %{x:.2f} in<br>Depth: %{y:.1f}<extra></extra>",
), row=1, col=5)
d_min, d_max = float(depth.min()), float(depth.max())
fig.update_layout(
yaxis=dict(
title="Depth (m)", autorange="reversed",
range=[d_max, d_min],
),
xaxis4=dict(type="log"),
legend=dict(x=1.01, y=1, xanchor="left"),
margin=dict(t=50, b=50, l=70, r=60),
height=700,
)
return fig
if __name__ == "__main__":
generate()
Made with Plotly