Archiving and analyzing runs

The micropurc.reporting package archives every estimation run into its own auto-numbered directory — runs/mymodel, runs/mymodel~00, runs/mymodel~01, … — so results are never overwritten (the naming convention follows biogeme). Each archive holds the full configuration, a dataset snapshot, machine/compute info, and an HTML report: everything needed to know which data and config produced these numbers and to reproduce them.

Archiving a run

The snippets on this page continue from An end-to-end pipeline: net, scale_m, y, origins, dests, est_cfg, and the fitted result are the objects built there.

import scipy.sparse as sp
from micropurc.config import ActiveSetConfig, ProjectionConfig
from micropurc.reporting import RunWriter

writer = RunWriter(
    "runs", "sim_grid",
    config={
        "estimation": est_cfg,             # dataclasses serialize automatically
        "active_set": ActiveSetConfig(),
        "projection": ProjectionConfig(),
        "seed": 1,
    },
)

# Snapshot the inputs. Small inputs are copied in full (compressed);
# above ``max_full_bytes`` they are stored as SHA-256 references so a
# future load can verify it reads identical data.
writer.snapshot_data(
    edges=net.edges, Z=net.Z, A=sp.csc_matrix(net.A), scale_m=scale_m,
    y=sp.csr_matrix(y), origins=origins, dests=dests,
)

writer.finalize(result={
    "beta_hat": result["beta_hat"],
    "se_hat": result.get("se_hat"),
    "converged": result["diagnostics"]["converged"],
    "Q_history": result["diagnostics"].get("Q_history"),
})

finalize also records the compute profile: wall time, CPU time, peak resident memory, and core count. The archive’s environment.json captures the host, platform, package versions, and the git commit of the code.

Loading a run and analyzing the estimates

from micropurc.reporting import load_run, render_fit_report

run = load_run("runs/sim_grid")
run.beta_hat                      # point estimates
run.config["estimation"]          # exact config that produced them

# Forward-solve at beta_hat for selected OD pairs (or trips):
od_pair = (int(origins[0]), int(dests[0]))
flows = run.predicted_flow(od_pairs=[od_pair])
flows = run.predicted_flow(trips=range(10))

# The links that carry flow for one OD pair, identified with the same
# dual-slack KKT rule the estimator uses:
sub = run.active_subnetwork(od_pair)
list(zip(sub["tail"], sub["head"], sub["flow"]))

# Predicted flow pattern vs the observed routes:
rows = run.compare_routes(trips=range(100))
rows[0]["flow_share_on_route"]    # 1.0 = all predicted mass on the route

# Human-readable report (parameters, convergence, provenance, compute):
render_fit_report(run, param_names=["b_time", "b_toll", "b_scenic"])

Batch studies

For Monte Carlo sweeps, write one record per replication and aggregate at the end; render_batch_report() renders the summary tables and pass/fail gates. Replace the body of the loop with one full simulate-then-estimate replication; record_exists makes an interrupted sweep resumable:

batch = RunWriter("runs", "mc_study", config={"n_reps": 3})
for rep in range(3):                   # hundreds, in a real study
    if batch.record_exists(f"rep{rep}"):
        continue                       # resumable by construction
    batch.add_record(f"rep{rep}", beta_hat=result["beta_hat"], seed=rep)
batch.finalize()