QCut 2.2.0__tar.gz → 2.3.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.3.0}/CHANGELOG.rst +31 -0
- {qcut-2.2.0 → qcut-2.3.0}/PKG-INFO +50 -33
- {qcut-2.2.0 → qcut-2.3.0}/QCut/execution/backend_utility.py +60 -35
- {qcut-2.2.0 → qcut-2.3.0}/QCut/execution/circuit_knitting.py +129 -47
- {qcut-2.2.0 → qcut-2.3.0}/QCut/options.py +19 -3
- {qcut-2.2.0 → qcut-2.3.0}/QCut/utils/circuit_utils.py +130 -1
- {qcut-2.2.0 → qcut-2.3.0}/QCut.egg-info/PKG-INFO +50 -33
- {qcut-2.2.0 → qcut-2.3.0}/QCut.egg-info/SOURCES.txt +2 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut.egg-info/requires.txt +0 -1
- {qcut-2.2.0 → qcut-2.3.0}/QCut.egg-info/scm_file_list.json +2 -0
- qcut-2.3.0/QCut.egg-info/scm_version.json +8 -0
- {qcut-2.2.0 → qcut-2.3.0}/README.md +44 -26
- {qcut-2.2.0 → qcut-2.3.0}/docs/Options.rst +31 -2
- qcut-2.3.0/docs/_static/images/diagram/qcut-diagram-left-logo.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/examples/AutomaticCuts.rst +2 -1
- {qcut-2.2.0 → qcut-2.3.0}/docs/index.rst +2 -0
- {qcut-2.2.0 → qcut-2.3.0}/pyproject.toml +7 -6
- qcut-2.3.0/tests/test_deduplication.py +406 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_observables.py +3 -2
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_parallel.py +13 -3
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_qcut_backend_utils.py +49 -1
- {qcut-2.2.0 → qcut-2.3.0}/uv.lock +204 -189
- qcut-2.2.0/QCut.egg-info/scm_version.json +0 -8
- {qcut-2.2.0 → qcut-2.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/.github/dependabot.yml +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/.github/workflows/ci.yml +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/.github/workflows/publish_docs.yml +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/.github/workflows/tag_and_release.yml +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/.gitignore +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/CONTRIBUTING.md +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/LICENSE +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/QCutFind/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/QCutFind/combine_subcircuits.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/QCutFind/cut_finding.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/QCutFind/graph_circuit_utils.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/QCutFind/metis.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/QCutFind/refine.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/cutcircuit.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/cutlocation.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/cutting/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/cutting/circuit_preparation.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/cutting/consolidate.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/errors/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/errors/qcuterror.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/execution/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/execution/basis_transform.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/execution/distribution.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/execution/move_routing.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/execution/observables.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/execution/parallel.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/execution/postprocess.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/execution/probabilities.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/execution/qcutresult.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/qpd/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/qpd/bundle.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/qpd/qpd.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/qpd/qpd_analytic.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/qpd/qpd_gates.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/qpd/qpd_generate.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/qpd/qpd_joint.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/qpd/qpd_locc.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/qpd/qpd_operations.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut/utils/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut.egg-info/dependency_links.txt +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/QCut.egg-info/top_level.txt +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/benchmarks/QCutVsAddon.ipynb +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/Acknowledgement.rst +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/Changelog.rst +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/Examples.rst +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/License.rst +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/Makefile +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/Notebooks.rst +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/QCut.rst +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/Theory.rst +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/Usage.rst +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ1.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ10.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ11.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ1_old.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ2.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ2_old.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ3.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ3_old.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ4.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ4_odl.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ5.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ6.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ7.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ8.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/circ9.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/logos/qcut-banner-transparent.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/logos/qcut-banner-transparent.svg +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/logos/qcut-banner.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/logos/qcut-banner.svg +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/logos/qcut-banner@2x.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/logos/qcut-logo-1024.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/logos/qcut-logo-2048.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/logos/qcut-logo-transparent-2048.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/logos/qcut-logo-transparent.svg +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/logos/qcut-logo.svg +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_static/images/probs1.png +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/_templates/page-toc.html +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/conf.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/examples/GateCuts.rst +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/examples/ParallelBackends.rst +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/examples/WireCuts.rst +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/make.bat +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/notebooks/QCutBasicUsage.ipynb +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/notebooks/QCutOptionsExample.ipynb +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/notebooks/QCutProbabilities.ipynb +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/notebooks/QCutQAOAExample.ipynb +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/theory/CZ_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/theory/General_2q_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/theory/Joint_rotation_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/theory/LOCC_wire_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/theory/Probability_reconstruction.md +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/theory/SWAP_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/docs/theory/Wire_derivation.md +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/setup.cfg +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tag-from-pipeline.sh +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/__init__.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/solutions_1q.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/solutions_automatic_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_1q_QCut.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_automatic_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_consolidate.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_cut_circuit.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_distribution.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_gate_cut.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_generic_gate_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_iqm_backend.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_joint_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_locc_wire_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_new_gate_cuts.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_probabilities.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_qcuterror.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_qpd_analytic.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_qpd_generate.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tests/test_sampling.py +0 -0
- {qcut-2.2.0 → qcut-2.3.0}/tox.ini +0 -0
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
Changelog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
**Version 2.3.0**
|
|
6
|
+
=================
|
|
7
|
+
|
|
8
|
+
Remove Qiskit-Aer dependency
|
|
9
|
+
----------------------------
|
|
10
|
+
|
|
11
|
+
- :code:`run()` and :code:`run_cut_circuit()` no longer default to :code:`AerSimulator()`;
|
|
12
|
+
pass a backend or sampler. :code:`qiskit-aer` is no longer installed with QCut, so
|
|
13
|
+
install it separately to simulate.
|
|
14
|
+
- :code:`transpile_subcircuits()` and :code:`transpile_experiments()` read a backend's
|
|
15
|
+
public :code:`target` rather than :code:`_target`, which Aer never fills in. Transpiling
|
|
16
|
+
for :code:`AerSimulator` works, and :code:`run_cut_circuit()` transpiles only for a
|
|
17
|
+
backend with a coupling map.
|
|
18
|
+
|
|
19
|
+
Deduplicated experiment circuits
|
|
20
|
+
--------------------------------
|
|
21
|
+
|
|
22
|
+
- A run now submits each *distinct* experiment circuit once and shares its result with
|
|
23
|
+
every slot it fills.
|
|
24
|
+
- :code:`shots` still means shots per circuit, so a run spends that many on each
|
|
25
|
+
distinct circuit and the total drops by the same factor. What the old total bought
|
|
26
|
+
was re-measuring circuits already measured. Sharing a sample between groups leaves the estimate unbiased,
|
|
27
|
+
since the groups are summed rather than multiplied.
|
|
28
|
+
- :code:`estimate_run()` counts the circuits and shots that are actually submitted, so
|
|
29
|
+
it still matches the run exactly.
|
|
30
|
+
- :code:`CutOptions(deduplicate=False)` restores one submission per slot.
|
|
31
|
+
:code:`CutExperiment.num_circuits` is unchanged: it still counts the circuits the
|
|
32
|
+
decomposition calls for.
|
|
33
|
+
- Experiments whose wire cuts communicate are unaffected for now. Their first wave
|
|
34
|
+
already shares circuits between the groups that differ only in the label they answer.
|
|
35
|
+
|
|
5
36
|
**Version 2.2.0**
|
|
6
37
|
=================
|
|
7
38
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: QCut
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
```
|
|
@@ -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
|
|
@@ -570,12 +575,18 @@ The cost of an experiment run can be checked before submission:
|
|
|
570
575
|
print(ck.estimate_run(cut_experiment, shots=4096, max_batch_size=40))
|
|
571
576
|
```
|
|
572
577
|
|
|
573
|
-
`Experiment will run
|
|
578
|
+
`Experiment will run 31 circuits in 1 jobs with a total of 126976 shots. See the returned object for the breakdown.`
|
|
574
579
|
|
|
575
580
|
```python
|
|
576
|
-
{
|
|
581
|
+
{"job1": JobEstimate(circuits=31, shots=4096)}
|
|
577
582
|
```
|
|
578
583
|
|
|
584
|
+
31 circuits, not the 144 the decomposition calls for: a subcircuit's experiment circuit
|
|
585
|
+
depends only on the QPD terms of the cuts touching it and on the measurement setting
|
|
586
|
+
over its own qubits, so most of them repeat. Each distinct one is submitted once and its
|
|
587
|
+
result shared, which is where the saving comes from. `shots` still means shots per
|
|
588
|
+
circuit. Pass `CutOptions(deduplicate=False)` to submit one circuit per slot instead.
|
|
589
|
+
|
|
579
590
|
Exact for non-cummunicating runs. Runs whose wire cuts communicate get executed in waves so that
|
|
580
591
|
each wave after the first spends its shots on what the one before it measured. For these jobs
|
|
581
592
|
the circuit count is exact and job and shot counts are estimates.
|
|
@@ -604,8 +615,11 @@ Comparing against the exact and noisy expectation values of the original circuit
|
|
|
604
615
|
|
|
605
616
|
```python
|
|
606
617
|
estimator = Estimator()
|
|
607
|
-
exact_expvals = [
|
|
608
|
-
|
|
618
|
+
exact_expvals = [
|
|
619
|
+
e.data.evs
|
|
620
|
+
for e in estimator.run(
|
|
621
|
+
[(x) for x in zip([circuit] * len(observables), observables)]
|
|
622
|
+
).result()
|
|
609
623
|
]
|
|
610
624
|
|
|
611
625
|
tr = transpile(circuit, backend=fake)
|
|
@@ -615,8 +629,11 @@ tr_obs_separate = [
|
|
|
615
629
|
]
|
|
616
630
|
|
|
617
631
|
fake_estimator = BackendEstimator(backend=fake)
|
|
618
|
-
exps = [
|
|
619
|
-
|
|
632
|
+
exps = [
|
|
633
|
+
e.data.evs
|
|
634
|
+
for e in fake_estimator.run(
|
|
635
|
+
[(x) for x in zip([tr] * len(tr_obs_separate), tr_obs_separate)]
|
|
636
|
+
).result()
|
|
620
637
|
]
|
|
621
638
|
```
|
|
622
639
|
|
|
@@ -682,19 +699,19 @@ for how the distribution is reconstructed.
|
|
|
682
699
|
The result is a mapping, so it indexes and plots like a dict, and carries three views:
|
|
683
700
|
|
|
684
701
|
```python
|
|
685
|
-
probs[
|
|
686
|
-
probs.quasi_probabilities()
|
|
687
|
-
probs.nearest_probabilities()
|
|
688
|
-
probs.counts()
|
|
689
|
-
probs.counts(shots=1000)
|
|
702
|
+
probs["00"] # 0.8658
|
|
703
|
+
probs.quasi_probabilities() # the same values, as a plain dict
|
|
704
|
+
probs.nearest_probabilities() # closest true distribution, negatives projected away
|
|
705
|
+
probs.counts() # scaled by the shots the experiment ran at
|
|
706
|
+
probs.counts(shots=1000) # or by any other shots
|
|
690
707
|
```
|
|
691
708
|
|
|
692
709
|
The three dict views report, by default, the ten most likely bitstrings. Pass `top` for a different
|
|
693
710
|
number, or `top=None` for all of them:
|
|
694
711
|
|
|
695
712
|
```python
|
|
696
|
-
probs.quasi_probabilities(top=50)
|
|
697
|
-
probs.counts(shots=1000, top=None)
|
|
713
|
+
probs.quasi_probabilities(top=50) # the fifty most likely
|
|
714
|
+
probs.counts(shots=1000, top=None) # the whole distribution, as before
|
|
698
715
|
```
|
|
699
716
|
|
|
700
717
|
Note that :code:`counts()` sums to :code:`shots` only with :code:`top=None` and
|
|
@@ -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 [
|
|
@@ -17,12 +17,11 @@ from qiskit.circuit import (
|
|
|
17
17
|
from qiskit.circuit.library import get_standard_gate_name_mapping
|
|
18
18
|
from qiskit.converters import circuit_to_dag, dag_to_circuit
|
|
19
19
|
from qiskit.primitives import BaseSamplerV2
|
|
20
|
-
from qiskit_aer import AerSimulator
|
|
21
20
|
|
|
22
21
|
from QCut.cutcircuit import CutCircuit, CutExperiment
|
|
23
22
|
from QCut.cutting.circuit_preparation import get_locations_and_subcircuits
|
|
24
23
|
from QCut.errors.qcuterror import QCutError
|
|
25
|
-
from QCut.execution.backend_utility import transpile_subcircuits
|
|
24
|
+
from QCut.execution.backend_utility import has_topology, transpile_subcircuits
|
|
26
25
|
from QCut.execution.basis_transform import (
|
|
27
26
|
_combine_pauli_ops,
|
|
28
27
|
_get_obs_subcircuits,
|
|
@@ -56,7 +55,12 @@ from QCut.qpd.qpd_operations import (
|
|
|
56
55
|
qpd_for_bundle,
|
|
57
56
|
sample_qpd_combinations,
|
|
58
57
|
)
|
|
59
|
-
from QCut.utils.circuit_utils import
|
|
58
|
+
from QCut.utils.circuit_utils import (
|
|
59
|
+
_remove_obsm,
|
|
60
|
+
_remove_obsm_2,
|
|
61
|
+
circuit_fingerprint,
|
|
62
|
+
compact_qpd_register,
|
|
63
|
+
)
|
|
60
64
|
|
|
61
65
|
logger: logging.Logger = logging.getLogger(__name__)
|
|
62
66
|
|
|
@@ -906,6 +910,13 @@ def _run_communicating(cut_experiment, shots, backend, max_batch_size, run_optio
|
|
|
906
910
|
The budget is not divided evenly between the waves. Sharing makes a measuring shot
|
|
907
911
|
worth more than a preparing one, so wave zero takes :data:`MEASURE_SHARE` of the run
|
|
908
912
|
and the later waves divide the rest. The total is unchanged either way.
|
|
913
|
+
|
|
914
|
+
TODO: the waves after the first do not share circuits at all. ``_later_wave_jobs``
|
|
915
|
+
gives every group a job of its own, where :func:`_deduplicate` would collapse the
|
|
916
|
+
groups holding the same circuit as wave zero's ``plan.keys`` already does. It is not
|
|
917
|
+
the same change as the plain path's, because each group carries its own shot
|
|
918
|
+
allocation by then, so the shared job would have to run at their sum and be scaled
|
|
919
|
+
back per group.
|
|
909
920
|
"""
|
|
910
921
|
plan = cut_experiment.plan
|
|
911
922
|
results: list[list[dict[int, CircuitResult]]] = [
|
|
@@ -1049,6 +1060,83 @@ def _projected_wave_jobs(cut_experiment, wave: int, shots: int):
|
|
|
1049
1060
|
]
|
|
1050
1061
|
|
|
1051
1062
|
|
|
1063
|
+
#: One slot of an experiment: which group, which measurement setting, which subcircuit.
|
|
1064
|
+
Slot = tuple[int, int, int]
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _runnable(cut_experiment) -> tuple[list, list]:
|
|
1068
|
+
"""Split an experiment's slots into the ones worth submitting and the ones not.
|
|
1069
|
+
|
|
1070
|
+
A QPD term that measures nothing on a subcircuit leaves a circuit with no
|
|
1071
|
+
measurements at all. There is nothing to run, and its counts are known: every shot
|
|
1072
|
+
reads zero. Those are filled in directly rather than submitted.
|
|
1073
|
+
|
|
1074
|
+
Returns:
|
|
1075
|
+
The slots to submit, as ``(slot, circuit)``, and the empty ones, as
|
|
1076
|
+
``(slot, num_clbits)``. Both in group, setting, subcircuit order.
|
|
1077
|
+
"""
|
|
1078
|
+
runnable: list[tuple[Slot, QuantumCircuit]] = []
|
|
1079
|
+
empty: list[tuple[Slot, int]] = []
|
|
1080
|
+
for group_idx, circuit_group in enumerate(cut_experiment.experiments):
|
|
1081
|
+
for obs_idx, obs_group in enumerate(circuit_group):
|
|
1082
|
+
for sub_idx, subcircuit in obs_group.items():
|
|
1083
|
+
slot = (group_idx, obs_idx, sub_idx)
|
|
1084
|
+
if _has_measurements(subcircuit):
|
|
1085
|
+
runnable.append((slot, subcircuit))
|
|
1086
|
+
else:
|
|
1087
|
+
empty.append((slot, subcircuit.num_clbits))
|
|
1088
|
+
return runnable, empty
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def _deduplicate(runnable: list, enabled: bool) -> list[tuple[list, QuantumCircuit]]:
|
|
1092
|
+
"""Group slots filled by the same circuit, so it is only submitted once.
|
|
1093
|
+
|
|
1094
|
+
A subcircuit's experiment circuit depends on the QPD terms of the cuts touching that
|
|
1095
|
+
subcircuit and on the measurement setting restricted to its own qubits, neither of
|
|
1096
|
+
which most of an experiment's groups change. The slot count grows exponentially in
|
|
1097
|
+
the number of pieces and the distinct count linearly, so almost all of a large run
|
|
1098
|
+
is the same handful of circuits submitted over and over.
|
|
1099
|
+
|
|
1100
|
+
The key carries the subcircuit index as well as the fingerprint. The estimator takes
|
|
1101
|
+
one factor per subcircuit from each group and multiplies them, so two factors of a
|
|
1102
|
+
single product sharing a sample would give ``E[X**2]`` where ``E[X]**2`` is meant.
|
|
1103
|
+
Keying on the subcircuit makes that impossible, since a group holds each subcircuit
|
|
1104
|
+
once. Sharing between groups is a different matter: groups are summed rather than
|
|
1105
|
+
multiplied, so it leaves the estimate unbiased and moves only its variance.
|
|
1106
|
+
|
|
1107
|
+
Two subcircuits of a symmetric circuit really can produce identical experiment
|
|
1108
|
+
circuits, so this is not a theoretical concern.
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
``(slots, circuit)`` per distinct circuit, in the order first seen, so two runs
|
|
1112
|
+
of one experiment submit the same circuits in the same order.
|
|
1113
|
+
"""
|
|
1114
|
+
if not enabled:
|
|
1115
|
+
return [([slot], circuit) for slot, circuit in runnable]
|
|
1116
|
+
|
|
1117
|
+
cache: dict = {}
|
|
1118
|
+
distinct: dict[tuple, tuple[list, QuantumCircuit]] = {}
|
|
1119
|
+
for slot, circuit in runnable:
|
|
1120
|
+
key = (slot[2], circuit_fingerprint(circuit, cache))
|
|
1121
|
+
hit = distinct.get(key)
|
|
1122
|
+
if hit is None:
|
|
1123
|
+
distinct[key] = ([slot], circuit)
|
|
1124
|
+
else:
|
|
1125
|
+
hit[0].append(slot)
|
|
1126
|
+
return list(distinct.values())
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _submission(cut_experiment) -> tuple[list[tuple[list, QuantumCircuit]], list]:
|
|
1130
|
+
"""What a run of this experiment actually submits, and what it fills in itself."""
|
|
1131
|
+
runnable, empty = _runnable(cut_experiment)
|
|
1132
|
+
jobs = _deduplicate(runnable, cut_experiment.options.deduplicate)
|
|
1133
|
+
if len(jobs) < len(runnable):
|
|
1134
|
+
logger.info(
|
|
1135
|
+
f"{len(runnable)} experiment circuits hold {len(jobs)} distinct ones"
|
|
1136
|
+
)
|
|
1137
|
+
return jobs, empty
|
|
1138
|
+
|
|
1139
|
+
|
|
1052
1140
|
def estimate_run(
|
|
1053
1141
|
cut_experiment: CutExperiment,
|
|
1054
1142
|
shots: int = DEFAULT_SHOTS,
|
|
@@ -1079,18 +1167,14 @@ def estimate_run(
|
|
|
1079
1167
|
cap = _backend_shot_cap(backend) if backend is not None else None
|
|
1080
1168
|
|
|
1081
1169
|
if cut_experiment.plan is None:
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
for obs_group in group
|
|
1086
|
-
for circuit in obs_group.values()
|
|
1087
|
-
if _has_measurements(circuit)
|
|
1088
|
-
]
|
|
1170
|
+
# Counted through the same pair the run uses, so an estimate cannot drift from
|
|
1171
|
+
# what is submitted.
|
|
1172
|
+
jobs, _empty = _submission(cut_experiment)
|
|
1089
1173
|
return RunEstimate(
|
|
1090
1174
|
_named(
|
|
1091
1175
|
[
|
|
1092
|
-
JobEstimate(len(
|
|
1093
|
-
for start in range(0, len(
|
|
1176
|
+
JobEstimate(len(jobs[start : start + max_batch_size]), shots)
|
|
1177
|
+
for start in range(0, len(jobs), max_batch_size)
|
|
1094
1178
|
]
|
|
1095
1179
|
),
|
|
1096
1180
|
exact=True,
|
|
@@ -1163,7 +1247,7 @@ def run_experiments( # noqa: C901
|
|
|
1163
1247
|
|
|
1164
1248
|
"""
|
|
1165
1249
|
if backend is None:
|
|
1166
|
-
backend
|
|
1250
|
+
raise QCutError("a backend or sampler to run the circuits on is needed")
|
|
1167
1251
|
|
|
1168
1252
|
if cut_experiment.plan is not None:
|
|
1169
1253
|
results = _run_communicating(
|
|
@@ -1172,26 +1256,19 @@ def run_experiments( # noqa: C901
|
|
|
1172
1256
|
return RawResult(results, shots, cut_experiment)
|
|
1173
1257
|
|
|
1174
1258
|
results: list[list[dict[int, CircuitResult]]] = [
|
|
1175
|
-
|
|
1259
|
+
# Seeded in subcircuit order, because the estimator reads a group's subcircuits
|
|
1260
|
+
# back in the order these were filled, and deduplicated circuits fill them in
|
|
1261
|
+
# the order they were submitted instead. The same reason _run_communicating
|
|
1262
|
+
# seeds its own.
|
|
1263
|
+
[{sub: CircuitResult({}) for sub in sorted(obs_group)} for obs_group in group]
|
|
1264
|
+
for group in cut_experiment.experiments
|
|
1176
1265
|
]
|
|
1177
1266
|
|
|
1178
|
-
|
|
1179
|
-
empty_locations: list[tuple[tuple[int, int, int], int]] = []
|
|
1180
|
-
for group_idx, circuit_group in enumerate(cut_experiment.experiments):
|
|
1181
|
-
for obs_idx, obs_group in enumerate(circuit_group):
|
|
1182
|
-
for sub_idx, subcircuit in obs_group.items():
|
|
1183
|
-
key = (group_idx, obs_idx, sub_idx)
|
|
1184
|
-
if _has_measurements(subcircuit):
|
|
1185
|
-
runnable.append((key, subcircuit))
|
|
1186
|
-
else:
|
|
1187
|
-
empty_locations.append((key, subcircuit.num_clbits))
|
|
1267
|
+
jobs, empty_locations = _submission(cut_experiment)
|
|
1188
1268
|
|
|
1189
|
-
num_batches = len(
|
|
1190
|
-
1 if len(runnable) % max_batch_size else 0
|
|
1191
|
-
)
|
|
1269
|
+
num_batches = len(jobs) // max_batch_size + (1 if len(jobs) % max_batch_size else 0)
|
|
1192
1270
|
logger.info(
|
|
1193
|
-
f"Running {len(
|
|
1194
|
-
f" backend {backend} with {shots} shots each"
|
|
1271
|
+
f"Running {len(jobs)} circuits on the backend {backend} with {shots} shots each"
|
|
1195
1272
|
)
|
|
1196
1273
|
logger.info(
|
|
1197
1274
|
"Circuits will be split into "
|
|
@@ -1200,8 +1277,8 @@ def run_experiments( # noqa: C901
|
|
|
1200
1277
|
)
|
|
1201
1278
|
# Submitted before any is collected, so every batch queues at once.
|
|
1202
1279
|
submitted = []
|
|
1203
|
-
for start in range(0, len(
|
|
1204
|
-
batch =
|
|
1280
|
+
for start in range(0, len(jobs), max_batch_size):
|
|
1281
|
+
batch = jobs[start : start + max_batch_size]
|
|
1205
1282
|
job = _submit(
|
|
1206
1283
|
backend,
|
|
1207
1284
|
[circ for _, circ in batch],
|
|
@@ -1214,9 +1291,13 @@ def run_experiments( # noqa: C901
|
|
|
1214
1291
|
|
|
1215
1292
|
for job, batch in submitted:
|
|
1216
1293
|
result = _result(job)
|
|
1217
|
-
for index, (
|
|
1218
|
-
|
|
1219
|
-
|
|
1294
|
+
for index, (slots, _circ) in enumerate(batch):
|
|
1295
|
+
# One result object for the circuit, shared by every slot it fills. It is
|
|
1296
|
+
# frozen and holds the job's result by reference, so there is nothing to
|
|
1297
|
+
# copy and nothing that could be mutated through one slot.
|
|
1298
|
+
circuit_result = CircuitResult(result, index)
|
|
1299
|
+
for group_idx, obs_idx, sub_idx in slots:
|
|
1300
|
+
results[group_idx][obs_idx][sub_idx] = circuit_result
|
|
1220
1301
|
|
|
1221
1302
|
for (group_idx, obs_idx, sub_idx), num_clbits in empty_locations:
|
|
1222
1303
|
results[group_idx][obs_idx][sub_idx] = CircuitResult(
|
|
@@ -1229,7 +1310,7 @@ def run_experiments( # noqa: C901
|
|
|
1229
1310
|
def run_cut_circuit(
|
|
1230
1311
|
cut_circuit: CutCircuit,
|
|
1231
1312
|
observables: ObservablesLike | None = None,
|
|
1232
|
-
backend=
|
|
1313
|
+
backend=None,
|
|
1233
1314
|
max_batch_size: int = 100,
|
|
1234
1315
|
options: CutOptions | None = None,
|
|
1235
1316
|
shots: int = DEFAULT_SHOTS,
|
|
@@ -1243,7 +1324,9 @@ def run_cut_circuit(
|
|
|
1243
1324
|
operations and cut locations
|
|
1244
1325
|
observables: the observables to estimate, taken as qiskit's estimator takes
|
|
1245
1326
|
them. See :func:`get_experiment_circuits`.
|
|
1246
|
-
backend: backend to
|
|
1327
|
+
backend: backend or V2 sampler to run the experiment circuits on. A device
|
|
1328
|
+
with a coupling map gets the subcircuits transpiled for it first. A
|
|
1329
|
+
sampler is not transpiled for, so it must take the circuits as built.
|
|
1247
1330
|
max_batch_size (int): maximum number of circuits submitted per backend.run
|
|
1248
1331
|
call (optional)
|
|
1249
1332
|
options (CutOptions): configuration, overriding what the CutCircuit carries
|
|
@@ -1269,16 +1352,13 @@ def run_cut_circuit(
|
|
|
1269
1352
|
if options is not None:
|
|
1270
1353
|
cut_circuit.options = options
|
|
1271
1354
|
|
|
1272
|
-
if
|
|
1273
|
-
|
|
1274
|
-
cut_circuit, backend, optimization_level=3
|
|
1275
|
-
)
|
|
1355
|
+
if backend is None:
|
|
1356
|
+
raise QCutError("a backend or sampler to run the circuits on is needed")
|
|
1276
1357
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
cut_experiment = get_experiment_circuits(cut_circuit, observables, qubits)
|
|
1358
|
+
if has_topology(backend):
|
|
1359
|
+
cut_circuit = transpile_subcircuits(cut_circuit, backend, optimization_level=3)
|
|
1360
|
+
|
|
1361
|
+
cut_experiment = get_experiment_circuits(cut_circuit, observables, qubits)
|
|
1282
1362
|
|
|
1283
1363
|
results = run_experiments(
|
|
1284
1364
|
cut_experiment,
|
|
@@ -1297,7 +1377,7 @@ def run_cut_circuit(
|
|
|
1297
1377
|
def run(
|
|
1298
1378
|
circuit: QuantumCircuit,
|
|
1299
1379
|
observables: ObservablesLike | None = None,
|
|
1300
|
-
backend=
|
|
1380
|
+
backend=None,
|
|
1301
1381
|
max_batch_size: int = 100,
|
|
1302
1382
|
options: CutOptions | None = None,
|
|
1303
1383
|
shots: int = DEFAULT_SHOTS,
|
|
@@ -1310,7 +1390,9 @@ def run(
|
|
|
1310
1390
|
circuit (QuantumCircuit): circuit with cut experiments
|
|
1311
1391
|
observables: the observables to estimate, taken as qiskit's estimator takes
|
|
1312
1392
|
them. See :func:`get_experiment_circuits`.
|
|
1313
|
-
backend: backend to
|
|
1393
|
+
backend: backend or V2 sampler to run the experiment circuits on. A device
|
|
1394
|
+
with a coupling map gets the subcircuits transpiled for it first. A
|
|
1395
|
+
sampler is not transpiled for, so it must take the circuits as built.
|
|
1314
1396
|
max_batch_size (int): maximum number of circuits submitted per backend.run
|
|
1315
1397
|
call (optional)
|
|
1316
1398
|
options (CutOptions): configuration for the run (optional)
|