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.
- {qcut-2.2.0 → qcut-2.4.0}/CHANGELOG.rst +52 -0
- {qcut-2.2.0 → qcut-2.4.0}/PKG-INFO +79 -50
- {qcut-2.2.0 → qcut-2.4.0}/QCut/__init__.py +5 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/backend_utility.py +60 -35
- {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/circuit_knitting.py +339 -179
- qcut-2.4.0/QCut/execution/job.py +507 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/parallel.py +1 -1
- {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/postprocess.py +2 -2
- {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/probabilities.py +3 -3
- {qcut-2.2.0 → qcut-2.4.0}/QCut/options.py +19 -3
- {qcut-2.2.0 → qcut-2.4.0}/QCut/utils/circuit_utils.py +130 -1
- qcut-2.4.0/QCut/utils/deprecation.py +40 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/PKG-INFO +79 -50
- {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/SOURCES.txt +5 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/requires.txt +0 -1
- {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/scm_file_list.json +5 -0
- qcut-2.4.0/QCut.egg-info/scm_version.json +8 -0
- {qcut-2.2.0 → qcut-2.4.0}/README.md +73 -43
- {qcut-2.2.0 → qcut-2.4.0}/docs/Options.rst +31 -2
- {qcut-2.2.0 → qcut-2.4.0}/docs/Usage.rst +24 -6
- qcut-2.4.0/docs/_static/images/diagram/qcut-diagram-left-logo.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/examples/AutomaticCuts.rst +2 -1
- {qcut-2.2.0 → qcut-2.4.0}/docs/examples/ParallelBackends.rst +3 -3
- {qcut-2.2.0 → qcut-2.4.0}/docs/index.rst +2 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/notebooks/QCutBasicUsage.ipynb +3 -3
- {qcut-2.2.0 → qcut-2.4.0}/docs/notebooks/QCutOptionsExample.ipynb +1 -1
- {qcut-2.2.0 → qcut-2.4.0}/docs/notebooks/QCutProbabilities.ipynb +1 -1
- {qcut-2.2.0 → qcut-2.4.0}/docs/notebooks/QCutQAOAExample.ipynb +1 -1
- {qcut-2.2.0 → qcut-2.4.0}/pyproject.toml +10 -6
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_cut_circuit.py +6 -6
- qcut-2.4.0/tests/test_deduplication.py +407 -0
- qcut-2.4.0/tests/test_job.py +262 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_observables.py +3 -2
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_parallel.py +13 -3
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_qcut_backend_utils.py +49 -1
- {qcut-2.2.0 → qcut-2.4.0}/tox.ini +1 -0
- {qcut-2.2.0 → qcut-2.4.0}/uv.lock +204 -189
- qcut-2.2.0/QCut.egg-info/scm_version.json +0 -8
- {qcut-2.2.0 → qcut-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/.github/dependabot.yml +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/.github/workflows/ci.yml +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/.github/workflows/publish_docs.yml +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/.github/workflows/tag_and_release.yml +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/.gitignore +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/CONTRIBUTING.md +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/LICENSE +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/combine_subcircuits.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/cut_finding.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/graph_circuit_utils.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/metis.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/QCutFind/refine.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/cutcircuit.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/cutlocation.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/cutting/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/cutting/circuit_preparation.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/cutting/consolidate.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/errors/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/errors/qcuterror.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/basis_transform.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/distribution.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/move_routing.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/observables.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/execution/qcutresult.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/bundle.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_analytic.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_gates.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_generate.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_joint.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_locc.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/qpd/qpd_operations.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut/utils/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/dependency_links.txt +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/QCut.egg-info/top_level.txt +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/benchmarks/QCutVsAddon.ipynb +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/Acknowledgement.rst +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/Changelog.rst +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/Examples.rst +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/License.rst +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/Makefile +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/Notebooks.rst +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/QCut.rst +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/Theory.rst +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ1.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ10.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ11.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ1_old.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ2.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ2_old.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ3.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ3_old.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ4.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ4_odl.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ5.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ6.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ7.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ8.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/circ9.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-banner-transparent.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-banner-transparent.svg +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-banner.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-banner.svg +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-banner@2x.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-logo-1024.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-logo-2048.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-logo-transparent-2048.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-logo-transparent.svg +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/logos/qcut-logo.svg +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_static/images/probs1.png +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/_templates/page-toc.html +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/conf.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/examples/GateCuts.rst +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/examples/WireCuts.rst +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/make.bat +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/theory/CZ_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/theory/General_2q_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/theory/Joint_rotation_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/theory/LOCC_wire_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/theory/Probability_reconstruction.md +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/theory/SWAP_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/docs/theory/Wire_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/setup.cfg +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tag-from-pipeline.sh +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/solutions_1q.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/solutions_automatic_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_1q_QCut.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_automatic_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_consolidate.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_distribution.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_gate_cut.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_generic_gate_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_iqm_backend.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_joint_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_locc_wire_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_new_gate_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_probabilities.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_qcuterror.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_qpd_analytic.py +0 -0
- {qcut-2.2.0 → qcut-2.4.0}/tests/test_qpd_generate.py +0 -0
- {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.
|
|
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@
|
|
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/
|
|
209
|
-
Project-URL: documentation, https://
|
|
210
|
-
Project-URL: repository, https://github.com/
|
|
211
|
-
Project-URL: changelog, https://github.com/
|
|
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
|
-
|
|
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
|
|
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. `
|
|
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()
|
|
531
|
-
sim = AerSimulator()
|
|
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
|
-
|
|
564
|
-
expectation_values = ck.estimate_expectation_values(
|
|
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
|
|
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
|
-
{
|
|
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
|
-
|
|
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 = [
|
|
608
|
-
|
|
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 = [
|
|
619
|
-
|
|
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
|
-
|
|
702
|
+
job = ck.submit_experiments(cut_experiment, shots=4096, backend=sim)
|
|
668
703
|
|
|
669
|
-
probs = ck.estimate_probabilities(
|
|
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
|
|
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.
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
102
|
-
loci
|
|
103
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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=
|
|
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,
|
|
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
|
-
#
|
|
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
|
|
558
|
-
|
|
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=
|
|
561
|
-
basis_gates=
|
|
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 [
|