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