qiskit-resume 0.2.0a1__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.0a1/LICENSE +21 -0
- qiskit_resume-0.2.0a1/PKG-INFO +128 -0
- qiskit_resume-0.2.0a1/README.md +100 -0
- qiskit_resume-0.2.0a1/pyproject.toml +48 -0
- qiskit_resume-0.2.0a1/qiskit_resume/__init__.py +51 -0
- qiskit_resume-0.2.0a1/qiskit_resume/__main__.py +3 -0
- qiskit_resume-0.2.0a1/qiskit_resume/_version.py +1 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/__init__.py +5 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/blobs/__init__.py +13 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/blobs/core.py +50 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/codec/__init__.py +20 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/codec/core.py +169 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/db/__init__.py +36 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/db/core.py +317 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l1_record/__init__.py +6 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l1_record/hashing/__init__.py +18 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l1_record/hashing/core.py +47 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l1_record/journal/__init__.py +12 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l1_record/journal/core.py +182 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l1_record/runs/__init__.py +12 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l1_record/runs/core.py +142 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l1_record/snapshots/__init__.py +12 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l1_record/snapshots/core.py +79 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/__init__.py +6 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/budget/__init__.py +11 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/budget/core.py +67 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/divergence/__init__.py +11 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/divergence/core.py +64 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/replay/__init__.py +11 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/replay/core.py +79 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/__init__.py +6 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/circuits/__init__.py +29 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/circuits/core.py +199 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/ibm/__init__.py +31 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/ibm/core.py +117 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/results/__init__.py +11 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/results/core.py +78 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/__init__.py +6 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/function/__init__.py +11 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/function/core.py +63 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/passmanager/__init__.py +11 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/passmanager/core.py +72 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/primitive/__init__.py +19 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/primitive/core.py +279 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/rng/__init__.py +29 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/rng/core.py +92 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/run/__init__.py +22 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/run/core.py +367 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/snapshot/__init__.py +11 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/snapshot/core.py +41 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/wrap/__init__.py +11 -0
- qiskit_resume-0.2.0a1/qiskit_resume/l4_api/wrap/core.py +30 -0
- qiskit_resume-0.2.0a1/qiskit_resume/tools/__init__.py +4 -0
- qiskit_resume-0.2.0a1/qiskit_resume/tools/cli/__init__.py +8 -0
- qiskit_resume-0.2.0a1/qiskit_resume/tools/cli/core.py +424 -0
- qiskit_resume-0.2.0a1/qiskit_resume/tools/selftest/__init__.py +8 -0
- qiskit_resume-0.2.0a1/qiskit_resume/tools/selftest/core.py +129 -0
- qiskit_resume-0.2.0a1/qiskit_resume.egg-info/PKG-INFO +128 -0
- qiskit_resume-0.2.0a1/qiskit_resume.egg-info/SOURCES.txt +64 -0
- qiskit_resume-0.2.0a1/qiskit_resume.egg-info/dependency_links.txt +1 -0
- qiskit_resume-0.2.0a1/qiskit_resume.egg-info/entry_points.txt +2 -0
- qiskit_resume-0.2.0a1/qiskit_resume.egg-info/requires.txt +12 -0
- qiskit_resume-0.2.0a1/qiskit_resume.egg-info/top_level.txt +1 -0
- qiskit_resume-0.2.0a1/setup.cfg +4 -0
- qiskit_resume-0.2.0a1/tests/test_examples.py +46 -0
- qiskit_resume-0.2.0a1/tests/test_layers.py +110 -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,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qiskit-resume
|
|
3
|
+
Version: 0.2.0a1
|
|
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 :: 3 - Alpha
|
|
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
|
+
> **Status: alpha (0.2.0a1).** Fully tested on local simulators (Qiskit 1.4 and 2.x, Linux and Windows).
|
|
34
|
+
> The IBM path (job reconnection, QPU usage tracking) is tested against a fake service only so far;
|
|
35
|
+
> a real-hardware check is the next milestone. The file format may still change before 0.2.0.
|
|
36
|
+
|
|
37
|
+
Wrap your Estimator / Sampler, run your script inside `resume_run(...)`, and if it stops
|
|
38
|
+
(quota used up, laptop closed, network, Ctrl+C), just run the same command again:
|
|
39
|
+
|
|
40
|
+
- finished calls are **replayed instantly** from a local `.qresume` file,
|
|
41
|
+
- a job that was still running on IBM is **reconnected**, not resubmitted,
|
|
42
|
+
- only new work uses the QPU.
|
|
43
|
+
|
|
44
|
+
It works for any algorithm (VQE, QAOA, sampling sweeps, your own loops) because it records
|
|
45
|
+
*calls*, not algorithm state: on a rerun your Python code runs again from the top, gets the
|
|
46
|
+
recorded answers, rebuilds its own state (optimizer memory included) and continues at the
|
|
47
|
+
first unrecorded call. Built first for the IBM Quantum Open Plan (10 QPU minutes per rolling
|
|
48
|
+
28 days), where every second counts.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from qiskit.primitives import StatevectorEstimator # or qiskit_ibm_runtime.EstimatorV2(mode=backend)
|
|
52
|
+
import qiskit_resume as resume
|
|
53
|
+
|
|
54
|
+
estimator = resume.wrap(StatevectorEstimator())
|
|
55
|
+
|
|
56
|
+
with resume.resume_run("vqe.qresume"):
|
|
57
|
+
theta0 = resume.rng().uniform(-3.14, 3.14, ansatz.num_parameters) # randomness that replays
|
|
58
|
+
res = minimize(lambda x: float(estimator.run([(ansatz, H, x)]).result()[0].data.evs),
|
|
59
|
+
theta0, method="COBYLA")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install qiskit-resume # Qiskit >= 1.4 (tested 1.4 and 2.6), Python >= 3.10
|
|
66
|
+
pip install "qiskit-resume[ibm]" # + qiskit-ibm-runtime: job reconnection, QPU usage
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## What you wrap
|
|
70
|
+
|
|
71
|
+
| `resume.wrap(...)` | recorded | on replay |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `EstimatorV2` / `SamplerV2` (local or IBM) | question hash, job id (before waiting), result, QPU seconds | result from the file; running IBM job reconnected |
|
|
74
|
+
| a pass manager — `resume.wrap(pm, backend=backend)` | transpiled circuit per (source circuit, device) | same circuit again, even if calibration changed |
|
|
75
|
+
| a slow, side-effect-free function — `@resume.wrap` | return value per arguments | value from the file |
|
|
76
|
+
|
|
77
|
+
Library algorithms that take an estimator (e.g. `qiskit_algorithms.VQE`) work too: pass the wrapped one.
|
|
78
|
+
|
|
79
|
+
## Rules of thumb
|
|
80
|
+
|
|
81
|
+
- **Randomness:** use `resume.rng()` / `resume.seed` (or seeded generators). Global `random`/`numpy`
|
|
82
|
+
and `qiskit_algorithms.algorithm_globals` are seeded automatically.
|
|
83
|
+
- **Changed code:** if a replayed call asks a different question, you get a warning and execution
|
|
84
|
+
continues from there (`on_divergence="stop"` to stop instead). Earlier records are kept as history.
|
|
85
|
+
- **Snapshots (optional):** `run.snapshot({...})` / `run.load_snapshot()` skip replaying the start.
|
|
86
|
+
Put *every* variable your loop needs in it; a missing one is detected and reported.
|
|
87
|
+
- **Budget:** `resume_run(..., qpu_budget_s=120)` stops cleanly before the job that would exceed it.
|
|
88
|
+
- **Device change** is detected, recorded and warned about (`on_backend_change="stop"` to stop).
|
|
89
|
+
|
|
90
|
+
## Before spending QPU time
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
qresume selftest my_script.py -- my.qresume # crash/resume at several points with synthetic results (no QPU)
|
|
94
|
+
qresume check my_script.py --file my.qresume -- my.qresume # can this file continue? (no IBM contact)
|
|
95
|
+
qresume status my.qresume # calls, snapshots, device, QPU used, warnings
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Other commands: `copy` (consistent copy while running), `export`, `quota`, `fork` (branch at a
|
|
99
|
+
snapshot), `pack`/`unpack`, `compact`.
|
|
100
|
+
|
|
101
|
+
## See it work
|
|
102
|
+
|
|
103
|
+
[`examples/crash_and_resume.py`](https://github.com/set3523/Qiskit-resume/blob/master/examples/crash_and_resume.py) takes a plain Qiskit workload
|
|
104
|
+
(VQE, QAOA, VQD from `qiskit_algorithms`, unmodified), wraps it, **kills the process part way**, runs it
|
|
105
|
+
again and checks the result equals an uninterrupted run, logging every call to the console:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
git clone https://github.com/set3523/Qiskit-resume && cd Qiskit-resume/examples
|
|
109
|
+
pip install "qiskit-resume[examples]" scipy
|
|
110
|
+
python crash_and_resume.py standard/qaoa_maxcut.py --kill-at-call 100 --compare
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Limits
|
|
114
|
+
|
|
115
|
+
- A single circuit execution cannot be paused (quantum states can't be saved); interruption
|
|
116
|
+
granularity is one primitive call.
|
|
117
|
+
- Code that is not deterministic in ways qiskit-resume can't seed (time, external data) may
|
|
118
|
+
diverge on replay — you'll be told where.
|
|
119
|
+
- Calls not made through Primitives (`backend.run`, direct `Statevector` maths) are not recorded.
|
|
120
|
+
|
|
121
|
+
## Documentation
|
|
122
|
+
|
|
123
|
+
[DECISIONS](https://github.com/set3523/Qiskit-resume/blob/master/docs/DECISIONS.md) (why) ·
|
|
124
|
+
[ARCHITECTURE](https://github.com/set3523/Qiskit-resume/blob/master/docs/ARCHITECTURE.md) (layers and flow) ·
|
|
125
|
+
[CONTRACTS](https://github.com/set3523/Qiskit-resume/blob/master/docs/CONTRACTS.md) (every module's inputs/outputs) ·
|
|
126
|
+
[Korean README](https://github.com/set3523/Qiskit-resume/blob/master/README_ko.md)
|
|
127
|
+
|
|
128
|
+
License: MIT
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
> **Status: alpha (0.2.0a1).** Fully tested on local simulators (Qiskit 1.4 and 2.x, Linux and Windows).
|
|
6
|
+
> The IBM path (job reconnection, QPU usage tracking) is tested against a fake service only so far;
|
|
7
|
+
> a real-hardware check is the next milestone. The file format may still change before 0.2.0.
|
|
8
|
+
|
|
9
|
+
Wrap your Estimator / Sampler, run your script inside `resume_run(...)`, and if it stops
|
|
10
|
+
(quota used up, laptop closed, network, Ctrl+C), just run the same command again:
|
|
11
|
+
|
|
12
|
+
- finished calls are **replayed instantly** from a local `.qresume` file,
|
|
13
|
+
- a job that was still running on IBM is **reconnected**, not resubmitted,
|
|
14
|
+
- only new work uses the QPU.
|
|
15
|
+
|
|
16
|
+
It works for any algorithm (VQE, QAOA, sampling sweeps, your own loops) because it records
|
|
17
|
+
*calls*, not algorithm state: on a rerun your Python code runs again from the top, gets the
|
|
18
|
+
recorded answers, rebuilds its own state (optimizer memory included) and continues at the
|
|
19
|
+
first unrecorded call. Built first for the IBM Quantum Open Plan (10 QPU minutes per rolling
|
|
20
|
+
28 days), where every second counts.
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from qiskit.primitives import StatevectorEstimator # or qiskit_ibm_runtime.EstimatorV2(mode=backend)
|
|
24
|
+
import qiskit_resume as resume
|
|
25
|
+
|
|
26
|
+
estimator = resume.wrap(StatevectorEstimator())
|
|
27
|
+
|
|
28
|
+
with resume.resume_run("vqe.qresume"):
|
|
29
|
+
theta0 = resume.rng().uniform(-3.14, 3.14, ansatz.num_parameters) # randomness that replays
|
|
30
|
+
res = minimize(lambda x: float(estimator.run([(ansatz, H, x)]).result()[0].data.evs),
|
|
31
|
+
theta0, method="COBYLA")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install qiskit-resume # Qiskit >= 1.4 (tested 1.4 and 2.6), Python >= 3.10
|
|
38
|
+
pip install "qiskit-resume[ibm]" # + qiskit-ibm-runtime: job reconnection, QPU usage
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What you wrap
|
|
42
|
+
|
|
43
|
+
| `resume.wrap(...)` | recorded | on replay |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `EstimatorV2` / `SamplerV2` (local or IBM) | question hash, job id (before waiting), result, QPU seconds | result from the file; running IBM job reconnected |
|
|
46
|
+
| a pass manager — `resume.wrap(pm, backend=backend)` | transpiled circuit per (source circuit, device) | same circuit again, even if calibration changed |
|
|
47
|
+
| a slow, side-effect-free function — `@resume.wrap` | return value per arguments | value from the file |
|
|
48
|
+
|
|
49
|
+
Library algorithms that take an estimator (e.g. `qiskit_algorithms.VQE`) work too: pass the wrapped one.
|
|
50
|
+
|
|
51
|
+
## Rules of thumb
|
|
52
|
+
|
|
53
|
+
- **Randomness:** use `resume.rng()` / `resume.seed` (or seeded generators). Global `random`/`numpy`
|
|
54
|
+
and `qiskit_algorithms.algorithm_globals` are seeded automatically.
|
|
55
|
+
- **Changed code:** if a replayed call asks a different question, you get a warning and execution
|
|
56
|
+
continues from there (`on_divergence="stop"` to stop instead). Earlier records are kept as history.
|
|
57
|
+
- **Snapshots (optional):** `run.snapshot({...})` / `run.load_snapshot()` skip replaying the start.
|
|
58
|
+
Put *every* variable your loop needs in it; a missing one is detected and reported.
|
|
59
|
+
- **Budget:** `resume_run(..., qpu_budget_s=120)` stops cleanly before the job that would exceed it.
|
|
60
|
+
- **Device change** is detected, recorded and warned about (`on_backend_change="stop"` to stop).
|
|
61
|
+
|
|
62
|
+
## Before spending QPU time
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
qresume selftest my_script.py -- my.qresume # crash/resume at several points with synthetic results (no QPU)
|
|
66
|
+
qresume check my_script.py --file my.qresume -- my.qresume # can this file continue? (no IBM contact)
|
|
67
|
+
qresume status my.qresume # calls, snapshots, device, QPU used, warnings
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Other commands: `copy` (consistent copy while running), `export`, `quota`, `fork` (branch at a
|
|
71
|
+
snapshot), `pack`/`unpack`, `compact`.
|
|
72
|
+
|
|
73
|
+
## See it work
|
|
74
|
+
|
|
75
|
+
[`examples/crash_and_resume.py`](https://github.com/set3523/Qiskit-resume/blob/master/examples/crash_and_resume.py) takes a plain Qiskit workload
|
|
76
|
+
(VQE, QAOA, VQD from `qiskit_algorithms`, unmodified), wraps it, **kills the process part way**, runs it
|
|
77
|
+
again and checks the result equals an uninterrupted run, logging every call to the console:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/set3523/Qiskit-resume && cd Qiskit-resume/examples
|
|
81
|
+
pip install "qiskit-resume[examples]" scipy
|
|
82
|
+
python crash_and_resume.py standard/qaoa_maxcut.py --kill-at-call 100 --compare
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Limits
|
|
86
|
+
|
|
87
|
+
- A single circuit execution cannot be paused (quantum states can't be saved); interruption
|
|
88
|
+
granularity is one primitive call.
|
|
89
|
+
- Code that is not deterministic in ways qiskit-resume can't seed (time, external data) may
|
|
90
|
+
diverge on replay — you'll be told where.
|
|
91
|
+
- Calls not made through Primitives (`backend.run`, direct `Statevector` maths) are not recorded.
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
[DECISIONS](https://github.com/set3523/Qiskit-resume/blob/master/docs/DECISIONS.md) (why) ·
|
|
96
|
+
[ARCHITECTURE](https://github.com/set3523/Qiskit-resume/blob/master/docs/ARCHITECTURE.md) (layers and flow) ·
|
|
97
|
+
[CONTRACTS](https://github.com/set3523/Qiskit-resume/blob/master/docs/CONTRACTS.md) (every module's inputs/outputs) ·
|
|
98
|
+
[Korean README](https://github.com/set3523/Qiskit-resume/blob/master/README_ko.md)
|
|
99
|
+
|
|
100
|
+
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 :: 3 - Alpha",
|
|
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,51 @@
|
|
|
1
|
+
"""qiskit-resume: record & replay for Qiskit Primitives workflows.
|
|
2
|
+
|
|
3
|
+
Typical use::
|
|
4
|
+
|
|
5
|
+
import qiskit_resume as resume
|
|
6
|
+
|
|
7
|
+
estimator = resume.wrap(EstimatorV2(mode=backend))
|
|
8
|
+
with resume.resume_run("vqe.qresume"):
|
|
9
|
+
... # plain Qiskit code; rerun the same script after an interruption
|
|
10
|
+
|
|
11
|
+
Design: docs/DECISIONS.md | structure: docs/ARCHITECTURE.md | contracts: docs/CONTRACTS.md
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from qiskit_resume._version import __version__
|
|
17
|
+
from qiskit_resume.l0_disk.codec import EncodeError
|
|
18
|
+
from qiskit_resume.l0_disk.db import AlreadyRunningError, IntegrityError
|
|
19
|
+
from qiskit_resume.l1_record.runs import ImmutableSettingError
|
|
20
|
+
from qiskit_resume.l4_api.primitive import BudgetExceeded, JobWaitTimeout, QuotaExhausted
|
|
21
|
+
from qiskit_resume.l4_api.rng import NoActiveRunError, rng
|
|
22
|
+
from qiskit_resume.l4_api.run import BackendChangedStop, DivergenceStop, ResumeRun, get_active_run, resume_run
|
|
23
|
+
from qiskit_resume.l4_api.wrap import wrap
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"AlreadyRunningError",
|
|
27
|
+
"BackendChangedStop",
|
|
28
|
+
"BudgetExceeded",
|
|
29
|
+
"DivergenceStop",
|
|
30
|
+
"EncodeError",
|
|
31
|
+
"ImmutableSettingError",
|
|
32
|
+
"IntegrityError",
|
|
33
|
+
"JobWaitTimeout",
|
|
34
|
+
"NoActiveRunError",
|
|
35
|
+
"QuotaExhausted",
|
|
36
|
+
"ResumeRun",
|
|
37
|
+
"__version__",
|
|
38
|
+
"get_active_run",
|
|
39
|
+
"resume_run",
|
|
40
|
+
"rng",
|
|
41
|
+
"seed",
|
|
42
|
+
"wrap",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def __getattr__(name: str) -> Any:
|
|
47
|
+
if name == "seed": # resume.seed -> the active experiment's seed
|
|
48
|
+
from qiskit_resume.l4_api.rng import seed as _seed
|
|
49
|
+
|
|
50
|
+
return _seed()
|
|
51
|
+
raise AttributeError(f"module 'qiskit_resume' has no attribute {name!r}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0a1"
|
|
@@ -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"]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Binary value codec for user values (wrapped-function results, snapshots) and
|
|
2
|
+
our own neutral records (D-013).
|
|
3
|
+
|
|
4
|
+
Layout of an encoded value::
|
|
5
|
+
|
|
6
|
+
b"QRV1" | u32 header_len | header (UTF-8 JSON) | segment bytes ...
|
|
7
|
+
|
|
8
|
+
The JSON header holds the structure. Numpy arrays (and, if allowed, pickles)
|
|
9
|
+
are stored as raw binary segments and referenced from the header, so numbers
|
|
10
|
+
inside arrays never go through text. Python floats in the header use ``repr``
|
|
11
|
+
which round-trips exactly.
|
|
12
|
+
|
|
13
|
+
Supported without pickle: None, bool, int, float, complex, str, bytes,
|
|
14
|
+
list, tuple, dict (str keys), numpy arrays and numpy scalars.
|
|
15
|
+
Anything else raises :class:`EncodeError` at encode time (fail fast, B7),
|
|
16
|
+
with the path to the offending value.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import pickle
|
|
23
|
+
import struct
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
MAGIC = b"QRV1"
|
|
29
|
+
_HDR = struct.Struct("<I")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EncodeError(TypeError):
|
|
33
|
+
"""A value cannot be stored. ``path`` points at the offending element."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, path: str, value: Any, hint: str = "") -> None:
|
|
36
|
+
self.path = path
|
|
37
|
+
msg = f"cannot store value at {path}: {type(value).__module__}.{type(value).__qualname__}"
|
|
38
|
+
if hint:
|
|
39
|
+
msg += f" - {hint}"
|
|
40
|
+
super().__init__(msg)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_DEFAULT_HINT = (
|
|
44
|
+
"return plain numbers/lists/dicts or numpy arrays instead "
|
|
45
|
+
"(or opt in with allow_pickle=True, which may break across library versions)"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def encode(value: Any, *, allow_pickle: bool = False) -> bytes:
|
|
50
|
+
segments: list[bytes] = []
|
|
51
|
+
|
|
52
|
+
def add_segment(data: bytes) -> int:
|
|
53
|
+
segments.append(data)
|
|
54
|
+
return len(segments) - 1
|
|
55
|
+
|
|
56
|
+
def walk(v: Any, path: str) -> Any:
|
|
57
|
+
if v is None or isinstance(v, (bool, str)):
|
|
58
|
+
return v
|
|
59
|
+
if isinstance(v, int) and not isinstance(v, bool):
|
|
60
|
+
return v
|
|
61
|
+
if isinstance(v, float):
|
|
62
|
+
if v != v or v in (float("inf"), float("-inf")):
|
|
63
|
+
return {"__f__": repr(v)}
|
|
64
|
+
return v
|
|
65
|
+
if isinstance(v, complex):
|
|
66
|
+
return {"__c__": [v.real, v.imag]}
|
|
67
|
+
if isinstance(v, (bytes, bytearray, memoryview)):
|
|
68
|
+
return {"__b__": add_segment(bytes(v))}
|
|
69
|
+
if isinstance(v, np.ndarray):
|
|
70
|
+
if v.dtype.hasobject:
|
|
71
|
+
if allow_pickle:
|
|
72
|
+
return {"__p__": add_segment(pickle.dumps(v, protocol=4))}
|
|
73
|
+
raise EncodeError(path, v, "object-dtype arrays are not supported; " + _DEFAULT_HINT)
|
|
74
|
+
arr = v if v.flags.c_contiguous else v.copy(order="C") # keeps 0-d shape
|
|
75
|
+
return {
|
|
76
|
+
"__nd__": add_segment(arr.tobytes(order="C")),
|
|
77
|
+
"dtype": arr.dtype.str,
|
|
78
|
+
"shape": list(arr.shape),
|
|
79
|
+
}
|
|
80
|
+
if isinstance(v, np.generic):
|
|
81
|
+
return walk(np.asarray(v), path) # 0-d array keeps the exact dtype
|
|
82
|
+
if isinstance(v, tuple):
|
|
83
|
+
return {"__t__": [walk(x, f"{path}[{i}]") for i, x in enumerate(v)]}
|
|
84
|
+
if isinstance(v, list):
|
|
85
|
+
return [walk(x, f"{path}[{i}]") for i, x in enumerate(v)]
|
|
86
|
+
if isinstance(v, dict):
|
|
87
|
+
out = {}
|
|
88
|
+
for k, x in v.items():
|
|
89
|
+
if not isinstance(k, str):
|
|
90
|
+
raise EncodeError(f"{path}.<key {k!r}>", k, "dict keys must be str")
|
|
91
|
+
if k.startswith("__") and k.endswith("__"):
|
|
92
|
+
raise EncodeError(f"{path}.{k}", k, "dunder keys are reserved")
|
|
93
|
+
out[k] = walk(x, f"{path}.{k}")
|
|
94
|
+
return out
|
|
95
|
+
if allow_pickle:
|
|
96
|
+
try:
|
|
97
|
+
return {"__p__": add_segment(pickle.dumps(v, protocol=4))}
|
|
98
|
+
except Exception as exc: # noqa: BLE001
|
|
99
|
+
raise EncodeError(path, v, f"pickle failed: {exc}") from exc
|
|
100
|
+
raise EncodeError(path, v, _DEFAULT_HINT)
|
|
101
|
+
|
|
102
|
+
header_obj = {"v": walk(value, "value"), "seg": None}
|
|
103
|
+
header_obj["seg"] = [len(s) for s in segments]
|
|
104
|
+
header = json.dumps(header_obj, separators=(",", ":"), allow_nan=False).encode("utf-8")
|
|
105
|
+
return MAGIC + _HDR.pack(len(header)) + header + b"".join(segments)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def decode(data: bytes) -> Any:
|
|
109
|
+
if data[:4] != MAGIC:
|
|
110
|
+
raise ValueError("not a qiskit-resume encoded value (bad magic)")
|
|
111
|
+
(hlen,) = _HDR.unpack_from(data, 4)
|
|
112
|
+
start = 4 + _HDR.size
|
|
113
|
+
header = json.loads(data[start : start + hlen].decode("utf-8"))
|
|
114
|
+
pos = start + hlen
|
|
115
|
+
segs: list[bytes] = []
|
|
116
|
+
for n in header["seg"]:
|
|
117
|
+
segs.append(data[pos : pos + n])
|
|
118
|
+
pos += n
|
|
119
|
+
|
|
120
|
+
def walk(v: Any) -> Any:
|
|
121
|
+
if isinstance(v, list):
|
|
122
|
+
return [walk(x) for x in v]
|
|
123
|
+
if isinstance(v, dict):
|
|
124
|
+
if "__nd__" in v:
|
|
125
|
+
arr = np.frombuffer(segs[v["__nd__"]], dtype=np.dtype(v["dtype"]))
|
|
126
|
+
return arr.reshape(tuple(v["shape"])).copy()
|
|
127
|
+
if "__t__" in v:
|
|
128
|
+
return tuple(walk(x) for x in v["__t__"])
|
|
129
|
+
if "__c__" in v:
|
|
130
|
+
return complex(*v["__c__"])
|
|
131
|
+
if "__f__" in v:
|
|
132
|
+
return float(v["__f__"])
|
|
133
|
+
if "__b__" in v:
|
|
134
|
+
return segs[v["__b__"]]
|
|
135
|
+
if "__p__" in v:
|
|
136
|
+
return pickle.loads(segs[v["__p__"]]) # only written when allow_pickle=True
|
|
137
|
+
return {k: walk(x) for k, x in v.items()}
|
|
138
|
+
return v
|
|
139
|
+
|
|
140
|
+
return walk(header["v"])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _fmt_sig(x: float, sig: int) -> str:
|
|
144
|
+
if x != x:
|
|
145
|
+
return "nan"
|
|
146
|
+
if x in (float("inf"), float("-inf")):
|
|
147
|
+
return "inf" if x > 0 else "-inf"
|
|
148
|
+
if x == 0.0:
|
|
149
|
+
return "0"
|
|
150
|
+
return format(x, f".{sig - 1}e")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def canonical_floats(values: Any, *, sig_digits: int = 12) -> bytes:
|
|
154
|
+
"""Canonical bytes of a real/complex array rounded to ``sig_digits`` significant digits (D-005).
|
|
155
|
+
|
|
156
|
+
Decimal text is exact and platform independent, so last-bit differences between
|
|
157
|
+
machines disappear while real changes (> 1e-12 relative) are kept. Shape is included.
|
|
158
|
+
"""
|
|
159
|
+
arr = np.asarray(values)
|
|
160
|
+
if np.iscomplexobj(arr):
|
|
161
|
+
items = [f"{_fmt_sig(float(z.real), sig_digits)}{_fmt_sig(float(z.imag), sig_digits)}j"
|
|
162
|
+
for z in arr.astype(np.complex128).ravel()]
|
|
163
|
+
else:
|
|
164
|
+
items = [_fmt_sig(float(x), sig_digits) for x in arr.astype(np.float64).ravel()]
|
|
165
|
+
return ("[" + "x".join(map(str, arr.shape)) + "]" + ",".join(items)).encode()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def canonical_scalar(x: float, *, sig_digits: int = 12) -> str:
|
|
169
|
+
return _fmt_sig(float(x), sig_digits)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""l0_disk.db: the .qresume SQLite file (D-004, D-012, D-014).
|
|
2
|
+
|
|
3
|
+
IN : file path
|
|
4
|
+
OUT: ExperimentDB (tx(), get_meta/set_meta, ensure_schema(ddl), integrity_check(), close())
|
|
5
|
+
backup_copy(src, dst) -> Path consistent copy, safe while running
|
|
6
|
+
verify_file(path) IntegrityError if damaged
|
|
7
|
+
running_info(path) -> dict | None {host, pid, started_at} if a run holds the file
|
|
8
|
+
Errors: AlreadyRunningError, IntegrityError, FormatError
|
|
9
|
+
Depends on: nothing
|
|
10
|
+
Owns table: meta
|
|
11
|
+
Contract (signatures, types, errors, invariants): docs/CONTRACTS.md section 1.1
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from qiskit_resume.l0_disk.db.core import (
|
|
15
|
+
FORMAT_VERSION,
|
|
16
|
+
AlreadyRunningError,
|
|
17
|
+
ExperimentDB,
|
|
18
|
+
FormatError,
|
|
19
|
+
IntegrityError,
|
|
20
|
+
backup_copy,
|
|
21
|
+
running_info,
|
|
22
|
+
utc_now,
|
|
23
|
+
verify_file,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"FORMAT_VERSION",
|
|
28
|
+
"AlreadyRunningError",
|
|
29
|
+
"ExperimentDB",
|
|
30
|
+
"FormatError",
|
|
31
|
+
"IntegrityError",
|
|
32
|
+
"backup_copy",
|
|
33
|
+
"running_info",
|
|
34
|
+
"utc_now",
|
|
35
|
+
"verify_file",
|
|
36
|
+
]
|