qiskit-resume 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of qiskit-resume might be problematic. Click here for more details.

Files changed (68) hide show
  1. qiskit_resume-0.2.0/LICENSE +21 -0
  2. qiskit_resume-0.2.0/PKG-INFO +204 -0
  3. qiskit_resume-0.2.0/README.md +176 -0
  4. qiskit_resume-0.2.0/pyproject.toml +48 -0
  5. qiskit_resume-0.2.0/qiskit_resume/__init__.py +75 -0
  6. qiskit_resume-0.2.0/qiskit_resume/__main__.py +3 -0
  7. qiskit_resume-0.2.0/qiskit_resume/_version.py +1 -0
  8. qiskit_resume-0.2.0/qiskit_resume/l0_disk/__init__.py +5 -0
  9. qiskit_resume-0.2.0/qiskit_resume/l0_disk/blobs/__init__.py +13 -0
  10. qiskit_resume-0.2.0/qiskit_resume/l0_disk/blobs/core.py +50 -0
  11. qiskit_resume-0.2.0/qiskit_resume/l0_disk/codec/__init__.py +20 -0
  12. qiskit_resume-0.2.0/qiskit_resume/l0_disk/codec/core.py +169 -0
  13. qiskit_resume-0.2.0/qiskit_resume/l0_disk/db/__init__.py +36 -0
  14. qiskit_resume-0.2.0/qiskit_resume/l0_disk/db/core.py +317 -0
  15. qiskit_resume-0.2.0/qiskit_resume/l1_record/__init__.py +6 -0
  16. qiskit_resume-0.2.0/qiskit_resume/l1_record/hashing/__init__.py +18 -0
  17. qiskit_resume-0.2.0/qiskit_resume/l1_record/hashing/core.py +47 -0
  18. qiskit_resume-0.2.0/qiskit_resume/l1_record/journal/__init__.py +12 -0
  19. qiskit_resume-0.2.0/qiskit_resume/l1_record/journal/core.py +182 -0
  20. qiskit_resume-0.2.0/qiskit_resume/l1_record/runs/__init__.py +12 -0
  21. qiskit_resume-0.2.0/qiskit_resume/l1_record/runs/core.py +142 -0
  22. qiskit_resume-0.2.0/qiskit_resume/l1_record/snapshots/__init__.py +12 -0
  23. qiskit_resume-0.2.0/qiskit_resume/l1_record/snapshots/core.py +79 -0
  24. qiskit_resume-0.2.0/qiskit_resume/l2_replay/__init__.py +6 -0
  25. qiskit_resume-0.2.0/qiskit_resume/l2_replay/budget/__init__.py +11 -0
  26. qiskit_resume-0.2.0/qiskit_resume/l2_replay/budget/core.py +67 -0
  27. qiskit_resume-0.2.0/qiskit_resume/l2_replay/divergence/__init__.py +11 -0
  28. qiskit_resume-0.2.0/qiskit_resume/l2_replay/divergence/core.py +72 -0
  29. qiskit_resume-0.2.0/qiskit_resume/l2_replay/replay/__init__.py +11 -0
  30. qiskit_resume-0.2.0/qiskit_resume/l2_replay/replay/core.py +79 -0
  31. qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/__init__.py +6 -0
  32. qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/circuits/__init__.py +29 -0
  33. qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/circuits/core.py +199 -0
  34. qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/ibm/__init__.py +31 -0
  35. qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/ibm/core.py +136 -0
  36. qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/results/__init__.py +11 -0
  37. qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/results/core.py +78 -0
  38. qiskit_resume-0.2.0/qiskit_resume/l4_api/__init__.py +6 -0
  39. qiskit_resume-0.2.0/qiskit_resume/l4_api/device/__init__.py +11 -0
  40. qiskit_resume-0.2.0/qiskit_resume/l4_api/device/core.py +45 -0
  41. qiskit_resume-0.2.0/qiskit_resume/l4_api/function/__init__.py +11 -0
  42. qiskit_resume-0.2.0/qiskit_resume/l4_api/function/core.py +103 -0
  43. qiskit_resume-0.2.0/qiskit_resume/l4_api/passmanager/__init__.py +11 -0
  44. qiskit_resume-0.2.0/qiskit_resume/l4_api/passmanager/core.py +105 -0
  45. qiskit_resume-0.2.0/qiskit_resume/l4_api/primitive/__init__.py +19 -0
  46. qiskit_resume-0.2.0/qiskit_resume/l4_api/primitive/core.py +287 -0
  47. qiskit_resume-0.2.0/qiskit_resume/l4_api/rng/__init__.py +31 -0
  48. qiskit_resume-0.2.0/qiskit_resume/l4_api/rng/core.py +135 -0
  49. qiskit_resume-0.2.0/qiskit_resume/l4_api/run/__init__.py +22 -0
  50. qiskit_resume-0.2.0/qiskit_resume/l4_api/run/core.py +368 -0
  51. qiskit_resume-0.2.0/qiskit_resume/l4_api/snapshot/__init__.py +11 -0
  52. qiskit_resume-0.2.0/qiskit_resume/l4_api/snapshot/core.py +41 -0
  53. qiskit_resume-0.2.0/qiskit_resume/l4_api/wrap/__init__.py +11 -0
  54. qiskit_resume-0.2.0/qiskit_resume/l4_api/wrap/core.py +45 -0
  55. qiskit_resume-0.2.0/qiskit_resume/tools/__init__.py +4 -0
  56. qiskit_resume-0.2.0/qiskit_resume/tools/cli/__init__.py +8 -0
  57. qiskit_resume-0.2.0/qiskit_resume/tools/cli/core.py +427 -0
  58. qiskit_resume-0.2.0/qiskit_resume/tools/selftest/__init__.py +8 -0
  59. qiskit_resume-0.2.0/qiskit_resume/tools/selftest/core.py +134 -0
  60. qiskit_resume-0.2.0/qiskit_resume.egg-info/PKG-INFO +204 -0
  61. qiskit_resume-0.2.0/qiskit_resume.egg-info/SOURCES.txt +66 -0
  62. qiskit_resume-0.2.0/qiskit_resume.egg-info/dependency_links.txt +1 -0
  63. qiskit_resume-0.2.0/qiskit_resume.egg-info/entry_points.txt +2 -0
  64. qiskit_resume-0.2.0/qiskit_resume.egg-info/requires.txt +12 -0
  65. qiskit_resume-0.2.0/qiskit_resume.egg-info/top_level.txt +1 -0
  66. qiskit_resume-0.2.0/setup.cfg +4 -0
  67. qiskit_resume-0.2.0/tests/test_examples.py +46 -0
  68. qiskit_resume-0.2.0/tests/test_layers.py +111 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jihoon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: qiskit-resume
3
+ Version: 0.2.0
4
+ Summary: Record & replay layer that lets any Qiskit Primitives workflow resume after interruption without re-spending QPU time.
5
+ Author: Jihoon
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/set3523/Qiskit-resume
8
+ Project-URL: Issues, https://github.com/set3523/Qiskit-resume/issues
9
+ Keywords: qiskit,quantum,checkpoint,resume,ibm-quantum,vqe,qaoa
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Physics
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: qiskit>=1.4
19
+ Requires-Dist: numpy>=1.23
20
+ Provides-Extra: ibm
21
+ Requires-Dist: qiskit-ibm-runtime>=0.30; extra == "ibm"
22
+ Provides-Extra: examples
23
+ Requires-Dist: qiskit-algorithms>=0.3; extra == "examples"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0; extra == "dev"
26
+ Requires-Dist: scipy; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # qiskit-resume
30
+
31
+ **Run any Qiskit Primitives workflow so that it can be interrupted and resumed — without paying for the same QPU time twice.**
32
+
33
+ If your script stops (quota used up, laptop closed, network, Ctrl+C, crash), run the same command again:
34
+
35
+ - finished calls are **replayed instantly** from a local `.qresume` file,
36
+ - a job that was still running on IBM is **reconnected**, not resubmitted,
37
+ - only new work uses the QPU.
38
+
39
+ It works for any algorithm (VQE, QAOA, sampling sweeps, `qiskit_algorithms`, your own loops) because it
40
+ records *calls*, not algorithm state: on a rerun your code runs again from the top, gets the recorded
41
+ answers, rebuilds its own state (optimizer memory included) and continues at the first unrecorded call.
42
+ Built first for the IBM Quantum Open Plan (10 QPU minutes per rolling 28 days).
43
+
44
+ Status: 0.x - tested on simulators and on real IBM hardware ([results](#test-results-on-real-ibm-hardware));
45
+ the file format may still change between minor versions.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install qiskit-resume # Qiskit >= 1.4 (tested 1.4 and 2.6), Python >= 3.10
51
+ pip install "qiskit-resume[ibm]" # + qiskit-ibm-runtime, for IBM hardware
52
+ ```
53
+
54
+ ## Add it to your script
55
+
56
+ Four changes; the rest of the script stays as it is.
57
+
58
+ **Before** (plain Qiskit):
59
+
60
+ ```python
61
+ from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2
62
+ from qiskit.transpiler import generate_preset_pass_manager
63
+
64
+ service = QiskitRuntimeService()
65
+ backend = service.least_busy(operational=True, simulator=False)
66
+ pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
67
+ estimator = EstimatorV2(mode=backend)
68
+
69
+ isa = pm.run(ansatz)
70
+ theta0 = np.random.default_rng().uniform(-np.pi, np.pi, ansatz.num_parameters)
71
+ res = minimize(lambda x: float(estimator.run([(isa, H.apply_layout(isa.layout), x)]).result()[0].data.evs),
72
+ theta0, method="COBYLA")
73
+ ```
74
+
75
+ **After**:
76
+
77
+ ```python
78
+ from qiskit_ibm_runtime import EstimatorV2
79
+ import qiskit_resume as resume
80
+
81
+ with resume.resume_run("vqe.qresume"): # 1. everything runs inside this block
82
+ backend = resume.backend() # 2. device chosen once, reused on reruns
83
+ pm = resume.pass_manager(backend, optimization_level=1) # 3. recorded, fixed-seed transpile
84
+ estimator = resume.wrap(EstimatorV2(mode=backend)) # 4. wrap every Estimator / Sampler
85
+
86
+ isa = pm.run(ansatz)
87
+ theta0 = resume.rng("init").uniform(-np.pi, np.pi, ansatz.num_parameters) # 5. randomness that replays
88
+ res = minimize(lambda x: float(estimator.run([(isa, H.apply_layout(isa.layout), x)]).result()[0].data.evs),
89
+ theta0, method="COBYLA")
90
+ ```
91
+
92
+ Then check it, without using QPU time or an account:
93
+
94
+ ```bash
95
+ qresume selftest my_script.py -- <your script arguments>
96
+ ```
97
+
98
+ `selftest` kills and resumes the script at several points with synthetic results and reports `OK: safe to
99
+ resume` or the exact call (and source line) that would not resume. Run it after every change to the script.
100
+
101
+ ### Rules (for people and for AI assistants applying this)
102
+
103
+ 1. Put the code after the imports inside `with resume.resume_run("<name>.qresume"):` (primitive calls,
104
+ `resume.rng`, `resume.seed_for` and `resume.backend` must be inside). One file per experiment, relative
105
+ to the working directory; rerunning with the same file continues it, a new name starts from scratch.
106
+ 2. Wrap **every** Estimator and Sampler with `resume.wrap(...)` (`EstimatorV2`/`SamplerV2`, the newer
107
+ `qiskit_ibm_runtime` `Estimator`/`Sampler`, or local `StatevectorEstimator`/`StatevectorSampler`).
108
+ Library algorithms (`qiskit_algorithms.VQE`, `QAOA`, `VQD`, `ComputeUncompute`, ...) get the wrapped
109
+ objects passed in.
110
+ 3. Replace `generate_preset_pass_manager(...)` with `resume.pass_manager(backend, ...)` (same arguments).
111
+ For a simulator without a backend: `resume.pass_manager(optimization_level=1, basis_gates=[...])`.
112
+ It works wherever a pass manager is accepted (e.g. `QAOA(..., transpiler=pm)`).
113
+ 4. On IBM, get the device with `resume.backend(service)` (or `resume.backend(service, name="ibm_...")`)
114
+ instead of `service.least_busy(...)`, so a rerun uses the same device. Extra filters stay as keyword
115
+ arguments: `resume.backend(service, min_num_qubits=5)`.
116
+ 5. Replace unseeded randomness that feeds the computation: `np.random.default_rng()` ->
117
+ `resume.rng("purpose")`; `np.random.rand(k)` -> `resume.rng("purpose").random(k)`; `random.random()` ->
118
+ `resume.rng("purpose").random()`; a random `int` seed (`seed=random.randint(...)`) ->
119
+ `resume.seed_for("purpose")`. `resume.rng("x")` returns the same generator for the same name, so it is
120
+ safe inside loops. Use one name per purpose. Seeded code (`default_rng(42)`) can stay.
121
+ 6. Do not change anything else (removing imports that became unused is fine). Run `qresume selftest` and
122
+ fix what it reports. Library-internal randomness (e.g. SPSA in `qiskit_algorithms`) needs nothing.
123
+ 7. Optional: decorate a slow, deterministic classical function with `@resume.wrap` so reruns skip it
124
+ (classical code otherwise reruns on every replay - correct, just slower). The end-of-run report lists
125
+ slow sections worth wrapping.
126
+
127
+ ## API
128
+
129
+ | call | what it does |
130
+ |---|---|
131
+ | `resume.resume_run(path, **settings)` | the experiment context; settings: `qpu_budget_s`, `job_wait_timeout_s`, `job_max_retries`, `on_divergence`, `on_backend_change`, `seed`, `force`, `log_calls`, ... |
132
+ | `resume.wrap(estimator_or_sampler)` | records each call: question hash, IBM job id (before waiting), result, QPU seconds; replays/reconnects on rerun |
133
+ | `resume.pass_manager(backend, **preset_args)` | recorded preset pass manager with a fixed `seed_transpiler`; same circuit on every rerun |
134
+ | `resume.wrap(pm, backend=backend)` | the same for a pass manager you built yourself |
135
+ | `resume.backend(service=None, name=None)` | the experiment's IBM device: least busy (or `name`) on the first run, the recorded one afterwards |
136
+ | `resume.rng(name=None, *, seed=None)` | numpy `Generator` that replays: one generator per name (order-independent, safe in loops), a new one per unnamed call (by call order), or from an explicit seed |
137
+ | `resume.seed_for(name)` / `resume.seed` | stable `int` seed per name / the experiment seed |
138
+ | `@resume.wrap` on a function | records a slow, side-effect-free function's return value per arguments |
139
+ | `run.snapshot({...})` / `run.load_snapshot()` | optional: skip replaying the start of a very long run |
140
+
141
+ Other behaviour: changed code is detected and reported (`on_divergence="stop"` to stop instead of
142
+ continuing); a device change is detected (`on_backend_change="stop"`); `qpu_budget_s` stops cleanly before
143
+ the job that would exceed it; global `random`/`numpy` and `qiskit_algorithms.algorithm_globals` are seeded.
144
+
145
+ ## Before spending QPU time
146
+
147
+ ```bash
148
+ qresume selftest my_script.py -- my.qresume # crash/resume at several points with synthetic results (no QPU)
149
+ qresume check my_script.py --file my.qresume -- my.qresume # can this file continue? (no IBM contact)
150
+ qresume status my.qresume # calls, snapshots, device, QPU used, warnings
151
+ ```
152
+
153
+ Other commands: `copy` (consistent copy while running), `export`, `quota`, `fork` (branch at a
154
+ snapshot), `pack`/`unpack`, `compact`.
155
+
156
+ ## See it work
157
+
158
+ [`examples/crash_and_resume.py`](https://github.com/set3523/Qiskit-resume/blob/master/examples/crash_and_resume.py) takes a plain Qiskit workload
159
+ (VQE, QAOA, VQD from `qiskit_algorithms`, unmodified), wraps it, **kills the process part way**, runs it
160
+ again and checks the result equals an uninterrupted run, logging every call to the console:
161
+
162
+ ```bash
163
+ git clone https://github.com/set3523/Qiskit-resume && cd Qiskit-resume/examples
164
+ pip install "qiskit-resume[examples]" scipy
165
+ python crash_and_resume.py standard/qaoa_maxcut.py --kill-at-call 100 --compare
166
+ ```
167
+
168
+ ## Test results on real IBM hardware
169
+
170
+ ibm_kingston, IBM Quantum Open Plan, 2026-10-24/25. Reproduce with
171
+ [`examples/ibm/`](https://github.com/set3523/Qiskit-resume/tree/master/examples/ibm).
172
+
173
+ | # | What was checked | Workload | Result |
174
+ |---|---|---|---|
175
+ | 1 | Estimator loop, hard kill, rerun: recorded calls replay from the file | VQE (EstimatorV2 + COBYLA) | PASS |
176
+ | 2 | Rerun reconnects to the job submitted before the kill (still running / already finished), no resubmission | VQE, QAOA, checks 6, 9 | PASS |
177
+ | 3 | QPU budget stops the run before the job that would exceed it | VQE | PASS |
178
+ | 4 | Sampler loop, hard kill, reconnect, run to completion | QAOA Max-Cut (SamplerV2 + COBYLA) | PASS |
179
+ | 5 | Jobs with several pubs and parameter sweeps replay bit-for-bit | Estimator 2 pubs (broadcast), Sampler 2 circuits (sweep) | PASS |
180
+ | 6 | Kill right after submit, rerun reconnects to the same job id | Estimator | PASS |
181
+ | 7 | Wait timeout stops cleanly, rerun reconnects to the waiting job | Estimator | PASS |
182
+ | 8 | Device change (ibm_kingston -> ibm_fez) stops before any submission | Estimator | PASS |
183
+ | 9 | New client-side `Estimator`/`Sampler` (qiskit-ibm-runtime >= 0.50): reconnect and replay | Estimator + Sampler | PASS |
184
+ | - | IBM-side job failure and automatic retry | - | not seen yet (tested with a fake service) |
185
+ | - | Real quota-exhausted stop | - | not seen yet (tested with a fake service) |
186
+ | - | Reconnect while the job is still QUEUED | - | not seen yet (queue was empty; same code path as above) |
187
+
188
+ Observed QPU cost per job on ibm_kingston: Sampler about 2 s, Estimator about 11-13 s (default error mitigation).
189
+
190
+ ## Limits
191
+
192
+ - A single circuit execution cannot be paused (quantum states can't be saved); interruption
193
+ granularity is one primitive call.
194
+ - Code that is not deterministic in ways qiskit-resume can't seed (time, external data) may
195
+ diverge on replay — you'll be told where.
196
+ - Calls not made through Primitives (`backend.run`, direct `Statevector` maths) are not recorded.
197
+
198
+ ## Documentation
199
+
200
+ [DECISIONS](https://github.com/set3523/Qiskit-resume/blob/master/docs/DECISIONS.md) (why) ·
201
+ [ARCHITECTURE](https://github.com/set3523/Qiskit-resume/blob/master/docs/ARCHITECTURE.md) (layers and flow) ·
202
+ [CONTRACTS](https://github.com/set3523/Qiskit-resume/blob/master/docs/CONTRACTS.md) (every module's inputs/outputs)
203
+
204
+ License: MIT
@@ -0,0 +1,176 @@
1
+ # qiskit-resume
2
+
3
+ **Run any Qiskit Primitives workflow so that it can be interrupted and resumed — without paying for the same QPU time twice.**
4
+
5
+ If your script stops (quota used up, laptop closed, network, Ctrl+C, crash), run the same command again:
6
+
7
+ - finished calls are **replayed instantly** from a local `.qresume` file,
8
+ - a job that was still running on IBM is **reconnected**, not resubmitted,
9
+ - only new work uses the QPU.
10
+
11
+ It works for any algorithm (VQE, QAOA, sampling sweeps, `qiskit_algorithms`, your own loops) because it
12
+ records *calls*, not algorithm state: on a rerun your code runs again from the top, gets the recorded
13
+ answers, rebuilds its own state (optimizer memory included) and continues at the first unrecorded call.
14
+ Built first for the IBM Quantum Open Plan (10 QPU minutes per rolling 28 days).
15
+
16
+ Status: 0.x - tested on simulators and on real IBM hardware ([results](#test-results-on-real-ibm-hardware));
17
+ the file format may still change between minor versions.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install qiskit-resume # Qiskit >= 1.4 (tested 1.4 and 2.6), Python >= 3.10
23
+ pip install "qiskit-resume[ibm]" # + qiskit-ibm-runtime, for IBM hardware
24
+ ```
25
+
26
+ ## Add it to your script
27
+
28
+ Four changes; the rest of the script stays as it is.
29
+
30
+ **Before** (plain Qiskit):
31
+
32
+ ```python
33
+ from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2
34
+ from qiskit.transpiler import generate_preset_pass_manager
35
+
36
+ service = QiskitRuntimeService()
37
+ backend = service.least_busy(operational=True, simulator=False)
38
+ pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
39
+ estimator = EstimatorV2(mode=backend)
40
+
41
+ isa = pm.run(ansatz)
42
+ theta0 = np.random.default_rng().uniform(-np.pi, np.pi, ansatz.num_parameters)
43
+ res = minimize(lambda x: float(estimator.run([(isa, H.apply_layout(isa.layout), x)]).result()[0].data.evs),
44
+ theta0, method="COBYLA")
45
+ ```
46
+
47
+ **After**:
48
+
49
+ ```python
50
+ from qiskit_ibm_runtime import EstimatorV2
51
+ import qiskit_resume as resume
52
+
53
+ with resume.resume_run("vqe.qresume"): # 1. everything runs inside this block
54
+ backend = resume.backend() # 2. device chosen once, reused on reruns
55
+ pm = resume.pass_manager(backend, optimization_level=1) # 3. recorded, fixed-seed transpile
56
+ estimator = resume.wrap(EstimatorV2(mode=backend)) # 4. wrap every Estimator / Sampler
57
+
58
+ isa = pm.run(ansatz)
59
+ theta0 = resume.rng("init").uniform(-np.pi, np.pi, ansatz.num_parameters) # 5. randomness that replays
60
+ res = minimize(lambda x: float(estimator.run([(isa, H.apply_layout(isa.layout), x)]).result()[0].data.evs),
61
+ theta0, method="COBYLA")
62
+ ```
63
+
64
+ Then check it, without using QPU time or an account:
65
+
66
+ ```bash
67
+ qresume selftest my_script.py -- <your script arguments>
68
+ ```
69
+
70
+ `selftest` kills and resumes the script at several points with synthetic results and reports `OK: safe to
71
+ resume` or the exact call (and source line) that would not resume. Run it after every change to the script.
72
+
73
+ ### Rules (for people and for AI assistants applying this)
74
+
75
+ 1. Put the code after the imports inside `with resume.resume_run("<name>.qresume"):` (primitive calls,
76
+ `resume.rng`, `resume.seed_for` and `resume.backend` must be inside). One file per experiment, relative
77
+ to the working directory; rerunning with the same file continues it, a new name starts from scratch.
78
+ 2. Wrap **every** Estimator and Sampler with `resume.wrap(...)` (`EstimatorV2`/`SamplerV2`, the newer
79
+ `qiskit_ibm_runtime` `Estimator`/`Sampler`, or local `StatevectorEstimator`/`StatevectorSampler`).
80
+ Library algorithms (`qiskit_algorithms.VQE`, `QAOA`, `VQD`, `ComputeUncompute`, ...) get the wrapped
81
+ objects passed in.
82
+ 3. Replace `generate_preset_pass_manager(...)` with `resume.pass_manager(backend, ...)` (same arguments).
83
+ For a simulator without a backend: `resume.pass_manager(optimization_level=1, basis_gates=[...])`.
84
+ It works wherever a pass manager is accepted (e.g. `QAOA(..., transpiler=pm)`).
85
+ 4. On IBM, get the device with `resume.backend(service)` (or `resume.backend(service, name="ibm_...")`)
86
+ instead of `service.least_busy(...)`, so a rerun uses the same device. Extra filters stay as keyword
87
+ arguments: `resume.backend(service, min_num_qubits=5)`.
88
+ 5. Replace unseeded randomness that feeds the computation: `np.random.default_rng()` ->
89
+ `resume.rng("purpose")`; `np.random.rand(k)` -> `resume.rng("purpose").random(k)`; `random.random()` ->
90
+ `resume.rng("purpose").random()`; a random `int` seed (`seed=random.randint(...)`) ->
91
+ `resume.seed_for("purpose")`. `resume.rng("x")` returns the same generator for the same name, so it is
92
+ safe inside loops. Use one name per purpose. Seeded code (`default_rng(42)`) can stay.
93
+ 6. Do not change anything else (removing imports that became unused is fine). Run `qresume selftest` and
94
+ fix what it reports. Library-internal randomness (e.g. SPSA in `qiskit_algorithms`) needs nothing.
95
+ 7. Optional: decorate a slow, deterministic classical function with `@resume.wrap` so reruns skip it
96
+ (classical code otherwise reruns on every replay - correct, just slower). The end-of-run report lists
97
+ slow sections worth wrapping.
98
+
99
+ ## API
100
+
101
+ | call | what it does |
102
+ |---|---|
103
+ | `resume.resume_run(path, **settings)` | the experiment context; settings: `qpu_budget_s`, `job_wait_timeout_s`, `job_max_retries`, `on_divergence`, `on_backend_change`, `seed`, `force`, `log_calls`, ... |
104
+ | `resume.wrap(estimator_or_sampler)` | records each call: question hash, IBM job id (before waiting), result, QPU seconds; replays/reconnects on rerun |
105
+ | `resume.pass_manager(backend, **preset_args)` | recorded preset pass manager with a fixed `seed_transpiler`; same circuit on every rerun |
106
+ | `resume.wrap(pm, backend=backend)` | the same for a pass manager you built yourself |
107
+ | `resume.backend(service=None, name=None)` | the experiment's IBM device: least busy (or `name`) on the first run, the recorded one afterwards |
108
+ | `resume.rng(name=None, *, seed=None)` | numpy `Generator` that replays: one generator per name (order-independent, safe in loops), a new one per unnamed call (by call order), or from an explicit seed |
109
+ | `resume.seed_for(name)` / `resume.seed` | stable `int` seed per name / the experiment seed |
110
+ | `@resume.wrap` on a function | records a slow, side-effect-free function's return value per arguments |
111
+ | `run.snapshot({...})` / `run.load_snapshot()` | optional: skip replaying the start of a very long run |
112
+
113
+ Other behaviour: changed code is detected and reported (`on_divergence="stop"` to stop instead of
114
+ continuing); a device change is detected (`on_backend_change="stop"`); `qpu_budget_s` stops cleanly before
115
+ the job that would exceed it; global `random`/`numpy` and `qiskit_algorithms.algorithm_globals` are seeded.
116
+
117
+ ## Before spending QPU time
118
+
119
+ ```bash
120
+ qresume selftest my_script.py -- my.qresume # crash/resume at several points with synthetic results (no QPU)
121
+ qresume check my_script.py --file my.qresume -- my.qresume # can this file continue? (no IBM contact)
122
+ qresume status my.qresume # calls, snapshots, device, QPU used, warnings
123
+ ```
124
+
125
+ Other commands: `copy` (consistent copy while running), `export`, `quota`, `fork` (branch at a
126
+ snapshot), `pack`/`unpack`, `compact`.
127
+
128
+ ## See it work
129
+
130
+ [`examples/crash_and_resume.py`](https://github.com/set3523/Qiskit-resume/blob/master/examples/crash_and_resume.py) takes a plain Qiskit workload
131
+ (VQE, QAOA, VQD from `qiskit_algorithms`, unmodified), wraps it, **kills the process part way**, runs it
132
+ again and checks the result equals an uninterrupted run, logging every call to the console:
133
+
134
+ ```bash
135
+ git clone https://github.com/set3523/Qiskit-resume && cd Qiskit-resume/examples
136
+ pip install "qiskit-resume[examples]" scipy
137
+ python crash_and_resume.py standard/qaoa_maxcut.py --kill-at-call 100 --compare
138
+ ```
139
+
140
+ ## Test results on real IBM hardware
141
+
142
+ ibm_kingston, IBM Quantum Open Plan, 2026-10-24/25. Reproduce with
143
+ [`examples/ibm/`](https://github.com/set3523/Qiskit-resume/tree/master/examples/ibm).
144
+
145
+ | # | What was checked | Workload | Result |
146
+ |---|---|---|---|
147
+ | 1 | Estimator loop, hard kill, rerun: recorded calls replay from the file | VQE (EstimatorV2 + COBYLA) | PASS |
148
+ | 2 | Rerun reconnects to the job submitted before the kill (still running / already finished), no resubmission | VQE, QAOA, checks 6, 9 | PASS |
149
+ | 3 | QPU budget stops the run before the job that would exceed it | VQE | PASS |
150
+ | 4 | Sampler loop, hard kill, reconnect, run to completion | QAOA Max-Cut (SamplerV2 + COBYLA) | PASS |
151
+ | 5 | Jobs with several pubs and parameter sweeps replay bit-for-bit | Estimator 2 pubs (broadcast), Sampler 2 circuits (sweep) | PASS |
152
+ | 6 | Kill right after submit, rerun reconnects to the same job id | Estimator | PASS |
153
+ | 7 | Wait timeout stops cleanly, rerun reconnects to the waiting job | Estimator | PASS |
154
+ | 8 | Device change (ibm_kingston -> ibm_fez) stops before any submission | Estimator | PASS |
155
+ | 9 | New client-side `Estimator`/`Sampler` (qiskit-ibm-runtime >= 0.50): reconnect and replay | Estimator + Sampler | PASS |
156
+ | - | IBM-side job failure and automatic retry | - | not seen yet (tested with a fake service) |
157
+ | - | Real quota-exhausted stop | - | not seen yet (tested with a fake service) |
158
+ | - | Reconnect while the job is still QUEUED | - | not seen yet (queue was empty; same code path as above) |
159
+
160
+ Observed QPU cost per job on ibm_kingston: Sampler about 2 s, Estimator about 11-13 s (default error mitigation).
161
+
162
+ ## Limits
163
+
164
+ - A single circuit execution cannot be paused (quantum states can't be saved); interruption
165
+ granularity is one primitive call.
166
+ - Code that is not deterministic in ways qiskit-resume can't seed (time, external data) may
167
+ diverge on replay — you'll be told where.
168
+ - Calls not made through Primitives (`backend.run`, direct `Statevector` maths) are not recorded.
169
+
170
+ ## Documentation
171
+
172
+ [DECISIONS](https://github.com/set3523/Qiskit-resume/blob/master/docs/DECISIONS.md) (why) ·
173
+ [ARCHITECTURE](https://github.com/set3523/Qiskit-resume/blob/master/docs/ARCHITECTURE.md) (layers and flow) ·
174
+ [CONTRACTS](https://github.com/set3523/Qiskit-resume/blob/master/docs/CONTRACTS.md) (every module's inputs/outputs)
175
+
176
+ License: MIT
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "qiskit-resume"
3
+ dynamic = ["version"]
4
+ description = "Record & replay layer that lets any Qiskit Primitives workflow resume after interruption without re-spending QPU time."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Jihoon" }]
10
+ keywords = ["qiskit", "quantum", "checkpoint", "resume", "ibm-quantum", "vqe", "qaoa"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Science/Research",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Scientific/Engineering :: Physics",
17
+ ]
18
+ dependencies = [
19
+ "qiskit>=1.4",
20
+ "numpy>=1.23",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/set3523/Qiskit-resume"
25
+ Issues = "https://github.com/set3523/Qiskit-resume/issues"
26
+
27
+ [project.optional-dependencies]
28
+ ibm = ["qiskit-ibm-runtime>=0.30"]
29
+ examples = ["qiskit-algorithms>=0.3"]
30
+ dev = ["pytest>=7.0", "scipy"]
31
+
32
+ [project.scripts]
33
+ qresume = "qiskit_resume.tools.cli:main"
34
+
35
+ [build-system]
36
+ requires = ["setuptools>=77"]
37
+ build-backend = "setuptools.build_meta"
38
+
39
+ [tool.setuptools.dynamic]
40
+ version = { attr = "qiskit_resume._version.__version__" }
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["."]
44
+ include = ["qiskit_resume*"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+ addopts = "-p no:cacheprovider"
@@ -0,0 +1,75 @@
1
+ """qiskit-resume: run a Qiskit Primitives script so it can be interrupted and resumed.
2
+
3
+ Rerun the same script after any interruption: finished calls replay from a local .qresume file,
4
+ an IBM job that was still running is reconnected (not resubmitted), only new work uses the QPU.
5
+
6
+ How to add it to an existing script (also for AI assistants):
7
+
8
+ import qiskit_resume as resume
9
+
10
+ with resume.resume_run("my_run.qresume"): # 1. run everything inside this
11
+ backend = resume.backend() # 2. device: chosen once, reused on reruns
12
+ pm = resume.pass_manager(backend, optimization_level=1) # 3. instead of generate_preset_pass_manager
13
+ estimator = resume.wrap(EstimatorV2(mode=backend)) # 4. wrap every Estimator / Sampler
14
+ theta0 = resume.rng("init").uniform(-1, 1, n) # 5. replace unseeded randomness
15
+ ... # the rest of the script is unchanged
16
+
17
+ Rules
18
+ - Replace np.random.default_rng() (no seed), np.random.rand(), random.random() used for
19
+ algorithm inputs with draws from resume.rng("purpose"); a random int seed becomes
20
+ resume.seed_for("purpose"). resume.rng("x") returns the same generator for the same name,
21
+ so it is safe inside loops.
22
+ - Replace generate_preset_pass_manager(...) with resume.pass_manager(...) (same arguments);
23
+ it works anywhere a pass manager is accepted, e.g. QAOA(..., transpiler=pm).
24
+ - Do not create Estimators/Samplers inside library code you cannot wrap; pass wrapped ones in.
25
+ - Optional: @resume.wrap on a slow, deterministic classical function records its result so
26
+ reruns skip it (help(resume.wrap)).
27
+ - Verify before spending QPU time: qresume selftest my_script.py -- <script args>
28
+ (crashes and resumes the script at several points with synthetic results; no QPU, no account).
29
+
30
+ Full guide: https://github.com/set3523/Qiskit-resume (README "Add it to your script", llms.txt)
31
+ """
32
+
33
+ from typing import Any
34
+
35
+ from qiskit_resume._version import __version__
36
+ from qiskit_resume.l0_disk.codec import EncodeError
37
+ from qiskit_resume.l0_disk.db import AlreadyRunningError, IntegrityError
38
+ from qiskit_resume.l1_record.runs import ImmutableSettingError
39
+ from qiskit_resume.l4_api.primitive import BudgetExceeded, JobWaitTimeout, QuotaExhausted
40
+ from qiskit_resume.l4_api.device import backend
41
+ from qiskit_resume.l4_api.passmanager import pass_manager
42
+ from qiskit_resume.l4_api.rng import NoActiveRunError, rng, seed_for
43
+ from qiskit_resume.l4_api.run import BackendChangedStop, DivergenceStop, ResumeRun, get_active_run, resume_run
44
+ from qiskit_resume.l4_api.wrap import wrap
45
+
46
+ __all__ = [
47
+ "AlreadyRunningError",
48
+ "BackendChangedStop",
49
+ "BudgetExceeded",
50
+ "DivergenceStop",
51
+ "EncodeError",
52
+ "ImmutableSettingError",
53
+ "IntegrityError",
54
+ "JobWaitTimeout",
55
+ "NoActiveRunError",
56
+ "QuotaExhausted",
57
+ "ResumeRun",
58
+ "__version__",
59
+ "backend",
60
+ "get_active_run",
61
+ "pass_manager",
62
+ "resume_run",
63
+ "rng",
64
+ "seed",
65
+ "seed_for",
66
+ "wrap",
67
+ ]
68
+
69
+
70
+ def __getattr__(name: str) -> Any:
71
+ if name == "seed": # resume.seed -> the active experiment's seed
72
+ from qiskit_resume.l4_api.rng import seed as _seed
73
+
74
+ return _seed()
75
+ raise AttributeError(f"module 'qiskit_resume' has no attribute {name!r}")
@@ -0,0 +1,3 @@
1
+ from qiskit_resume.tools.cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,5 @@
1
+ """L0 disk: the .qresume file, the blob store and the value codec.
2
+
3
+ Knows nothing about Qiskit or what records mean.
4
+ Depends on: standard library + numpy only. Contracts: docs/CONTRACTS.md section 1.
5
+ """
@@ -0,0 +1,13 @@
1
+ """l0_disk.blobs: content-addressed blob store (D-013 dedup).
2
+
3
+ IN : bytes, kind
4
+ OUT: BlobRef (str, SHA-256 hex); get(BlobRef) -> bytes; has(BlobRef) -> bool
5
+ content_hash(bytes) -> str
6
+ Depends on: l0_disk.db
7
+ Owns table: blobs
8
+ Contract (signatures, types, errors, invariants): docs/CONTRACTS.md section 1.2
9
+ """
10
+
11
+ from qiskit_resume.l0_disk.blobs.core import BlobStore, content_hash
12
+
13
+ __all__ = ["BlobStore", "content_hash"]
@@ -0,0 +1,50 @@
1
+ """Content-addressed blob store inside the experiment DB (D-013 dedup).
2
+
3
+ Identical payloads (same circuit, same observable, ...) are stored once and
4
+ referenced by their SHA-256 hex digest.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import sqlite3
11
+
12
+ from qiskit_resume.l0_disk.db import ExperimentDB
13
+
14
+
15
+ _DDL = """
16
+ CREATE TABLE IF NOT EXISTS blobs (
17
+ hash TEXT PRIMARY KEY,
18
+ kind TEXT NOT NULL,
19
+ size INTEGER NOT NULL,
20
+ data BLOB NOT NULL
21
+ );
22
+ """
23
+
24
+
25
+ def content_hash(data: bytes) -> str:
26
+ return hashlib.sha256(data).hexdigest()
27
+
28
+
29
+ class BlobStore:
30
+ def __init__(self, db: ExperimentDB) -> None:
31
+ db.ensure_schema(_DDL)
32
+ self._conn = db.conn
33
+
34
+ def put(self, data: bytes, *, kind: str = "raw") -> str:
35
+ """Store ``data`` (if new) and return its hash. Caller owns the transaction."""
36
+ h = content_hash(data)
37
+ self._conn.execute(
38
+ "INSERT OR IGNORE INTO blobs(hash, kind, size, data) VALUES (?, ?, ?, ?)",
39
+ (h, kind, len(data), sqlite3.Binary(data)),
40
+ )
41
+ return h
42
+
43
+ def get(self, h: str) -> bytes:
44
+ row = self._conn.execute("SELECT data FROM blobs WHERE hash = ?", (h,)).fetchone()
45
+ if row is None:
46
+ raise KeyError(f"blob not found: {h}")
47
+ return bytes(row[0])
48
+
49
+ def has(self, h: str) -> bool:
50
+ return self._conn.execute("SELECT 1 FROM blobs WHERE hash = ?", (h,)).fetchone() is not None
@@ -0,0 +1,20 @@
1
+ """l0_disk.codec: value <-> bytes (D-013).
2
+
3
+ IN : Python value (None/bool/int/float/complex/str/bytes/list/tuple/dict[str, ...]/numpy)
4
+ OUT: bytes; decode(bytes) -> value
5
+ canonical_floats(array) -> bytes 12-significant-digit text used as hash input (shared by l1 and l3)
6
+ Errors: EncodeError (with the path of the unstorable value, raised at record time)
7
+ Option: allow_pickle=True (fragile across library versions)
8
+ Depends on: nothing
9
+ Contract (signatures, types, errors, invariants): docs/CONTRACTS.md section 1.3
10
+ """
11
+
12
+ from qiskit_resume.l0_disk.codec.core import (
13
+ EncodeError,
14
+ canonical_floats,
15
+ canonical_scalar,
16
+ decode,
17
+ encode,
18
+ )
19
+
20
+ __all__ = ["EncodeError", "canonical_floats", "canonical_scalar", "decode", "encode"]