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.

Files changed (66) hide show
  1. qiskit_resume-0.2.0a1/LICENSE +21 -0
  2. qiskit_resume-0.2.0a1/PKG-INFO +128 -0
  3. qiskit_resume-0.2.0a1/README.md +100 -0
  4. qiskit_resume-0.2.0a1/pyproject.toml +48 -0
  5. qiskit_resume-0.2.0a1/qiskit_resume/__init__.py +51 -0
  6. qiskit_resume-0.2.0a1/qiskit_resume/__main__.py +3 -0
  7. qiskit_resume-0.2.0a1/qiskit_resume/_version.py +1 -0
  8. qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/__init__.py +5 -0
  9. qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/blobs/__init__.py +13 -0
  10. qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/blobs/core.py +50 -0
  11. qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/codec/__init__.py +20 -0
  12. qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/codec/core.py +169 -0
  13. qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/db/__init__.py +36 -0
  14. qiskit_resume-0.2.0a1/qiskit_resume/l0_disk/db/core.py +317 -0
  15. qiskit_resume-0.2.0a1/qiskit_resume/l1_record/__init__.py +6 -0
  16. qiskit_resume-0.2.0a1/qiskit_resume/l1_record/hashing/__init__.py +18 -0
  17. qiskit_resume-0.2.0a1/qiskit_resume/l1_record/hashing/core.py +47 -0
  18. qiskit_resume-0.2.0a1/qiskit_resume/l1_record/journal/__init__.py +12 -0
  19. qiskit_resume-0.2.0a1/qiskit_resume/l1_record/journal/core.py +182 -0
  20. qiskit_resume-0.2.0a1/qiskit_resume/l1_record/runs/__init__.py +12 -0
  21. qiskit_resume-0.2.0a1/qiskit_resume/l1_record/runs/core.py +142 -0
  22. qiskit_resume-0.2.0a1/qiskit_resume/l1_record/snapshots/__init__.py +12 -0
  23. qiskit_resume-0.2.0a1/qiskit_resume/l1_record/snapshots/core.py +79 -0
  24. qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/__init__.py +6 -0
  25. qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/budget/__init__.py +11 -0
  26. qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/budget/core.py +67 -0
  27. qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/divergence/__init__.py +11 -0
  28. qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/divergence/core.py +64 -0
  29. qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/replay/__init__.py +11 -0
  30. qiskit_resume-0.2.0a1/qiskit_resume/l2_replay/replay/core.py +79 -0
  31. qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/__init__.py +6 -0
  32. qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/circuits/__init__.py +29 -0
  33. qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/circuits/core.py +199 -0
  34. qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/ibm/__init__.py +31 -0
  35. qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/ibm/core.py +117 -0
  36. qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/results/__init__.py +11 -0
  37. qiskit_resume-0.2.0a1/qiskit_resume/l3_qiskit/results/core.py +78 -0
  38. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/__init__.py +6 -0
  39. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/function/__init__.py +11 -0
  40. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/function/core.py +63 -0
  41. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/passmanager/__init__.py +11 -0
  42. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/passmanager/core.py +72 -0
  43. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/primitive/__init__.py +19 -0
  44. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/primitive/core.py +279 -0
  45. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/rng/__init__.py +29 -0
  46. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/rng/core.py +92 -0
  47. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/run/__init__.py +22 -0
  48. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/run/core.py +367 -0
  49. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/snapshot/__init__.py +11 -0
  50. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/snapshot/core.py +41 -0
  51. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/wrap/__init__.py +11 -0
  52. qiskit_resume-0.2.0a1/qiskit_resume/l4_api/wrap/core.py +30 -0
  53. qiskit_resume-0.2.0a1/qiskit_resume/tools/__init__.py +4 -0
  54. qiskit_resume-0.2.0a1/qiskit_resume/tools/cli/__init__.py +8 -0
  55. qiskit_resume-0.2.0a1/qiskit_resume/tools/cli/core.py +424 -0
  56. qiskit_resume-0.2.0a1/qiskit_resume/tools/selftest/__init__.py +8 -0
  57. qiskit_resume-0.2.0a1/qiskit_resume/tools/selftest/core.py +129 -0
  58. qiskit_resume-0.2.0a1/qiskit_resume.egg-info/PKG-INFO +128 -0
  59. qiskit_resume-0.2.0a1/qiskit_resume.egg-info/SOURCES.txt +64 -0
  60. qiskit_resume-0.2.0a1/qiskit_resume.egg-info/dependency_links.txt +1 -0
  61. qiskit_resume-0.2.0a1/qiskit_resume.egg-info/entry_points.txt +2 -0
  62. qiskit_resume-0.2.0a1/qiskit_resume.egg-info/requires.txt +12 -0
  63. qiskit_resume-0.2.0a1/qiskit_resume.egg-info/top_level.txt +1 -0
  64. qiskit_resume-0.2.0a1/setup.cfg +4 -0
  65. qiskit_resume-0.2.0a1/tests/test_examples.py +46 -0
  66. 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,3 @@
1
+ from qiskit_resume.tools.cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0a1"
@@ -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"]
@@ -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
+ ]