QCut 2.2.0__tar.gz → 2.4.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.
Files changed (148) hide show
  1. {qcut-2.2.0 → qcut-2.4.0}/CHANGELOG.rst +52 -0
  2. {qcut-2.2.0 → qcut-2.4.0}/PKG-INFO +79 -50
  3. {qcut-2.2.0 → qcut-2.4.0}/QCut/__init__.py +5 -0
  4. {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/backend_utility.py +60 -35
  5. {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/circuit_knitting.py +339 -179
  6. qcut-2.4.0/QCut/execution/job.py +507 -0
  7. {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/parallel.py +1 -1
  8. {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/postprocess.py +2 -2
  9. {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/probabilities.py +3 -3
  10. {qcut-2.2.0 → qcut-2.4.0}/QCut/options.py +19 -3
  11. {qcut-2.2.0 → qcut-2.4.0}/QCut/utils/circuit_utils.py +130 -1
  12. qcut-2.4.0/QCut/utils/deprecation.py +40 -0
  13. {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/PKG-INFO +79 -50
  14. {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/SOURCES.txt +5 -0
  15. {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/requires.txt +0 -1
  16. {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/scm_file_list.json +5 -0
  17. qcut-2.4.0/QCut.egg-info/scm_version.json +8 -0
  18. {qcut-2.2.0 → qcut-2.4.0}/README.md +73 -43
  19. {qcut-2.2.0 → qcut-2.4.0}/docs/Options.rst +31 -2
  20. {qcut-2.2.0 → qcut-2.4.0}/docs/Usage.rst +24 -6
  21. qcut-2.4.0/docs/_static/images/diagram/qcut-diagram-left-logo.png +0 -0
  22. {qcut-2.2.0 → qcut-2.4.0}/docs/examples/AutomaticCuts.rst +2 -1
  23. {qcut-2.2.0 → qcut-2.4.0}/docs/examples/ParallelBackends.rst +3 -3
  24. {qcut-2.2.0 → qcut-2.4.0}/docs/index.rst +2 -0
  25. {qcut-2.2.0 → qcut-2.4.0}/docs/notebooks/QCutBasicUsage.ipynb +3 -3
  26. {qcut-2.2.0 → qcut-2.4.0}/docs/notebooks/QCutOptionsExample.ipynb +1 -1
  27. {qcut-2.2.0 → qcut-2.4.0}/docs/notebooks/QCutProbabilities.ipynb +1 -1
  28. {qcut-2.2.0 → qcut-2.4.0}/docs/notebooks/QCutQAOAExample.ipynb +1 -1
  29. {qcut-2.2.0 → qcut-2.4.0}/pyproject.toml +10 -6
  30. {qcut-2.2.0 → qcut-2.4.0}/tests/test_cut_circuit.py +6 -6
  31. qcut-2.4.0/tests/test_deduplication.py +407 -0
  32. qcut-2.4.0/tests/test_job.py +262 -0
  33. {qcut-2.2.0 → qcut-2.4.0}/tests/test_observables.py +3 -2
  34. {qcut-2.2.0 → qcut-2.4.0}/tests/test_parallel.py +13 -3
  35. {qcut-2.2.0 → qcut-2.4.0}/tests/test_qcut_backend_utils.py +49 -1
  36. {qcut-2.2.0 → qcut-2.4.0}/tox.ini +1 -0
  37. {qcut-2.2.0 → qcut-2.4.0}/uv.lock +204 -189
  38. qcut-2.2.0/QCut.egg-info/scm_version.json +0 -8
  39. {qcut-2.2.0 → qcut-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  40. {qcut-2.2.0 → qcut-2.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  41. {qcut-2.2.0 → qcut-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  42. {qcut-2.2.0 → qcut-2.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  43. {qcut-2.2.0 → qcut-2.4.0}/.github/dependabot.yml +0 -0
  44. {qcut-2.2.0 → qcut-2.4.0}/.github/workflows/ci.yml +0 -0
  45. {qcut-2.2.0 → qcut-2.4.0}/.github/workflows/publish_docs.yml +0 -0
  46. {qcut-2.2.0 → qcut-2.4.0}/.github/workflows/publish_to_pypi.yml +0 -0
  47. {qcut-2.2.0 → qcut-2.4.0}/.github/workflows/tag_and_release.yml +0 -0
  48. {qcut-2.2.0 → qcut-2.4.0}/.gitignore +0 -0
  49. {qcut-2.2.0 → qcut-2.4.0}/CONTRIBUTING.md +0 -0
  50. {qcut-2.2.0 → qcut-2.4.0}/LICENSE +0 -0
  51. {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/__init__.py +0 -0
  52. {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/combine_subcircuits.py +0 -0
  53. {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/cut_finding.py +0 -0
  54. {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/graph_circuit_utils.py +0 -0
  55. {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/metis.py +0 -0
  56. {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/refine.py +0 -0
  57. {qcut-2.2.0 → qcut-2.4.0}/QCut/cutcircuit.py +0 -0
  58. {qcut-2.2.0 → qcut-2.4.0}/QCut/cutlocation.py +0 -0
  59. {qcut-2.2.0 → qcut-2.4.0}/QCut/cutting/__init__.py +0 -0
  60. {qcut-2.2.0 → qcut-2.4.0}/QCut/cutting/circuit_preparation.py +0 -0
  61. {qcut-2.2.0 → qcut-2.4.0}/QCut/cutting/consolidate.py +0 -0
  62. {qcut-2.2.0 → qcut-2.4.0}/QCut/errors/__init__.py +0 -0
  63. {qcut-2.2.0 → qcut-2.4.0}/QCut/errors/qcuterror.py +0 -0
  64. {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/__init__.py +0 -0
  65. {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/basis_transform.py +0 -0
  66. {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/distribution.py +0 -0
  67. {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/move_routing.py +0 -0
  68. {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/observables.py +0 -0
  69. {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/qcutresult.py +0 -0
  70. {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/__init__.py +0 -0
  71. {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/bundle.py +0 -0
  72. {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd.py +0 -0
  73. {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_analytic.py +0 -0
  74. {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_gates.py +0 -0
  75. {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_generate.py +0 -0
  76. {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_joint.py +0 -0
  77. {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_locc.py +0 -0
  78. {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_operations.py +0 -0
  79. {qcut-2.2.0 → qcut-2.4.0}/QCut/utils/__init__.py +0 -0
  80. {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/dependency_links.txt +0 -0
  81. {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/top_level.txt +0 -0
  82. {qcut-2.2.0 → qcut-2.4.0}/benchmarks/QCutVsAddon.ipynb +0 -0
  83. {qcut-2.2.0 → qcut-2.4.0}/docs/Acknowledgement.rst +0 -0
  84. {qcut-2.2.0 → qcut-2.4.0}/docs/Changelog.rst +0 -0
  85. {qcut-2.2.0 → qcut-2.4.0}/docs/Examples.rst +0 -0
  86. {qcut-2.2.0 → qcut-2.4.0}/docs/License.rst +0 -0
  87. {qcut-2.2.0 → qcut-2.4.0}/docs/Makefile +0 -0
  88. {qcut-2.2.0 → qcut-2.4.0}/docs/Notebooks.rst +0 -0
  89. {qcut-2.2.0 → qcut-2.4.0}/docs/QCut.rst +0 -0
  90. {qcut-2.2.0 → qcut-2.4.0}/docs/Theory.rst +0 -0
  91. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ1.png +0 -0
  92. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ10.png +0 -0
  93. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ11.png +0 -0
  94. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ1_old.png +0 -0
  95. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ2.png +0 -0
  96. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ2_old.png +0 -0
  97. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ3.png +0 -0
  98. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ3_old.png +0 -0
  99. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ4.png +0 -0
  100. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ4_odl.png +0 -0
  101. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ5.png +0 -0
  102. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ6.png +0 -0
  103. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ7.png +0 -0
  104. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ8.png +0 -0
  105. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ9.png +0 -0
  106. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-banner-transparent.png +0 -0
  107. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-banner-transparent.svg +0 -0
  108. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-banner.png +0 -0
  109. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-banner.svg +0 -0
  110. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-banner@2x.png +0 -0
  111. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-logo-1024.png +0 -0
  112. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-logo-2048.png +0 -0
  113. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-logo-transparent-2048.png +0 -0
  114. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-logo-transparent.svg +0 -0
  115. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-logo.svg +0 -0
  116. {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/probs1.png +0 -0
  117. {qcut-2.2.0 → qcut-2.4.0}/docs/_templates/page-toc.html +0 -0
  118. {qcut-2.2.0 → qcut-2.4.0}/docs/conf.py +0 -0
  119. {qcut-2.2.0 → qcut-2.4.0}/docs/examples/GateCuts.rst +0 -0
  120. {qcut-2.2.0 → qcut-2.4.0}/docs/examples/WireCuts.rst +0 -0
  121. {qcut-2.2.0 → qcut-2.4.0}/docs/make.bat +0 -0
  122. {qcut-2.2.0 → qcut-2.4.0}/docs/theory/CZ_derivation.md +0 -0
  123. {qcut-2.2.0 → qcut-2.4.0}/docs/theory/General_2q_derivation.md +0 -0
  124. {qcut-2.2.0 → qcut-2.4.0}/docs/theory/Joint_rotation_derivation.md +0 -0
  125. {qcut-2.2.0 → qcut-2.4.0}/docs/theory/LOCC_wire_derivation.md +0 -0
  126. {qcut-2.2.0 → qcut-2.4.0}/docs/theory/Probability_reconstruction.md +0 -0
  127. {qcut-2.2.0 → qcut-2.4.0}/docs/theory/SWAP_derivation.md +0 -0
  128. {qcut-2.2.0 → qcut-2.4.0}/docs/theory/Wire_derivation.md +0 -0
  129. {qcut-2.2.0 → qcut-2.4.0}/setup.cfg +0 -0
  130. {qcut-2.2.0 → qcut-2.4.0}/tag-from-pipeline.sh +0 -0
  131. {qcut-2.2.0 → qcut-2.4.0}/tests/__init__.py +0 -0
  132. {qcut-2.2.0 → qcut-2.4.0}/tests/solutions_1q.py +0 -0
  133. {qcut-2.2.0 → qcut-2.4.0}/tests/solutions_automatic_cuts.py +0 -0
  134. {qcut-2.2.0 → qcut-2.4.0}/tests/test_1q_QCut.py +0 -0
  135. {qcut-2.2.0 → qcut-2.4.0}/tests/test_automatic_cuts.py +0 -0
  136. {qcut-2.2.0 → qcut-2.4.0}/tests/test_consolidate.py +0 -0
  137. {qcut-2.2.0 → qcut-2.4.0}/tests/test_distribution.py +0 -0
  138. {qcut-2.2.0 → qcut-2.4.0}/tests/test_gate_cut.py +0 -0
  139. {qcut-2.2.0 → qcut-2.4.0}/tests/test_generic_gate_cuts.py +0 -0
  140. {qcut-2.2.0 → qcut-2.4.0}/tests/test_iqm_backend.py +0 -0
  141. {qcut-2.2.0 → qcut-2.4.0}/tests/test_joint_cuts.py +0 -0
  142. {qcut-2.2.0 → qcut-2.4.0}/tests/test_locc_wire_cuts.py +0 -0
  143. {qcut-2.2.0 → qcut-2.4.0}/tests/test_new_gate_cuts.py +0 -0
  144. {qcut-2.2.0 → qcut-2.4.0}/tests/test_probabilities.py +0 -0
  145. {qcut-2.2.0 → qcut-2.4.0}/tests/test_qcuterror.py +0 -0
  146. {qcut-2.2.0 → qcut-2.4.0}/tests/test_qpd_analytic.py +0 -0
  147. {qcut-2.2.0 → qcut-2.4.0}/tests/test_qpd_generate.py +0 -0
  148. {qcut-2.2.0 → qcut-2.4.0}/tests/test_sampling.py +0 -0
@@ -2,6 +2,58 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ **Version 2.4.0**
6
+ =================
7
+
8
+ Submitting without blocking
9
+ ---------------------------
10
+
11
+ - New :code:`submit_experiments()`, now the recommended way to run an experiment. It
12
+ takes the same arguments as :code:`run_experiments()` and returns a :code:`QCutJob`
13
+ once everything is submitted, without waiting for results. Its :code:`job_ids()`,
14
+ :code:`status()`, :code:`statuses()`, :code:`done()`, :code:`cancel()` and
15
+ :code:`result()` follow Qiskit's job interface, and :code:`partial_results()` reports
16
+ each batch's wave, job id, status, circuit and shot count, and result once it is in.
17
+ - :code:`run_experiments()` is now :code:`submit_experiments(...).result()`. Its
18
+ signature and return value are unchanged.
19
+ - Experiments whose wire cuts communicate return just as quickly. Wave zero is
20
+ submitted before :code:`submit_experiments()` returns, and a thread of the job's own
21
+ submits each later wave as soon as the one before it is in.
22
+ - A target refusing a batch part-way through no longer loses the jobs already queued.
23
+ Nothing after it is submitted, the job reports :code:`ERROR`, and :code:`result()`
24
+ raises the refusal.
25
+
26
+ **Version 2.3.0**
27
+ =================
28
+
29
+ Remove Qiskit-Aer dependency
30
+ ----------------------------
31
+
32
+ - :code:`run()` and :code:`run_cut_circuit()` no longer default to :code:`AerSimulator()`;
33
+ pass a backend or sampler. :code:`qiskit-aer` is no longer installed with QCut, so
34
+ install it separately to simulate.
35
+ - :code:`transpile_subcircuits()` and :code:`transpile_experiments()` read a backend's
36
+ public :code:`target` rather than :code:`_target`, which Aer never fills in. Transpiling
37
+ for :code:`AerSimulator` works, and :code:`run_cut_circuit()` transpiles only for a
38
+ backend with a coupling map.
39
+
40
+ Deduplicated experiment circuits
41
+ --------------------------------
42
+
43
+ - A run now submits each *distinct* experiment circuit once and shares its result with
44
+ every slot it fills.
45
+ - :code:`shots` still means shots per circuit, so a run spends that many on each
46
+ distinct circuit and the total drops by the same factor. What the old total bought
47
+ was re-measuring circuits already measured. Sharing a sample between groups leaves the estimate unbiased,
48
+ since the groups are summed rather than multiplied.
49
+ - :code:`estimate_run()` counts the circuits and shots that are actually submitted, so
50
+ it still matches the run exactly.
51
+ - :code:`CutOptions(deduplicate=False)` restores one submission per slot.
52
+ :code:`CutExperiment.num_circuits` is unchanged: it still counts the circuits the
53
+ decomposition calls for.
54
+ - Experiments whose wire cuts communicate are unaffected for now. Their first wave
55
+ already shares circuits between the groups that differ only in the label they answer.
56
+
5
57
  **Version 2.2.0**
6
58
  =================
7
59
 
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QCut
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: QCut is a quantum circuit knitting package built on top of qiskit for performing gate cuts and resetless wire cuts allowing simulation of larger quantum circuits on smaller quantum devices or simulators at the cost of a circuit overhead.
5
- Author-email: Joonas Nivala <joonas.nivala@gmail.com>
5
+ Author-email: Joonas Nivala <joonas.nivala@csc.fi>, FiQCI <servicedesk@csc.fi>
6
6
  License: Apache License
7
7
  Version 2.0, January 2004
8
8
  http://www.apache.org/licenses/
@@ -205,10 +205,10 @@ License: Apache License
205
205
  See the License for the specific language governing permissions and
206
206
  limitations under the License.
207
207
 
208
- Project-URL: homepage, https://github.com/JooNiv/QCut
209
- Project-URL: documentation, https://jooniv.github.io/QCut/
210
- Project-URL: repository, https://github.com/JooNiv/QCut.git
211
- Project-URL: changelog, https://github.com/JooNiv/QCut/blob/main/CHANGELOG.rst
208
+ Project-URL: homepage, https://github.com/FiQCI/QCut
209
+ Project-URL: documentation, https://fiqci.fi/QCut/
210
+ Project-URL: repository, https://github.com/FiQCI/QCut.git
211
+ Project-URL: changelog, https://github.com/FiQCI/QCut/blob/main/CHANGELOG.rst
212
212
  Classifier: Programming Language :: Python :: 3 :: Only
213
213
  Classifier: Programming Language :: Python :: 3.11
214
214
  Classifier: Programming Language :: Python :: 3.12
@@ -219,7 +219,6 @@ Description-Content-Type: text/markdown
219
219
  License-File: LICENSE
220
220
  Requires-Dist: qiskit<3.0,>=1.1
221
221
  Requires-Dist: numpy
222
- Requires-Dist: qiskit-aer<0.18
223
222
  Requires-Dist: pymetis==2025.2.2
224
223
  Provides-Extra: dev
225
224
  Requires-Dist: pytest; extra == "dev"
@@ -258,7 +257,7 @@ Dynamic: license-file
258
257
 
259
258
  # QCut
260
259
 
261
- ![QCut Banner](docs/_static/images/logos/qcut-banner.png)
260
+ <img src="docs/_static/images/diagram/qcut-diagram-left-logo.png" alt="QCut Diagram" width="1000">
262
261
 
263
262
  QCut is a quantum circuit knitting package built on top of qiskit for performing gate cuts and resetless wire cuts allowing simulation of larger quantum circuits on smaller quantum devices or simulators at the cost of a circuit overhead. QCut has been designed and tested to work with IQM's qpus, and the Finnish Quantum Computing Infrastructure ([FiQCI](https://fiqci.fi/)).
264
263
 
@@ -281,6 +280,8 @@ uv add QCut
281
280
 
282
281
  If using other than the default Qiskit version (newest) it is recommended to install Qiskit first before installing QCut.
283
282
 
283
+ QCut does not install a simulator. The examples below run on [Qiskit Aer](https://github.com/Qiskit/qiskit-aer), which needs `uv pip install qiskit-aer`.
284
+
284
285
  **IQM hardware and fake backends:**
285
286
 
286
287
  ```bash
@@ -297,6 +298,7 @@ After installing you can simply import what you need:
297
298
 
298
299
  ```python
299
300
  from iqm.qiskit_iqm import IQMFakeAdonis
301
+
300
302
  backend = IQMFakeAdonis()
301
303
  ```
302
304
 
@@ -342,7 +344,10 @@ from qiskit import QuantumCircuit, transpile
342
344
  from qiskit.circuit.library import CXGate
343
345
  from qiskit.quantum_info import SparsePauliOp
344
346
  from qiskit_aer import AerSimulator
345
- from qiskit.primitives import StatevectorEstimator as Estimator, BackendEstimatorV2 as BackendEstimator
347
+ from qiskit.primitives import (
348
+ StatevectorEstimator as Estimator,
349
+ BackendEstimatorV2 as BackendEstimator,
350
+ )
346
351
  from iqm.qiskit_iqm import IQMFakeAdonis
347
352
  ```
348
353
 
@@ -352,11 +357,11 @@ from iqm.qiskit_iqm import IQMFakeAdonis
352
357
  circuit = QuantumCircuit(5)
353
358
 
354
359
  for qubit in range(5):
355
- circuit.ry(0.4 + 0.3*qubit, qubit)
356
- circuit.cx(0,1)
357
- circuit.cx(1,2)
358
- circuit.cx(2,3)
359
- circuit.cx(3,4)
360
+ circuit.ry(0.4 + 0.3 * qubit, qubit)
361
+ circuit.cx(0, 1)
362
+ circuit.cx(1, 2)
363
+ circuit.cx(2, 3)
364
+ circuit.cx(3, 4)
360
365
 
361
366
  circuit.draw("mpl")
362
367
  ```
@@ -371,12 +376,12 @@ Note that here we don't insert any measurements. Measurements will be automatica
371
376
  marked_circuit = QuantumCircuit(5)
372
377
 
373
378
  for qubit in range(5):
374
- marked_circuit.ry(0.4 + 0.3*qubit, qubit)
375
- marked_circuit.cx(0,1)
379
+ marked_circuit.ry(0.4 + 0.3 * qubit, qubit)
380
+ marked_circuit.cx(0, 1)
376
381
  marked_circuit.append(**cutGate(CXGate(), 1, 2))
377
- marked_circuit.cx(2,3)
382
+ marked_circuit.cx(2, 3)
378
383
  marked_circuit.append(cut(), [3])
379
- marked_circuit.cx(3,4)
384
+ marked_circuit.cx(3, 4)
380
385
 
381
386
  marked_circuit.decompose(gates_to_decompose=["CutGate"]).draw("mpl")
382
387
  ```
@@ -491,7 +496,7 @@ print(block.gamma, local.gamma, local.optimal_gamma)
491
496
  `7.0 16.0 7.0`
492
497
 
493
498
  Those cuts run in waves, since one side has to be measured before the other can prepare
494
- what it measured. `run_experiments()` handles that itself.
499
+ what it measured. `submit_experiments()` handles that itself.
495
500
 
496
501
  ### Options
497
502
 
@@ -527,8 +532,8 @@ finder's own options and how it chooses.
527
532
  Two helpers, `transpile_subcircuits()` and `transpile_experiments()` , differing in when they run:
528
533
 
529
534
  ```python
530
- fake = IQMFakeAdonis() #noisy
531
- sim = AerSimulator() #ideal
535
+ fake = IQMFakeAdonis() # noisy
536
+ sim = AerSimulator() # ideal
532
537
  ```
533
538
 
534
539
  `transpile_circuits()` does whichever of the two the thing it is given calls for. Each
@@ -560,33 +565,57 @@ routed in for you. See
560
565
  ## Execution
561
566
 
562
567
  ```python
563
- results = ck.run_experiments(cut_experiment, backend=fake)
564
- expectation_values = ck.estimate_expectation_values(results)
568
+ job = ck.submit_experiments(cut_experiment, backend=fake)
569
+ expectation_values = ck.estimate_expectation_values(job.result())
570
+ ```
571
+
572
+ `submit_experiments()` returns a `QCutJob` as soon as everything is submitted, and
573
+ `result()` waits for the results. The job is shaped like a Qiskit job, so it can be
574
+ checked on while the run is under way:
575
+
576
+ ```python
577
+ job.job_ids() # every backend job, in submission order
578
+ job.status() # one JobStatus for the whole experiment
579
+ job.partial_results() # per batch: wave, job id, status, circuits, shots, result
565
580
  ```
566
581
 
582
+ `run_experiments()` takes the same arguments and is the same as
583
+ `submit_experiments(...).result()`.
584
+
567
585
  The cost of an experiment run can be checked before submission:
568
586
 
569
587
  ```python
570
588
  print(ck.estimate_run(cut_experiment, shots=4096, max_batch_size=40))
571
589
  ```
572
590
 
573
- `Experiment will run 144 circuits in 4 jobs with a total of 589824 shots. See the returned object for the breakdown.`
591
+ `Experiment will run 31 circuits in 1 jobs with a total of 126976 shots. See the returned object for the breakdown.`
574
592
 
575
593
  ```python
576
- {'job1': JobEstimate(circuits=40, shots=4096), 'job2': JobEstimate(circuits=40, shots=4096), ...}
594
+ {"job1": JobEstimate(circuits=31, shots=4096)}
577
595
  ```
578
596
 
597
+ 31 circuits, not the 144 the decomposition calls for: a subcircuit's experiment circuit
598
+ depends only on the QPD terms of the cuts touching it and on the measurement setting
599
+ over its own qubits, so most of them repeat. Each distinct one is submitted once and its
600
+ result shared, which is where the saving comes from. `shots` still means shots per
601
+ circuit. Pass `CutOptions(deduplicate=False)` to submit one circuit per slot instead.
602
+
579
603
  Exact for non-cummunicating runs. Runs whose wire cuts communicate get executed in waves so that
580
604
  each wave after the first spends its shots on what the one before it measured. For these jobs
581
605
  the circuit count is exact and job and shot counts are estimates.
582
606
 
607
+ The job submits those waves itself. The first goes out before `submit_experiments()`
608
+ returns, and a thread of the job's own submits each later one as soon as the one before
609
+ it is in, so nothing has to keep polling for the run to move on. `job.cancel()` cancels
610
+ what is still queued and submits no further waves.
611
+
583
612
  `backend` takes a Qiskit backend, a V2 sampler, or anything else shaped like a backend,
584
613
  which is what lets e.g. [fiqci-ems](https://github.com/FiQCI/fiqci-ems) run the experiment:
585
614
 
586
615
  ```python
587
616
  from fiqci.ems import FiQCISampler
588
617
 
589
- results = ck.run_experiments(
618
+ job = ck.submit_experiments(
590
619
  cut_experiment, backend=FiQCISampler(backend, mitigation_level=1)
591
620
  )
592
621
  ```
@@ -604,8 +633,11 @@ Comparing against the exact and noisy expectation values of the original circuit
604
633
 
605
634
  ```python
606
635
  estimator = Estimator()
607
- exact_expvals = [e.data.evs for e in
608
- estimator.run([(x) for x in zip([circuit] * len(observables), observables)]).result()
636
+ exact_expvals = [
637
+ e.data.evs
638
+ for e in estimator.run(
639
+ [(x) for x in zip([circuit] * len(observables), observables)]
640
+ ).result()
609
641
  ]
610
642
 
611
643
  tr = transpile(circuit, backend=fake)
@@ -615,8 +647,11 @@ tr_obs_separate = [
615
647
  ]
616
648
 
617
649
  fake_estimator = BackendEstimator(backend=fake)
618
- exps = [e.data.evs for e in
619
- fake_estimator.run([(x) for x in zip([tr] * len(tr_obs_separate), tr_obs_separate)]).result()
650
+ exps = [
651
+ e.data.evs
652
+ for e in fake_estimator.run(
653
+ [(x) for x in zip([tr] * len(tr_obs_separate), tr_obs_separate)]
654
+ ).result()
620
655
  ]
621
656
  ```
622
657
 
@@ -664,12 +699,16 @@ instead of `observables`:
664
699
 
665
700
  ```python
666
701
  cut_experiment = ck.get_experiment_circuits(cut_circuit, qubits=[0, 1])
667
- results = ck.run_experiments(cut_experiment, shots=4096, backend=sim)
702
+ job = ck.submit_experiments(cut_experiment, shots=4096, backend=sim)
668
703
 
669
- probs = ck.estimate_probabilities(results)
704
+ probs = ck.estimate_probabilities(job.result())
705
+ print(probs)
670
706
  ```
671
707
 
672
- `{'00': 0.8658, '01': -0.0024, '10': 0.1041, '11': 0.0324}`
708
+ `QuasiProbabilities({'00': 0.8658, '01': -0.0024, '10': 0.1041, '11': 0.0324})`
709
+
710
+ The bitstrings are read with the first of the chosen qubits as the rightmost character, so
711
+ with `qubits=[2, 0]` qubit 2 is the last character and qubit 0 the first.
673
712
 
674
713
  `run()` and `run_cut_circuit()` take `qubits` in the same way, and hand back the
675
714
  distribution rather than expectation values.
@@ -679,27 +718,17 @@ would have been for a single observable however many qubits are asked for. See
679
718
  [the derivation](https://jooniv.github.io/QCut/theory/Probability_reconstruction.html)
680
719
  for how the distribution is reconstructed.
681
720
 
682
- The result is a mapping, so it indexes and plots like a dict, and carries three views:
683
-
684
- ```python
685
- probs['00'] # 0.8658
686
- probs.quasi_probabilities() # the same values, as a plain dict
687
- probs.nearest_probabilities() # closest true distribution, negatives projected away
688
- probs.counts() # scaled by the shots the experiment ran at
689
- probs.counts(shots=1000) # or by any other shots
690
- ```
691
-
692
- The three dict views report, by default, the ten most likely bitstrings. Pass `top` for a different
693
- number, or `top=None` for all of them:
721
+ The distribution is never built as a table of `2**k` values unless asked for, so the
722
+ recommended way to read it is `top()`, which finds the most likely bitstrings directly:
694
723
 
695
724
  ```python
696
- probs.quasi_probabilities(top=50) # the fifty most likely
697
- probs.counts(shots=1000, top=None) # the whole distribution, as before
725
+ probs.top(10) # {'00': 0.8658, '10': 0.1041, '11': 0.0324, '01': -0.0024}
698
726
  ```
699
727
 
700
- Note that :code:`counts()` sums to :code:`shots` only with :code:`top=None` and
701
- Only :code:`quasi_probabilities()` gets cheaper this way since the other two project onto the
702
- nearest physical distribution first.
728
+ `probability_of()` and `marginal()` give one bitstring or a subset of the qubits.
729
+ `nearest_probabilities()` and `counts()` give the closest true distribution, as
730
+ probabilities or scaled to shots. They report the ten most likely bitstrings, or all of
731
+ them with `top=None`.
703
732
 
704
733
  Against the same circuit run whole:
705
734
 
@@ -19,7 +19,9 @@ from QCut.execution.circuit_knitting import (
19
19
  run,
20
20
  run_cut_circuit,
21
21
  run_experiments,
22
+ submit_experiments,
22
23
  )
24
+ from QCut.execution.job import PartialBatch, QCutJob
23
25
  from QCut.execution.observables import ObservablesLike
24
26
  from QCut.execution.parallel import ParallelBackend
25
27
  from QCut.execution.postprocess import (
@@ -53,6 +55,9 @@ __all__ = [
53
55
  "run",
54
56
  "run_cut_circuit",
55
57
  "run_experiments",
58
+ "submit_experiments",
59
+ "QCutJob",
60
+ "PartialBatch",
56
61
  "cut",
57
62
  "cutCZ",
58
63
  "cutSWAP",
@@ -8,7 +8,8 @@ import logging
8
8
  from collections import defaultdict
9
9
 
10
10
  from qiskit import QuantumCircuit, transpile
11
- from qiskit.circuit import Gate
11
+ from qiskit.circuit import Gate, Instruction
12
+ from qiskit.circuit.library import get_standard_gate_name_mapping
12
13
  from qiskit.transpiler import PassManager, Target
13
14
 
14
15
  from QCut.cutcircuit import CutCircuit, CutExperiment
@@ -96,11 +97,15 @@ def _add_block_gates(target, coupling_map, members: dict[str, list[str]]) -> set
96
97
  is free to hand the marker its qubits either way round. Qiskit can turn a ``cz`` the
97
98
  right way because it knows the gate is symmetric; a marker is opaque, so an
98
99
  unsupported orientation is a transpiler error rather than something it can fix.
100
+
101
+ Without a coupling map every pair is coupled, so the marker is declared global.
99
102
  """
100
- loci = {}
101
- for first, second in coupling_map.get_edges():
102
- loci[(first, second)] = None
103
- loci[(second, first)] = None
103
+ loci = None
104
+ if coupling_map is not None:
105
+ loci = {}
106
+ for first, second in coupling_map.get_edges():
107
+ loci[(first, second)] = None
108
+ loci[(second, first)] = None
104
109
  for name, group in members.items():
105
110
  target.add_instruction(
106
111
  Gate(num_qubits=len(group), name=name, params=[], label=name),
@@ -235,6 +240,40 @@ def _placeholder_gates(cut_circuit) -> dict[str, Gate]:
235
240
  return gates
236
241
 
237
242
 
243
+ def _target_of(backend) -> Target:
244
+ """A backend's target, read through the public ``target``.
245
+
246
+ Raises:
247
+ QCutError: the backend has no target to transpile for.
248
+ """
249
+ target = getattr(backend, "target", None)
250
+ if target is None:
251
+ raise QCutError(
252
+ f"{backend} has no target to transpile for. Pass a backend that has one."
253
+ )
254
+ return target
255
+
256
+
257
+ def _basis(target: Target) -> tuple[list[str], dict[str, Instruction]]:
258
+ """The operations a target names, and those of them qiskit would not recognise."""
259
+ standard = get_standard_gate_name_mapping()
260
+ names: set[str] = set()
261
+ custom: dict[str, Instruction] = {}
262
+ for operation in target.operations:
263
+ if not isinstance(operation, Instruction):
264
+ continue
265
+ names.add(operation.name)
266
+ if operation.name not in standard:
267
+ custom[operation.name] = operation
268
+ return sorted(names), custom
269
+
270
+
271
+ def has_topology(backend) -> bool:
272
+ """Whether running on ``backend`` needs its circuits transpiled first."""
273
+ target = getattr(backend, "target", None)
274
+ return target is not None and target.build_coupling_map() is not None
275
+
276
+
238
277
  def _iqm_transpiler_for(backend):
239
278
  """Return IQM's transpiler if it is installed and ``backend`` is one of its own.
240
279
 
@@ -312,27 +351,16 @@ def transpile_subcircuits(
312
351
  # the multi-qubit operation it is rather than as unrelated single-qubit ones.
313
352
  spans, block_members = _block_spans(cut_circuit)
314
353
 
315
- target = Target()
316
-
317
- try:
318
- basis_gates = list(
319
- {
320
- i[0].name
321
- for i in backend._target.instructions
322
- if isinstance(i[0].name, str)
323
- }
324
- )
325
-
326
- except Exception as e:
327
- raise ValueError(f"Error accessing backend target instructions: {e}")
328
-
329
- target = target.from_configuration(
354
+ backend_target = _target_of(backend)
355
+ coupling_map = backend_target.build_coupling_map()
356
+ basis_gates, backend_gates = _basis(backend_target)
357
+ target = Target.from_configuration(
330
358
  num_qubits=backend.num_qubits,
331
- coupling_map=backend._coupling_map,
359
+ coupling_map=coupling_map,
332
360
  basis_gates=basis_gates + list(custom_gates.keys()),
333
- custom_name_mapping=custom_gates,
361
+ custom_name_mapping={**backend_gates, **custom_gates},
334
362
  )
335
- block_names = _add_block_gates(target, backend._coupling_map, block_members)
363
+ block_names = _add_block_gates(target, coupling_map, block_members)
336
364
 
337
365
  # Fenced first: a placeholder is a single-qubit gate as far as the transpiler is
338
366
  # concerned, so without barriers it gets commuted past the others and the experiment
@@ -551,20 +579,17 @@ def transpile_circuits(
551
579
  for circuit in circuits
552
580
  ]
553
581
 
554
- # A simulator has no target to build one from, and needs none: it takes the
555
- # circuits as they are, so it is handed itself instead.
582
+ # An ideal simulator has no topology to build a target from, and needs none: it
583
+ # takes the circuits as they are, so it is handed itself instead.
556
584
  target = None
557
- if getattr(backend, "_target", None) is not None:
558
- target = Target().from_configuration(
585
+ if has_topology(backend):
586
+ backend_target = _target_of(backend)
587
+ basis_gates, backend_gates = _basis(backend_target)
588
+ target = Target.from_configuration(
559
589
  num_qubits=backend.num_qubits,
560
- coupling_map=backend._coupling_map,
561
- basis_gates=sorted(
562
- {
563
- item[0].name
564
- for item in backend._target.instructions
565
- if isinstance(item[0].name, str)
566
- }
567
- ),
590
+ coupling_map=backend_target.build_coupling_map(),
591
+ basis_gates=basis_gates,
592
+ custom_name_mapping=backend_gates,
568
593
  )
569
594
 
570
595
  return [