Examples

The examples/ directory in the source tree contains four runnable scripts. Each is self-contained and uses the synthetic-data generator so it can run on any machine without external files.

quick_start.py

A three-line tour of the package: load synthetic data, auto-detect events, run Bourdet / Horner / reservoir-parameter analysis on the longest buildup.

 1"""
 2quick_start.py — three-line tour of welltest-pta.
 3
 4This example uses synthetic data so it can run without any external file.
 5Replace the synthetic load with WellTest.from_file("yourfile.txt") for real
 6gauge data.
 7"""
 8
 9from welltest_pta import WellTest
10from welltest_pta.utils.synthetic import generate_synthetic_dst
11
12
13# ── 1. Load (here: synthetic; in practice: from a gauge file) ────────────
14df = generate_synthetic_dst(n_samples=10_000, sample_period_s=4.0)
15wt = WellTest.from_dataframe(df)
16
17# ── 2. Inspect the event catalogue ───────────────────────────────────────
18wt.print_summary()
19
20# ── 3. Per-event analysis on the longest buildup ─────────────────────────
21bu = wt.events.longest_buildup
22bu.print()
23
24print("\n>>> Bourdet log-derivative")
25dt, deriv = bu.bourdet(L=0.2)
26print(f"    {len(dt)} derivative points covering Δt = "
27      f"[{dt[0]:.4f}, {dt[-1]:.2f}] hr")
28
29print("\n>>> Horner extrapolation")
30h = bu.horner()
31print(f"    P*    = {h['p_star']:.1f} psi")
32print(f"    slope = {h['slope_m']:.2f} psi/cycle")
33print(f"    R²    = {h['r2']:.4f}")
34
35print("\n>>> Reservoir parameters (oilfield units)")
36params = bu.reservoir_params(
37    q=850, mu=0.45, B=1.18,
38    h=18, phi=0.12, ct=1.2e-5, rw=0.108,
39    method="horner",
40)
41print(f"    k    = {params['k']:.3f} mD")
42print(f"    kh   = {params['kh']:.1f} mD·ft")
43print(f"    skin = {params['skin']:+.3f}")
44
45print("\n>>> Flow-regime identification")
46for reg in bu.flow_regimes():
47    print(f"    {reg['regime']:<22s} slope={reg['slope_mean']:+.2f}  "
48          f"Δt = [{reg['dt_start']:.3f}, {reg['dt_end']:.3f}] hr")

manual_split.py

Demonstrates the workflow when the auto-detector’s CV score is marginal. After auto-detection, the user passes a list of (type, t_start, t_end) tuples to split_manual(), which rebuilds the EventCollection from explicit timestamps.

 1"""
 2manual_split.py — manual override of the auto-detector.
 3
 4Demonstrates the workflow when the cross-validation score is marginal:
 51. Run auto-detect first (required — sets up p_smooth / elapsed_hr columns).
 62. Inspect the catalogue and decide which events you want to override.
 73. Pass a list of (type, t_start, t_end) tuples to wt.split_manual().
 8"""
 9
10from welltest_pta import WellTest, EventDetectorConfig
11from welltest_pta.utils.synthetic import generate_synthetic_dst
12
13
14# ── 1. Synthetic test with 4 events (DD-BU-DD-BU) ────────────────────────
15df = generate_synthetic_dst(
16    n_samples=20_000,
17    sample_period_s=3.0,
18    sequence=[
19        ("DD", 0.5, 3300.0),   # short flow #1
20        ("BU", 1.0, 4490.0),   # short BU
21        ("DD", 1.0, 3000.0),   # main flow
22        ("BU", 8.0, 4495.0),   # extended BU
23    ],
24)
25
26# ── 2. Auto-detection first ──────────────────────────────────────────────
27wt = WellTest.from_dataframe(df, auto_detect=True)
28print("[AUTO]")
29wt.events.print()
30
31# ── 3. Suppose the auto-detector misclassified BU-1 as part of BU-2.
32#     We override with explicit timestamps.
33ts = df["timestamp"]
34
35# Find indices roughly bracketing the four events (the synthetic generator
36# places them in order with small inter-segment gaps).
37idx_dd1_a, idx_dd1_b = 4_500, 5_500
38idx_bu1_a, idx_bu1_b = 5_700, 7_500
39idx_dd2_a, idx_dd2_b = 7_700, 9_000
40idx_bu2_a, idx_bu2_b = 9_100, 18_500
41
42wt.split_manual([
43    ("DD", ts.iloc[idx_dd1_a], ts.iloc[idx_dd1_b]),
44    ("BU", ts.iloc[idx_bu1_a], ts.iloc[idx_bu1_b]),
45    ("DD", ts.iloc[idx_dd2_a], ts.iloc[idx_dd2_b]),
46    ("BU", ts.iloc[idx_bu2_a], ts.iloc[idx_bu2_b]),
47])
48
49print("\n[MANUAL OVERRIDE]")
50wt.events.print()
51
52# ── 4. Per-event analysis is now identical to the auto-detect case ─────
53bu_long = wt.events.longest_buildup
54print(f"\nLongest BU = {bu_long.event_id}, duration = {bu_long.duration_hr:.2f} hr")
55print(f"Preceding tp = {bu_long.preceding_dd_dur_hr:.3f} hr")
56
57h = bu_long.horner()
58print(f"Horner: P* = {h['p_star']:.1f} psi, m = {h['slope_m']:.2f}, R² = {h['r2']:.4f}")

deconvolution_demo.py

Multi-event deconvolution on a 6-event synthetic test. The recovered unit-rate response merges all DDs and BUs into one diagnostic plot.

 1"""
 2deconvolution_demo.py — multi-event deconvolution merging multiple buildups.
 3
 4This example shows how deconvolve() takes all DD/BU events from a single
 5test and merges them into one equivalent unit-rate response — the
 6"diagnostic master plot" of the entire test.
 7"""
 8
 9import matplotlib.pyplot as plt
10
11from welltest_pta import WellTest, deconvolve
12from welltest_pta.utils.synthetic import generate_synthetic_dst
13
14
15# ── 1. Multi-rate synthetic test with three buildups ─────────────────────
16df = generate_synthetic_dst(
17    n_samples=24_000,
18    sample_period_s=4.0,
19    sequence=[
20        ("DD", 0.4, 3500.0),
21        ("BU", 0.8, 4480.0),
22        ("DD", 0.8, 3100.0),
23        ("BU", 1.5, 4490.0),
24        ("DD", 1.0, 2900.0),
25        ("BU", 8.0, 4495.0),
26    ],
27)
28wt = WellTest.from_dataframe(df)
29wt.print_summary()
30
31# ── 2. Deconvolve ────────────────────────────────────────────────────────
32print("\n→ Running vSH04 deconvolution …")
33res = deconvolve(
34    wt.events,
35    default_q=850,                # STB/D for drawdowns
36    nu=1e-2,                      # regularisation
37    n_response_nodes=60,
38    fit_p_initial=True,
39)
40print(f"   converged = {res.converged}, iters = {res.iterations}")
41print(f"   p_initial = {res.p_initial:.1f} psi   ||r|| = {res.residual_norm:.2f} psi")
42
43# ── 3. Plot ──────────────────────────────────────────────────────────────
44fig, axes = plt.subplots(1, 2, figsize=(14, 5))
45
46# Left: observed pressure + reconstruction
47axes[0].plot(res.obs_time, res.obs_pressure, "o", ms=1.5,
48             color="#888888", label="Observed")
49axes[0].plot(res.obs_time, res.fit_pressure, "-", lw=1.0,
50             color="#1f77b4", label="Reconstructed")
51axes[0].set_xlabel("Elapsed time (hr)")
52axes[0].set_ylabel("Pressure (psi)")
53axes[0].set_title("Pressure: observed vs deconvolution reconstruction")
54axes[0].legend()
55axes[0].grid(alpha=0.3)
56
57# Right: log-log diagnostic of the recovered unit-rate response
58res.plot(ax=axes[1])
59
60plt.tight_layout()
61plt.savefig("deconvolution_demo.png", dpi=200, bbox_inches="tight")
62print("\n→ Saved deconvolution_demo.png")

full_workflow.py

End-to-end pipeline: load → cross-validate → per-event analytics → reservoir parameters → deconvolution → composite report → bulk export. The recommended starting point for a real analysis.

  1"""
  2full_workflow.py — end-to-end pipeline demo.
  3
  4Shows everything in sequence:
  5  load → cross-validate → manual override → per-event analytics →
  6  reservoir parameters → deconvolution → composite report → bulk export.
  7"""
  8
  9from pathlib import Path
 10import warnings
 11warnings.filterwarnings("ignore")
 12
 13import matplotlib.pyplot as plt
 14
 15from welltest_pta import WellTest, deconvolve, EventDetectorConfig
 16from welltest_pta.utils.synthetic import generate_synthetic_dst
 17
 18
 19OUTPUT_DIR = Path("welltest_output")
 20OUTPUT_DIR.mkdir(exist_ok=True)
 21
 22
 23# ─── 1. Load gauge data ──────────────────────────────────────────────────
 24print("\n[1/7]  Loading gauge data…")
 25df = generate_synthetic_dst(
 26    n_samples=20_000,
 27    sample_period_s=4.0,
 28    sequence=[
 29        ("DD", 0.5, 3300.0),
 30        ("BU", 1.2, 4485.0),
 31        ("DD", 1.0, 3000.0),
 32        ("BU", 8.0, 4495.0),
 33    ],
 34)
 35df.drop(columns="true_event").to_csv(OUTPUT_DIR / "synthetic_input.csv", index=False)
 36
 37
 38# ─── 2. WellTest construction with cross-validation ─────────────────────
 39print("\n[2/7]  Auto-detect + cross-validate…")
 40cfg = EventDetectorConfig(
 41    hampel_sigma=3.0,
 42    spike_percentile=95.0,
 43    min_pta_dp_psi=15.0,
 44    tail_trim_enabled=True,
 45)
 46wt = WellTest.from_dataframe(df, cfg=cfg)
 47wt.cross_validate(n_bootstrap=4)
 48
 49
 50# ─── 3. Print catalogue ──────────────────────────────────────────────────
 51print("\n[3/7]  Event catalogue")
 52wt.print_summary()
 53
 54
 55# ─── 4. Per-event analysis ───────────────────────────────────────────────
 56print("\n[4/7]  Per-event analytics")
 57for ev in wt.events:
 58    print(f"\n   {ev}")
 59    if ev.event_type != "buildup":
 60        continue
 61    h = ev.horner()
 62    print(f"     Horner P*  = {h['p_star']:.1f} psi   m = {h['slope_m']:.2f}  R² = {h['r2']:.4f}")
 63    m = ev.mdh()
 64    print(f"     MDH p_1hr  = {m['intercept_p1hr']:.1f} psi   m = {m['slope_m']:.2f}  R² = {m['r2']:.4f}")
 65    fr = ev.flow_regimes()
 66    if fr:
 67        names = ", ".join(r["regime"] for r in fr)
 68        print(f"     Regimes    : {names}")
 69
 70
 71# ─── 5. Reservoir parameters on the longest BU ──────────────────────────
 72print("\n[5/7]  Reservoir parameters (longest BU)")
 73bu = wt.events.longest_buildup
 74params = bu.reservoir_params(
 75    q=850, mu=0.45, B=1.18,
 76    h=18, phi=0.12, ct=1.2e-5, rw=0.108,
 77    method="horner",
 78)
 79for k, v in params.items():
 80    if v is None or (isinstance(v, float) and v != v):  # NaN check
 81        print(f"     {k:<8s} = (n/a)")
 82    elif isinstance(v, float):
 83        print(f"     {k:<8s} = {v:+.4f}")
 84    else:
 85        print(f"     {k:<8s} = {v}")
 86
 87
 88# ─── 6. Deconvolution ────────────────────────────────────────────────────
 89print("\n[6/7]  Multi-event deconvolution")
 90res = deconvolve(wt.events, default_q=850, nu=1e-2, n_response_nodes=50)
 91print(f"     converged = {res.converged}, ||r|| = {res.residual_norm:.2f} psi")
 92res.export(OUTPUT_DIR / "deconvolution.csv")
 93
 94
 95# ─── 7. Plots + bulk export ──────────────────────────────────────────────
 96print("\n[7/7]  Plots and bulk export")
 97fig = wt.plot_composite(out_path=OUTPUT_DIR / "composite_report.pdf")
 98plt.close(fig)
 99fig = bu.plot_loglog(); plt.savefig(OUTPUT_DIR / "longest_BU_loglog.png", dpi=180); plt.close(fig)
100fig = bu.plot_horner(); plt.savefig(OUTPUT_DIR / "longest_BU_horner.png", dpi=180); plt.close(fig)
101fig = res.plot(); plt.savefig(OUTPUT_DIR / "deconvolution.png", dpi=180); plt.close(fig)
102
103paths = wt.export_all(OUTPUT_DIR, prefix="full_workflow", per_event=True)
104print(f"\n   Wrote {len(paths)} files to {OUTPUT_DIR}/")
105print("\n✓ Pipeline complete.")

Running the examples

After installing the package in development mode:

pip install -e ".[dev]"
python examples/quick_start.py
python examples/manual_split.py
python examples/deconvolution_demo.py
python examples/full_workflow.py

For headless servers (no display) set the matplotlib backend:

MPLBACKEND=Agg python examples/full_workflow.py