6 · End-to-end Parkfield walkthrough

Phases 0–6 in a single run_workflow call

Step goal Run the entire six-phase pipeline in one call and read the per-phase summary, the diagnostic figure, and the posterior-predictive whiteness check that closes the Bayesian loop. This is the page to copy when you bring your own data.

Code
import sys, numpy as np, matplotlib.pyplot as plt
sys.path.insert(0, ".")
from _synth import make_synthetic, build_parkfield_site, set_style, C
from codameter import run_workflow
set_style()

dvv, forcings, eq_times, truth, comp = make_synthetic()
site = build_parkfield_site()
result = run_workflow(dvv, forcings, site, earthquake_times=eq_times)
print(result.summary())
=== codameter result for site 'parkfield_synthetic' ===

Phase 0  Data:    n=3650 samples, outliers=0
Phase 1  Kernel:  fc=1.04 Hz -> peak depth = 385 m  (mu=3.2 GPa, K=9.5 GPa)
Phase 2  Coupling: Pe=213.01 -> safe; escalate=False
Phase 3  Design:  forcings=['hydrological', 'thermoelastic', 'damage'], n_par=4
           Coupling likelihood: low (score=0.32) — linear model probably acceptable, but note uncertainty
           Model:   dv/v(t) = a0 + p(p1_dGWL)*f_p1_dGWL(t) + p(p2_T)*f_p2_T(t) + p(s_eq_126144000)*f_s_eq_126144000(t) + eps(t)
Phase 4  Fit:     chi2_red=1.01, rank=4/4
           a0                     = -6.831e-06 +/- 3.52e-06
           p1_dGWL                = -3.000e-03 +/- 4.23e-08
           p2_T                   = +7.934e-05 +/- 4.90e-07
           s_eq_126144000         = -2.043e-03 +/- 1.83e-05
Phase 5  Anomaly: whiteness p=0.276, transients=0
Staged-fit decision trail:
  - Stage A (hydrological, thermoelastic, damage): chi2_red = 1.01, AIC = -64224.6
  - Residual patterns: storm_band=no, seasonal=no, low_freq_drift=no
  - Stage B skipped — no residual pattern triggered an optional term. no storm-band residual structure
Phase 6  Interp:  d(dv/v)/dp = -3.06e-07 +/- 4.31e-12 1/Pa
           mu_prime  = +1938 +/- 0

The six-panel diagnostic

result.plot_phases() renders the canonical diagnostic the package ships — data, fit, residuals, and phase summaries in one figure.

Code
fig = result.plot_phases()
fig.set_size_inches(10, 7)
plt.show()
Figure 1: The codameter six-panel diagnostic for the synthetic Parkfield run, generated live by result.plot_phases().

Closing the loop with a posterior predictive check

Phase 5 runs the Ljung–Box whiteness test (Ljung–Box) on the standardised residuals. A large \(p\)-value means the residuals are white — no evidence of unmodelled physics — and the decoupled model is adequate. A small one would send us back up the iterative loop to re-examine coupling or the velocity model.

Code
a = result.phase5.report
print(f"Ljung–Box Q          : {a.whiteness_q:.2f}  ({a.whiteness_lags} lags)")
print(f"whiteness p-value    : {a.whiteness_pvalue:.3f}")
print(f"passes whiteness?    : {a.passes_whiteness}")
print(f"transients detected  : {a.n_transients}")
print(f"residual std         : {a.residual_std:.2e}")
Ljung–Box Q          : 23.27  (20 lags)
whiteness p-value    : 0.276
passes whiteness?    : True
transients detected  : 0
residual std         : 1.51e-04
Code
resid = result.phase4.fit.residuals
r = np.asarray(resid)
r = r[np.isfinite(r)]
r = r - r.mean()
nlags = 40
ac = np.array([1.0] + [np.corrcoef(r[:-k], r[k:])[0, 1] for k in range(1, nlags+1)])
band = 2.0 / np.sqrt(len(r))
fig, ax = plt.subplots(figsize=(9, 3))
ax.axhspan(-band, band, color="0.85", label="±2/√N white-noise band")
ax.vlines(range(len(ac)), 0, ac, color=C["accent"], lw=1.5)
ax.axhline(0, color="k", lw=0.6)
ax.set(xlabel="lag", ylabel="autocorrelation",
       title=f"Ljung–Box p = {a.whiteness_pvalue:.3f} → residuals white")
ax.legend(fontsize=8, frameon=False)
plt.show()
Figure 2: Residual autocorrelation. Bars inside the ±2/√N band (grey) are consistent with white noise — the posterior predictive check passes, so the linear-Gaussian inference stands for this site.

Bringing your own data

The exact same call works on real data — supply a dvv DataFrame (with a dvv_err column), a forcings dict, and a Site from your YAML:

from codameter import run_workflow, load_site
from codameter.data import load_dvv, load_timeseries

site = load_site("examples/configs/parkfield.yaml")
dvv  = load_dvv("my_dvv.parquet")          # needs columns dvv, dvv_err
forc = {
    "temperature":   load_timeseries("T.csv"),
    "precipitation": load_timeseries("P.csv"),
}
result = run_workflow(dvv, forc, site)
result.export("runs/my_site/")             # summary, JSON, params, residuals, figure

Run codameter data-check first (see step 1) to learn what your file can honestly support.


The whole arc, in one sentence We started from completely decoupled forward models, assembled them into a linear-superposition inversion with an exact Gaussian posterior, used the coupling diagnostics to decide that posterior is trustworthy here (and to know when it would not be), and propagated it through the bridge relation to stress and water-table change — with intervals at every step. That closed, uncertainty-aware loop is the contribution.

Back to top