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.
- qiskit_resume-0.2.0/LICENSE +21 -0
- qiskit_resume-0.2.0/PKG-INFO +204 -0
- qiskit_resume-0.2.0/README.md +176 -0
- qiskit_resume-0.2.0/pyproject.toml +48 -0
- qiskit_resume-0.2.0/qiskit_resume/__init__.py +75 -0
- qiskit_resume-0.2.0/qiskit_resume/__main__.py +3 -0
- qiskit_resume-0.2.0/qiskit_resume/_version.py +1 -0
- qiskit_resume-0.2.0/qiskit_resume/l0_disk/__init__.py +5 -0
- qiskit_resume-0.2.0/qiskit_resume/l0_disk/blobs/__init__.py +13 -0
- qiskit_resume-0.2.0/qiskit_resume/l0_disk/blobs/core.py +50 -0
- qiskit_resume-0.2.0/qiskit_resume/l0_disk/codec/__init__.py +20 -0
- qiskit_resume-0.2.0/qiskit_resume/l0_disk/codec/core.py +169 -0
- qiskit_resume-0.2.0/qiskit_resume/l0_disk/db/__init__.py +36 -0
- qiskit_resume-0.2.0/qiskit_resume/l0_disk/db/core.py +317 -0
- qiskit_resume-0.2.0/qiskit_resume/l1_record/__init__.py +6 -0
- qiskit_resume-0.2.0/qiskit_resume/l1_record/hashing/__init__.py +18 -0
- qiskit_resume-0.2.0/qiskit_resume/l1_record/hashing/core.py +47 -0
- qiskit_resume-0.2.0/qiskit_resume/l1_record/journal/__init__.py +12 -0
- qiskit_resume-0.2.0/qiskit_resume/l1_record/journal/core.py +182 -0
- qiskit_resume-0.2.0/qiskit_resume/l1_record/runs/__init__.py +12 -0
- qiskit_resume-0.2.0/qiskit_resume/l1_record/runs/core.py +142 -0
- qiskit_resume-0.2.0/qiskit_resume/l1_record/snapshots/__init__.py +12 -0
- qiskit_resume-0.2.0/qiskit_resume/l1_record/snapshots/core.py +79 -0
- qiskit_resume-0.2.0/qiskit_resume/l2_replay/__init__.py +6 -0
- qiskit_resume-0.2.0/qiskit_resume/l2_replay/budget/__init__.py +11 -0
- qiskit_resume-0.2.0/qiskit_resume/l2_replay/budget/core.py +67 -0
- qiskit_resume-0.2.0/qiskit_resume/l2_replay/divergence/__init__.py +11 -0
- qiskit_resume-0.2.0/qiskit_resume/l2_replay/divergence/core.py +72 -0
- qiskit_resume-0.2.0/qiskit_resume/l2_replay/replay/__init__.py +11 -0
- qiskit_resume-0.2.0/qiskit_resume/l2_replay/replay/core.py +79 -0
- qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/__init__.py +6 -0
- qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/circuits/__init__.py +29 -0
- qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/circuits/core.py +199 -0
- qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/ibm/__init__.py +31 -0
- qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/ibm/core.py +136 -0
- qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/results/__init__.py +11 -0
- qiskit_resume-0.2.0/qiskit_resume/l3_qiskit/results/core.py +78 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/__init__.py +6 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/device/__init__.py +11 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/device/core.py +45 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/function/__init__.py +11 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/function/core.py +103 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/passmanager/__init__.py +11 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/passmanager/core.py +105 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/primitive/__init__.py +19 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/primitive/core.py +287 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/rng/__init__.py +31 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/rng/core.py +135 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/run/__init__.py +22 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/run/core.py +368 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/snapshot/__init__.py +11 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/snapshot/core.py +41 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/wrap/__init__.py +11 -0
- qiskit_resume-0.2.0/qiskit_resume/l4_api/wrap/core.py +45 -0
- qiskit_resume-0.2.0/qiskit_resume/tools/__init__.py +4 -0
- qiskit_resume-0.2.0/qiskit_resume/tools/cli/__init__.py +8 -0
- qiskit_resume-0.2.0/qiskit_resume/tools/cli/core.py +427 -0
- qiskit_resume-0.2.0/qiskit_resume/tools/selftest/__init__.py +8 -0
- qiskit_resume-0.2.0/qiskit_resume/tools/selftest/core.py +134 -0
- qiskit_resume-0.2.0/qiskit_resume.egg-info/PKG-INFO +204 -0
- qiskit_resume-0.2.0/qiskit_resume.egg-info/SOURCES.txt +66 -0
- qiskit_resume-0.2.0/qiskit_resume.egg-info/dependency_links.txt +1 -0
- qiskit_resume-0.2.0/qiskit_resume.egg-info/entry_points.txt +2 -0
- qiskit_resume-0.2.0/qiskit_resume.egg-info/requires.txt +12 -0
- qiskit_resume-0.2.0/qiskit_resume.egg-info/top_level.txt +1 -0
- qiskit_resume-0.2.0/setup.cfg +4 -0
- qiskit_resume-0.2.0/tests/test_examples.py +46 -0
- 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 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -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"]
|