From 9f21ce6869ce9c8b5b327667685e59c377dd219a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=B7=E6=B1=9F=E7=9D=BF?= Date: Tue, 11 Aug 2026 10:59:36 -0500 Subject: [PATCH] Add resume and iteration-log blog posts --- PLAN.md | 8 +- STATUS.md | 24 +- blog/iteration-log/figures/classic-loop.png | Bin 0 -> 24420 bytes .../figures/iteration-log-resume.png | Bin 0 -> 44267 bytes blog/iteration-log/index.qmd | 304 ++++++++++++++++++ blog/resume-feature/index.qmd | 271 ++++++++++++++++ 6 files changed, 594 insertions(+), 13 deletions(-) create mode 100644 blog/iteration-log/figures/classic-loop.png create mode 100644 blog/iteration-log/figures/iteration-log-resume.png create mode 100644 blog/iteration-log/index.qmd create mode 100644 blog/resume-feature/index.qmd diff --git a/PLAN.md b/PLAN.md index 3a77b99..696a778 100644 --- a/PLAN.md +++ b/PLAN.md @@ -106,6 +106,9 @@ replace project documentation. living-article revisions to “Why Add Q to MC?”. - [x] Verify last-revised dates, authors, local links, code, mathematics, and all 48 images in a complete local render and browser review. +- [x] Add the two May 2026 QMCPy documentation blogs on iteration logs and + resuming integrations, retaining links to their executable notebooks. +- [x] Expand the current website archive from the historic 18 posts to 20 posts. - [ ] Define and verify redirects or canonical handling for legacy URLs. - [ ] Publish the complete archive only after collaborator review. @@ -174,5 +177,6 @@ replace project documentation. with QMCSoftware collaborators. - [ ] Record requested changes and resolve launch-blocking issues. - [ ] Agree on ownership and cadence for blog, news, and community updates. -- [x] Authorize and locally validate the 18-post blog migration. -- [ ] Approve the completed local archive before publishing it for review. +- [x] Authorize and locally validate the historic 18-post blog migration. +- [x] Authorize migration of the two newer resume and iteration-log blogs. +- [ ] Approve the completed 20-post archive before merging its review PR. diff --git a/STATUS.md b/STATUS.md index 048dbcd..898b38a 100644 --- a/STATUS.md +++ b/STATUS.md @@ -32,17 +32,19 @@ - [x] Added repository guidance, roadmap, ignore rules, and local instructions. - [x] Rendered the complete 13-page site and verified local link targets and rendered `CNAME` preservation. -- [x] Migrated the complete 18-post QMCPy blog archive into self-contained - Quarto posts while retaining the technical documentation and notebooks in - the QMCPy repository. +- [x] Migrated the complete historic 18-post QMCPy blog archive into + self-contained Quarto posts while retaining the technical documentation and + notebooks in the QMCPy repository. +- [x] Added the two newer QMCPy documentation blogs, “Stop Re-running” and + “How Much Accuracy Do You Need?”, bringing the current archive to 20 posts. - [x] Standardized blog chronology on one last-revised `date` per post, with no separate first-publication field, and retained the approved revised version of “Why Add Q to MC?”. -- [x] Preserved and published all 48 blog images in the local render, expanded - MkDocs code snippets, converted callouts and image groups, and normalized - legacy mathematical delimiters for Quarto. -- [x] Rendered all 30 site pages and completed local link, desktop, mobile, and - per-post browser checks for the 18-post archive. +- [x] Preserved all 48 historic blog images and added two result figures for + the newer iteration-log article; expanded MkDocs code snippets, converted + callouts and image groups, and normalized mathematical delimiters for Quarto. +- [x] Rendered all 32 site pages and completed local link, desktop, and mobile + checks for the 20-post archive. ## Underway @@ -54,8 +56,8 @@ ## Next recommended task -- [ ] Review and approve the completed local 18-post archive before any branch - is pushed or a pull request is opened. +- [ ] Review the two new posts in their draft pull request and approve the + completed 20-post archive before merge and deployment. ## Decisions and scope boundaries @@ -64,7 +66,7 @@ link to their respective repositories and documentation. - The site uses `qmcsoftware.org` as its canonical URL and custom domain; redirect behavior from the former domain still requires verification. -- The complete 18-post QMCPy blog archive is migrated locally; publishing it +- The complete 20-post QMCPy blog archive is migrated locally; publishing it remains approval-gated. - Blog cards and title blocks use each post's last-revised date; the archive is sorted newest revision first and does not carry a separate first-published diff --git a/blog/iteration-log/figures/classic-loop.png b/blog/iteration-log/figures/classic-loop.png new file mode 100644 index 0000000000000000000000000000000000000000..9232268dfe99fc4522fb4e7eb0c3c59b27378ddd GIT binary patch literal 24420 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 H<9g~(4^!N& literal 0 HcmV?d00001 diff --git a/blog/iteration-log/index.qmd b/blog/iteration-log/index.qmd new file mode 100644 index 0000000..adf5070 --- /dev/null +++ b/blog/iteration-log/index.qmd @@ -0,0 +1,304 @@ +--- +title: "Stop Re-running: Efficient Numerical Integration via Solver Log and Resumption" +author: "Sou-Cheng Choi" +date: 2026-05-04 +date-format: "MMMM D, YYYY" +description: "Comparing repeated tolerance sweeps with QMCPy's iteration-log and resume workflow." +categories: + - "QMCPy" + - "Performance" + - "Stopping Criteria" +image: figures/iteration-log-resume.png +--- + +This post compares a classic tolerance sweep with QMCPy's iteration-log and +resume workflow for high-dimensional numerical integration. + +The executable source is the +[`Iteration_Log_Tolerance_Demo.ipynb`](https://github.com/QMCSoftware/QMCSoftware/blob/develop/demos/demo_resume_data/Iteration_Log_Tolerance_Demo.ipynb) +notebook in the QMCPy repository. + +In high-dimensional integration, achieving high precision in the solution +estimate often requires solving the same problem across a wide range of +tolerances ($\varepsilon$). Traditionally, this meant running the entire +simulation multiple times, leading to prohibitive computational costs. This +demo shows how QMCPy's resume feature and internal solver logs can reduce +redundant computation while maintaining the requested accuracy. + +::: {.callout-note} +The runtimes below are empirical outputs saved in the source notebook and will +vary by machine and software environment. They are not theoretical performance +guarantees. The sample counts correspond to the stated methods, tolerances, and +random seed. +::: + +## Approach 1: Classic loop + +Following the setup in the +[`MCQMC2022_Article_Figures.ipynb`](https://github.com/QMCSoftware/QMCSoftware/blob/develop/demos/talk_paper_demos/MCQMC2022_Article_Figures/MCQMC2022_Article_Figures.ipynb) +notebook, we create two tolerance plots: + +1. Time versus tolerance. +2. Number of samples, $n$, versus tolerance. + +Both use log-log axes and compare the lattice results with an +$\mathcal{O}(\varepsilon^{-1})$ reference trend. + +This naive approach re-runs the solver for every target tolerance, so it +discards work completed at earlier tolerances. + +```python +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +from matplotlib.ticker import FuncFormatter +from time import perf_counter +import qmcpy as qp + +tol0, n_tol = 1e-3, 9 +tol = np.array([tol0 / (2**i) for i in range(n_tol)]) + +def run_lattice_tolerance_curve(seed): + integ = qp.Keister(qp.Gaussian(qp.Lattice(3, seed=seed))) + times, ns = [], [] + for eps in tol: + _, data = qp.CubQMCLatticeG( + integ, abs_tol=float(eps) + ).integrate() + times.append(float(data.time_integrate)) + ns.append(float(data.n_total)) + return np.asarray(times), np.asarray(ns) + +def _time_fmt(y, _): + """Format seconds at a scale appropriate for the plotted value.""" + if y >= 1: + return f"{y:g} s" + if y >= 1e-3: + return f"{y * 1e3:g} ms" + return f"{y * 1e6:g} us" + +approach1_tic = perf_counter() +ld_time, ld_n = run_lattice_tolerance_curve(seed=7) +ref_time = (ld_time[0] * tol[0]) / tol +ref_n = (ld_n[0] * tol[0]) / tol +approach1_elapsed = perf_counter() - approach1_tic +print(f"Approach 1: elapsed={approach1_elapsed:.6f} s") + +fig1, ax1 = plt.subplots( + 1, 2, figsize=(11, 4.8), constrained_layout=True +) +fig1.suptitle(f"Classic Loop (elapsed: {approach1_elapsed:.3f} s)") +for axis, values, reference, ylabel in zip( + ax1, + [ld_time, ld_n], + [ref_time, ref_n], + ["Time", "n"], +): + axis.scatter(tol, values, color="tab:blue") + axis.plot(tol, reference, color="tab:blue") + axis.set_ylabel(ylabel) + axis.set_xlim([tol.min() * 0.8, tol.max() * 1.2]) + axis.set_ylim([ + np.r_[values, reference].min() * 0.8, + np.r_[values, reference].max() * 1.2, + ]) + axis.set_xlabel("Tolerance, " + r"$\varepsilon$") + axis.set_xscale("log") + axis.set_yscale("log") + axis.legend( + ["Lattice", r"$\mathcal{O}(\varepsilon^{-1})$"], + frameon=False, + ) + axis.set_box_aspect(1) + +ax1[0].yaxis.set_major_formatter(FuncFormatter(_time_fmt)) +plt.show() +``` + +```text +Approach 1: elapsed=0.490905 s +``` + +![Classic-loop time and sample count versus tolerance.](figures/classic-loop.png){fig-alt="Two log-log plots showing elapsed time and total sample count against tolerance for independent fresh lattice runs."} + +## Approach 2: Iteration log with resume + +The solver first runs at the loosest tolerance and then resumes at +progressively tighter tolerances. Because each resumed run starts from the +previous state, the solver performs only the additional work needed for the +new accuracy target. + +After an initial or resumed run, `get_iteration_log()` returns a pandas +`DataFrame` containing the stored iteration history. For QMC stopping +criteria, the error surrogate is typically `comb_bound_diff`; for +root-mean-square-error criteria, it may be `rmse_estimate` or `rmse_tol`. +Resumed runs must use the same solver instance. + +The panels below plot cumulative elapsed time and sample count against +tolerance. Each point represents the work completed through that stopping +point. + +```python +def collect_log_rows_resume(method_name, seed): + """Resume through the tolerance sequence and return each stop row.""" + integ = qp.Keister(qp.Gaussian(qp.Lattice(3, seed=seed))) + stopping_criterion = None + data = None + + for eps in tol: # loosest to tightest + if stopping_criterion is None: + stopping_criterion = qp.CubQMCLatticeG( + integ, abs_tol=float(eps) + ) + _, data = stopping_criterion.integrate() + else: + stopping_criterion.set_tolerance(abs_tol=float(eps)) + _, data = stopping_criterion.integrate(resume=data) + + result = stopping_criterion.get_iteration_log( + formatted=False, view="stage_last" + ).copy() + result.insert(0, "method", method_name) + result.insert(1, "abs_tol", np.asarray(tol)[: len(result)]) + columns = ["method", "abs_tol", "elapsed_time", "n_total"] + return result[columns].sort_values( + "abs_tol", ascending=False + ).reset_index(drop=True) + +approach2_tic = perf_counter() +iter_log = collect_log_rows_resume("Lattice", seed=7) +approach2_elapsed = perf_counter() - approach2_tic +print(f"Approach 2: elapsed={approach2_elapsed:.6f} s") + +iter_log.head(9) +``` + +```text +Approach 2: elapsed=0.210797 s +``` + +| | method | absolute tolerance | cumulative time (s) | total samples | +|---:|:---|---:|---:|---:| +| 0 | Lattice | 0.001000 | 0.003221 | 8,192 | +| 1 | Lattice | 0.000500 | 0.005953 | 16,384 | +| 2 | Lattice | 0.000250 | 0.005953 | 16,384 | +| 3 | Lattice | 0.000125 | 0.016573 | 65,536 | +| 4 | Lattice | 0.000063 | 0.029973 | 131,072 | +| 5 | Lattice | 0.000031 | 0.056201 | 262,144 | +| 6 | Lattice | 0.000016 | 0.056201 | 262,144 | +| 7 | Lattice | 0.000008 | 0.109378 | 524,288 | +| 8 | Lattice | 0.000004 | 0.206681 | 1,048,576 | + +```python +plot_df = iter_log.copy() +plot_df[["abs_tol", "elapsed_time", "n_total"]] = plot_df[ + ["abs_tol", "elapsed_time", "n_total"] +].apply(pd.to_numeric, errors="coerce") +plot_df = plot_df.sort_values(["method", "abs_tol"]) + +def _positive_finite(values): + values = np.asarray(values, dtype=float) + return values[np.isfinite(values) & (values > 0)] + +def draw_panel(axis, y_column, y_label): + data = plot_df[plot_df["method"] == "Lattice"].sort_values( + "abs_tol" + ) + x = data["abs_tol"].to_numpy(dtype=float) + y = data[y_column].to_numpy(dtype=float) + reference = y[-1] * x[-1] / x + + axis.scatter( + x, y, color="tab:blue", marker="o", s=50, + label="Lattice", edgecolors="black", + ) + axis.plot( + x, reference, color="tab:blue", linewidth=2, + label=r"$\mathcal{O}(\varepsilon^{-1})$", + ) + + values = _positive_finite(np.r_[y, reference]) + axis.set_xscale("log") + axis.set_yscale("log") + axis.set_xlim([x.min() * 0.8, x.max() * 1.2]) + axis.set_ylim([values.min() * 0.8, values.max() * 1.25]) + axis.set_xlabel("Tolerance, " + r"$\varepsilon$") + axis.set_ylabel(y_label) + axis.grid(True, which="major", alpha=0.25) + axis.legend(frameon=False) + axis.set_box_aspect(1) + +fig2, ax2 = plt.subplots( + 1, 2, figsize=(12, 5.4), constrained_layout=True +) +fig2.suptitle( + f"Iteration Log with Resume Workflow " + f"(elapsed: {approach2_elapsed:.3f} s)" +) +draw_panel(ax2[0], "elapsed_time", "Time") +draw_panel(ax2[1], "n_total", "n") +ax2[0].yaxis.set_major_formatter(FuncFormatter(_time_fmt)) +plt.show() +``` + +![Iteration-log and resume time and sample count versus tolerance.](figures/iteration-log-resume.png){fig-alt="Two log-log plots showing cumulative elapsed time and total sample count against tolerance for the resumed lattice workflow."} + +Although the two sets of plots look similar, the second workflow reuses prior +work instead of starting fresh at every tolerance. + +## Repeated timing comparison + +A single `perf_counter()` measurement is noisy, so the notebook also uses +`timeit.repeat()` to run each workflow ten times. + +```python +import timeit + +REPEAT = 10 + +def run_approach1_once(): + times, sample_counts = run_lattice_tolerance_curve(seed=7) + return float(times.sum()), float(sample_counts[-1]) + +def run_approach2_once(): + log = collect_log_rows_resume("Lattice", seed=7) + return ( + float(log["elapsed_time"].iloc[-1]), + float(log["n_total"].iloc[-1]), + ) + +def benchmark_callable(function, repeat=REPEAT): + samples = np.asarray( + timeit.repeat(function, number=1, repeat=repeat), + dtype=float, + ) + return pd.Series({ + "average": float(samples.mean()), + "stdev": float(samples.std(ddof=1)), + "min": float(samples.min()), + "max": float(samples.max()), + "repeat": int(repeat), + }) + +benchmark_results = pd.DataFrame({ + "Classic Loop": benchmark_callable(run_approach1_once), + "Iteration Log + Resume": benchmark_callable(run_approach2_once), +}).T +benchmark_results +``` + +| workflow | average (s) | standard deviation (s) | minimum (s) | maximum (s) | repeats | +|:---|---:|---:|---:|---:|---:| +| Classic Loop | 0.459063 | 0.006500 | 0.449794 | 0.469225 | 10 | +| Iteration Log + Resume | 0.212442 | 0.009557 | 0.202143 | 0.229744 | 10 | + +## Conclusion + +For this multi-tolerance experiment, the iteration-log and resume workflow +avoids repeatedly solving the same problem from scratch. It gives researchers +access to the solver history and reuses completed work when the target +tolerance is tightened. + +The gain here applies to sequential tolerance exploration. If the final tight +tolerance is already known, running directly at that tolerance remains the +appropriate baseline. diff --git a/blog/resume-feature/index.qmd b/blog/resume-feature/index.qmd new file mode 100644 index 0000000..d0ceef8 --- /dev/null +++ b/blog/resume-feature/index.qmd @@ -0,0 +1,271 @@ +--- +title: "How Much Accuracy Do You Need?" +author: "Sou-Cheng Choi (with edits by Fred Hickernell)" +date: 2026-05-04 +date-format: "MMMM D, YYYY" +description: "Checkpointing and resuming QMCPy integration when accuracy requirements change." +categories: + - "QMCPy" + - "Checkpointing" + - "Stopping Criteria" +--- + +This post explains how QMCPy's resume feature lets users begin with a loose +tolerance, inspect the result, and later continue to a tighter tolerance +without discarding prior samples. + +The executable source is the +[`accuracy_and_resume.ipynb`](https://github.com/QMCSoftware/QMCSoftware/blob/develop/demos/demo_resume_data/accuracy_and_resume.ipynb) +notebook in the QMCPy repository. A shorter recipe is available in +[`resume_examples.ipynb`](https://github.com/QMCSoftware/QMCSoftware/blob/develop/demos/demo_resume_data/resume_examples.ipynb). + +## Art Owen's reflections on Lyness and the accuracy question + +This notebook explores a discussion about the accuracy requirements for +numerical integration, particularly in automatic quadrature routines. The +central question is how a scientist determines the accuracy needed for a +specific application. + +Three common responses illustrate the challenge: + +**Case A: The "plenty of time" response** + +> I would like 8-figure accuracy. I have quite enough computer time available +> for this. + +This relatively rare response focuses on the result rather than computational +cost. It fits small problems for which high accuracy is the main concern. + +**Case B: The "time-constrained" response** + +> I need at least 4-figure accuracy. But I don't want to use more than 2 +> seconds CPU time. If this can't be done, I shall abandon this problem. If it +> can be done, I should prefer 6- or 7-figure accuracy. But if the marginal +> cost for more figures is really small let's go to 12 figures. + +This more typical response reflects a limited computational budget and a +preference for better accuracy when its marginal cost is small. Automatic +quadrature with a restart or resume facility can refine an initial answer when +the user later asks for more accuracy. + +**Case C: The "I don't know" response** + +> I really don't know. Let me explain... + +This response highlights the numerical analyst's role in helping a scientist +connect application requirements to a quantitative accuracy target. + +## The problem + +Automatic quadrature routines such as those in QMCPy require a target +accuracy. In practice, users may not know that target initially, or their +needs may change after they inspect preliminary results or reconsider the +available computational budget. + +## The solution: Resumable integration in QMCPy + +With QMCPy's `resume` feature, a user can: + +1. Start with a loose tolerance and get a quick estimate. +2. Save the computation state. +3. Resume with a tighter tolerance instead of starting over. +4. Repeat the process as needed. + +The workflow supports checkpointing across Python sessions. Resuming requires +a compatible QMCPy version and compatible problem settings, including the +integrand definition, dimension, randomization, and stopping-criterion family. + +## Implementation + +Supported subclasses of `StoppingCriterion` implement resumption through three +pieces: + +1. `integrate()` accepts a `resume` parameter. +2. When `resume=` is supplied, the solver restores the previous + state, including sample points, transformed values, and relevant statistics. + The new run begins at the previous `n_total` instead of zero. +3. `Data.save()` and `Data.load()` checkpoint the integration state for a + later Python session. + +The example below uses a three-dimensional Genz oscillatory integrand and +QMCPy's `CubQMCLatticeG` stopping criterion. + +::: {.callout-note} +The timing values below are empirical notebook outputs and will vary by +machine. The fixed random seed makes the example reproducible, but the timings +are not theoretical guarantees. +::: + +```python +from pathlib import Path +from qmcpy import CubQMCLatticeG, Genz, Lattice +from qmcpy.util.data import Data +import resume_util as ru +``` + +### Step 1: Quick estimate + +The first run uses a loose absolute tolerance of $10^{-6}$. This example keeps +the loose tolerance near the later tight tolerance so that the initial run has +already completed useful work. + +```python +def make_cub_qmc_lattice_solver( + abs_tol=1e-4, rel_tol=0, seed=7, dimension=3 +): + """Build a CubQMCLatticeG solver for the demo case.""" + integrand = Genz( + Lattice(dimension=dimension, seed=seed), + kind_func="oscillatory", + kind_coeff=1, + ) + return CubQMCLatticeG( + integrand, abs_tol=abs_tol, rel_tol=rel_tol + ) + +abs_tol_loose = 1e-6 +rel_tol = 0 +dimension = 3 +seed = 7 + +solver = make_cub_qmc_lattice_solver( + abs_tol_loose, + rel_tol=rel_tol, + seed=seed, + dimension=dimension, +) +solver.trace_iterations = True +solver.verbose = True +solution1, data1 = solver.integrate() +``` + +```text +stage iter solution comb_bound_diff n_min n_total m xfull.shape +------------------------------------------------------------------------------------------- +ITER 1 -0.4289211 1.943e-03 0 1024 10 (1024, 3) +ITER 2 -0.4289245 8.371e-04 1024 2048 11 (2048, 3) +ITER 3 -0.4289312 1.955e-04 2048 4096 12 (4096, 3) +ITER 4 -0.4289320 1.101e-04 4096 8192 13 (8192, 3) +ITER 5 -0.4289320 1.444e-04 8192 16384 14 (16384, 3) +ITER 6 -0.4289321 1.865e-05 16384 32768 15 (32768, 3) +ITER 7 -0.4289321 9.302e-06 32768 65536 16 (65536, 3) +ITER 8 -0.4289321 1.870e-06 65536 131072 17 (131072, 3) +``` + +The resulting `data1` object records an estimate of approximately +$-0.4289321$, a combined-bound difference of $1.87\times 10^{-6}$, and +$2^{17}=131{,}072$ total samples. It also retains the solver, integrand, +measure, and lattice metadata required for diagnostics and resumption. + +### Step 2: Save the state + +Saving is optional when the same Python process will continue to use `data1`, +but it enables resumption in a later session. + +```python +output_dir = Path("output") +output_dir.mkdir(parents=True, exist_ok=True) +save_path = output_dir / "demo_resume_data.pkl" +data1.save(save_path, overwrite=True) +``` + +`Data.save()` also supports gzip compression. When `compress=True`, QMCPy +appends `.gz` when necessary. + +```python +# Save compressed and load it again. +data1.save("data.pkl", compress=True, overwrite=True) +loaded_data = Data.load("data.pkl.gz") +``` + +In the saved notebook output, the uncompressed checkpoint occupied 7,346,730 +bytes and the compressed checkpoint occupied 4,548,870 bytes, a 38.1% reduction +for that particular state. + +### Step 3: Resume with a tighter tolerance + +The next run tightens the absolute tolerance to $10^{-7}$ while reusing the +saved state and the same solver instance. + +```python +loaded_data = Data.load(save_path) +old_n_total = int(loaded_data.n_total) +old_time = float(loaded_data.time_integrate) + +abs_tol_tight = 1e-7 +solver.set_tolerance(abs_tol=abs_tol_tight) +solution2, data2 = solver.integrate(resume=loaded_data) + +resume_wall_time = float(data2.time_integrate) +new_samples_resume = int(data2.n_total) - old_n_total +two_step_time = old_time + resume_wall_time +``` + +```text +stage iter solution comb_bound_diff n_min n_total m xfull.shape +------------------------------------------------------------------------------------------- +RESUME 8 -0.4289321 1.870e-06 131072 131072 17 (131072, 3) +ITER 9 -0.4289321 7.475e-07 131072 262144 18 (262144, 3) +ITER 10 -0.4289321 2.536e-07 262144 524288 19 (524288, 3) +ITER 11 -0.4289321 9.139e-08 524288 1048576 20 (1048576, 3) +``` + +The resumed result keeps the estimate near $-0.4289321$, reduces the +combined-bound difference to $9.14\times 10^{-8}$, and increases the total +sample count to $2^{20}=1{,}048{,}576$. + +### Step 4: Compare with starting from scratch + +There are three useful comparisons: + +1. **Incremental cost after the loose run already exists:** the resumed run + adds samples from $N_1$ to $N_2$, while a fresh tight run starts at zero. +2. **New sample count:** this is less noisy than wall-clock time. +3. **End-to-end time:** loose plus resume versus a fresh tight run. + +If the tight tolerance is known in advance, a direct tight run is usually just +as efficient or slightly more efficient because checkpointing has overhead. +The practical benefit appears when the loose run is already completed and the +user later requests more accuracy. + +```python +solver2 = make_cub_qmc_lattice_solver( + abs_tol=abs_tol_tight, + rel_tol=rel_tol, + seed=seed, + dimension=dimension, +) +solver2.trace_iterations = True +solver2.verbose = True +solution3, data3 = solver2.integrate() + +fresh_wall_time = float(data3.time_integrate) +new_samples_fresh = int(data3.n_total) +samples_saved = new_samples_fresh - new_samples_resume + +ru.print_stage_summary( + resume_solver=solver, + loose_data=data1, + resume_data=data2, + fresh_solver=solver2, + fresh_data=data3, +) +``` + +| stage | absolute tolerance | total samples | new samples | iterations | solution | half-width | time (s) | +|:---|---:|---:|---:|---:|---:|---:|---:| +| Loose | $10^{-6}$ | 131,072 | 131,072 | 8 | -0.42893206 | $9.35\times10^{-7}$ | 0.0158 | +| Resumed | $10^{-7}$ | 1,048,576 | 917,504 | 11 | -0.42893206 | $4.57\times10^{-8}$ | 0.1128 | +| Fresh | $10^{-7}$ | 1,048,576 | 1,048,576 | 11 | -0.42893206 | $4.57\times10^{-8}$ | 0.1240 | + +For this saved run, resumption evaluated 917,504 new samples, while a fresh +tight run evaluated 1,048,576. The 131,072 samples from the loose run were +reused rather than discarded. The resumed and fresh calculations reached the +same displayed estimate and error bound. + +## Conclusion + +QMCPy's resume feature supports an adaptive workflow: begin with a preliminary +accuracy target, inspect the result, checkpoint the state, and pay for more +samples only if a tighter answer is later needed. This provides a practical +response to time-constrained or initially uncertain accuracy requirements.