From ce4df59d502fd463760f95e22e58c1e4dd9a1240 Mon Sep 17 00:00:00 2001 From: ikkoham Date: Fri, 2 Dec 2022 19:59:49 +0900 Subject: [PATCH 1/5] initial commit --- qiskit/primitives/utils.py | 17 +++++++++++++---- ...y-supports-controlflow-a956ebd2fcebaece.yaml | 6 ++++++ test/python/primitives/test_sampler.py | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml diff --git a/qiskit/primitives/utils.py b/qiskit/primitives/utils.py index e2270b874297..f2fa4eb65dca 100644 --- a/qiskit/primitives/utils.py +++ b/qiskit/primitives/utils.py @@ -14,6 +14,8 @@ """ from __future__ import annotations +from collections.abc import Iterable + import numpy as np from qiskit.circuit import Instruction, ParameterExpression, QuantumCircuit @@ -125,6 +127,16 @@ def _bits_key(bits: tuple[Bit, ...], circuit: QuantumCircuit) -> tuple: ) +def _format_params(param): + if isinstance(param, np.ndarray): + return param.data.tobytes() + elif isinstance(param, QuantumCircuit): + return _circuit_key(param) + elif isinstance(param, Iterable): + return tuple(param) + return param + + def _circuit_key(circuit: QuantumCircuit, functional: bool = True) -> tuple: """Private key function for QuantumCircuit. @@ -147,10 +159,7 @@ def _circuit_key(circuit: QuantumCircuit, functional: bool = True) -> tuple: _bits_key(data.qubits, circuit), # qubits _bits_key(data.clbits, circuit), # clbits data.operation.name, # operation.name - tuple( - param.data.tobytes() if isinstance(param, np.ndarray) else param - for param in data.operation.params - ), # operation.params + tuple(_format_params(param) for param in data.operation.params), # operation.params ) for data in circuit.data ), diff --git a/releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml b/releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml new file mode 100644 index 000000000000..539760ed7f24 --- /dev/null +++ b/releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Primitives support the dynamic circuits with control flow. + If backend supports it, users can run the circuits using :class:`~BackendSampler` + and :class:`~BackendEstimator`. diff --git a/test/python/primitives/test_sampler.py b/test/python/primitives/test_sampler.py index 2c526facf533..7b537b1450b0 100644 --- a/test/python/primitives/test_sampler.py +++ b/test/python/primitives/test_sampler.py @@ -12,6 +12,7 @@ """Tests for Sampler.""" +import json import unittest from test import combine @@ -783,6 +784,19 @@ def test_circuit_with_unitary(self): sampler_result = sampler.run([circuit]).result() self.assertDictAlmostEqual(sampler_result.quasi_dists[0], {0: 1, 1: 0}) + def test_circuit_key_controlflow(self): + """Test for a circuit with control flow.""" + qc = QuantumCircuit(2, 1) + + with qc.for_loop(range(5)): + qc.h(0) + qc.cx(0, 1) + qc.measure(0, 0) + qc.break_loop().c_if(0, True) + + self.assertIsInstance(hash(_circuit_key(qc)), int) + self.assertIsInstance(json.dumps(_circuit_key(qc)), str) + if __name__ == "__main__": unittest.main() From e82577a265c203804d8d7415cd41dd7f3c1e3128 Mon Sep 17 00:00:00 2001 From: ikkoham Date: Mon, 16 Jan 2023 22:49:30 +0900 Subject: [PATCH 2/5] update releasenote --- .../circuit-key-supports-controlflow-a956ebd2fcebaece.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml b/releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml index 539760ed7f24..d3a41f5c4d8d 100644 --- a/releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml +++ b/releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml @@ -2,5 +2,5 @@ features: - | Primitives support the dynamic circuits with control flow. - If backend supports it, users can run the circuits using :class:`~BackendSampler` - and :class:`~BackendEstimator`. + This improvement is applied to all :class:`~BaseSampler` and :class:`~BaseEstimator` subclasses, + if the backend support running the dynamic circuits. From 3b9e5f950ba3ccbcd8fb93bdf383ab5125ea2790 Mon Sep 17 00:00:00 2001 From: ikkoham Date: Tue, 17 Jan 2023 11:12:41 +0900 Subject: [PATCH 3/5] add test with aer and move tests --- .../python/primitives/test_backend_sampler.py | 20 ++++++- test/python/primitives/test_primitive.py | 55 ++++++++++++++++++- test/python/primitives/test_sampler.py | 48 +--------------- 3 files changed, 73 insertions(+), 50 deletions(-) diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py index ccf420afbbbd..8b6e7aba7567 100644 --- a/test/python/primitives/test_backend_sampler.py +++ b/test/python/primitives/test_backend_sampler.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 2023. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -24,6 +24,7 @@ from qiskit.providers import JobStatus, JobV1 from qiskit.providers.fake_provider import FakeNairobi, FakeNairobiV2 from qiskit.test import QiskitTestCase +from qiskit.utils import optionals BACKENDS = [FakeNairobi(), FakeNairobiV2()] @@ -319,6 +320,23 @@ def test_primitive_job_size_limit_backend_v1(self): self.assertDictAlmostEqual(result.quasi_dists[0], {0: 1}, 0.1) self.assertDictAlmostEqual(result.quasi_dists[1], {1: 1}, 0.1) + @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") + def test_circuit_with_dynamic_circuit(self): + """Test BackendSampler with QuantumCircuit with a dynamic circuit""" + from qiskit_aer import Aer + + qc = QuantumCircuit(2, 1) + + with qc.for_loop(range(5)): + qc.h(0) + qc.cx(0, 1) + qc.measure(0, 0) + qc.break_loop().c_if(0, True) + + sampler = BackendSampler(Aer.get_backend("aer_simulator"), skip_transpilation=True) + result = sampler.run(qc).result() + print(result) + if __name__ == "__main__": unittest.main() diff --git a/test/python/primitives/test_primitive.py b/test/python/primitives/test_primitive.py index 59036b963bd8..cc60a17abc7e 100644 --- a/test/python/primitives/test_primitive.py +++ b/test/python/primitives/test_primitive.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 2023. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -12,12 +12,16 @@ """Tests for BasePrimitive.""" -from ddt import ddt, data, unpack +import json -from numpy import array, int32, int64, float32, float64 +from ddt import data, ddt, unpack +from numpy import array, float32, float64, int32, int64 +from qiskit import QuantumCircuit, pulse, transpile from qiskit.circuit.random import random_circuit from qiskit.primitives.base.base_primitive import BasePrimitive +from qiskit.primitives.utils import _circuit_key +from qiskit.providers.fake_provider import FakeAlmaden from qiskit.test import QiskitTestCase @@ -110,3 +114,48 @@ def test_value_error(self): """Test value error if no parameter_values or default are provided.""" with self.assertRaises(ValueError): BasePrimitive._validate_parameter_values(None) + + +class TestCircuitKey(QiskitTestCase): + """Tests for _circuit_key function""" + + def test_different_circuits(self): + """Test collision of quantum circuits.""" + + with self.subTest("Ry circuit"): + + def test_func(n): + qc = QuantumCircuit(1, 1, name="foo") + qc.ry(n, 0) + return qc + + keys = [_circuit_key(test_func(i)) for i in range(5)] + self.assertEqual(len(keys), len(set(keys))) + + with self.subTest("pulse circuit"): + + def test_with_scheduling(n): + custom_gate = pulse.Schedule(name="custom_x_gate") + custom_gate.insert( + 0, pulse.Play(pulse.Constant(160 * n, 0.1), pulse.DriveChannel(0)), inplace=True + ) + qc = QuantumCircuit(1) + qc.x(0) + qc.add_calibration("x", qubits=(0,), schedule=custom_gate) + return transpile(qc, FakeAlmaden(), scheduling_method="alap") + + keys = [_circuit_key(test_with_scheduling(i)) for i in range(1, 5)] + self.assertEqual(len(keys), len(set(keys))) + + def test_circuit_key_controlflow(self): + """Test for a circuit with control flow.""" + qc = QuantumCircuit(2, 1) + + with qc.for_loop(range(5)): + qc.h(0) + qc.cx(0, 1) + qc.measure(0, 0) + qc.break_loop().c_if(0, True) + + self.assertIsInstance(hash(_circuit_key(qc)), int) + self.assertIsInstance(json.dumps(_circuit_key(qc)), str) diff --git a/test/python/primitives/test_sampler.py b/test/python/primitives/test_sampler.py index 7b537b1450b0..314c2aa7248c 100644 --- a/test/python/primitives/test_sampler.py +++ b/test/python/primitives/test_sampler.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 2023. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -12,22 +12,19 @@ """Tests for Sampler.""" -import json import unittest from test import combine import numpy as np from ddt import ddt -from qiskit import QuantumCircuit, pulse, transpile +from qiskit import QuantumCircuit from qiskit.circuit import Parameter from qiskit.circuit.library import RealAmplitudes from qiskit.exceptions import QiskitError from qiskit.extensions.unitary import UnitaryGate from qiskit.primitives import Sampler, SamplerResult -from qiskit.primitives.utils import _circuit_key from qiskit.providers import JobStatus, JobV1 -from qiskit.providers.fake_provider import FakeAlmaden from qiskit.test import QiskitTestCase @@ -744,34 +741,6 @@ def test_options(self): self._compare_probs(result.quasi_dists, target) self.assertEqual(result.quasi_dists[0].shots, 1024) - def test_different_circuits(self): - """Test collision of quantum circuits.""" - - with self.subTest("Ry circuit"): - - def test_func(n): - qc = QuantumCircuit(1, 1, name="foo") - qc.ry(n, 0) - return qc - - keys = [_circuit_key(test_func(i)) for i in range(5)] - self.assertEqual(len(keys), len(set(keys))) - - with self.subTest("pulse circuit"): - - def test_with_scheduling(n): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(160 * n, 0.1), pulse.DriveChannel(0)), inplace=True - ) - qc = QuantumCircuit(1) - qc.x(0) - qc.add_calibration("x", qubits=(0,), schedule=custom_gate) - return transpile(qc, FakeAlmaden(), scheduling_method="alap") - - keys = [_circuit_key(test_with_scheduling(i)) for i in range(1, 5)] - self.assertEqual(len(keys), len(set(keys))) - def test_circuit_with_unitary(self): """Test for circuit with unitary gate.""" gate = UnitaryGate(np.eye(2)) @@ -784,19 +753,6 @@ def test_circuit_with_unitary(self): sampler_result = sampler.run([circuit]).result() self.assertDictAlmostEqual(sampler_result.quasi_dists[0], {0: 1, 1: 0}) - def test_circuit_key_controlflow(self): - """Test for a circuit with control flow.""" - qc = QuantumCircuit(2, 1) - - with qc.for_loop(range(5)): - qc.h(0) - qc.cx(0, 1) - qc.measure(0, 0) - qc.break_loop().c_if(0, True) - - self.assertIsInstance(hash(_circuit_key(qc)), int) - self.assertIsInstance(json.dumps(_circuit_key(qc)), str) - if __name__ == "__main__": unittest.main() From eaa5f4bccb245e0ce8beacae046a62de65cd34e0 Mon Sep 17 00:00:00 2001 From: ikkoham Date: Tue, 17 Jan 2023 11:31:25 +0900 Subject: [PATCH 4/5] fix test --- test/python/primitives/test_backend_sampler.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py index 4d4eaddff10c..ef3172eab96e 100644 --- a/test/python/primitives/test_backend_sampler.py +++ b/test/python/primitives/test_backend_sampler.py @@ -333,9 +333,12 @@ def test_circuit_with_dynamic_circuit(self): qc.measure(0, 0) qc.break_loop().c_if(0, True) - sampler = BackendSampler(Aer.get_backend("aer_simulator"), skip_transpilation=True) + backend = Aer.get_backend("aer_simulator") + backend.set_options(seed_simulator=15) + sampler = BackendSampler(backend, skip_transpilation=True) + sampler.set_transpile_options(seed_transpiler=15) result = sampler.run(qc).result() - print(result) + self.assertDictAlmostEqual(result.quasi_dists[0], {0: 0.5029296875, 1: 0.4970703125}) def test_sequential_run(self): """Test sequential run.""" From b7e3ebc840c71b398d5d9cd4e9dbb7d11c859b83 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 17 Jan 2023 11:16:22 +0000 Subject: [PATCH 5/5] Reword release note --- ...ircuit-key-supports-controlflow-a956ebd2fcebaece.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml b/releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml index d3a41f5c4d8d..963506455bbb 100644 --- a/releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml +++ b/releasenotes/notes/circuit-key-supports-controlflow-a956ebd2fcebaece.yaml @@ -1,6 +1,9 @@ --- features: - | - Primitives support the dynamic circuits with control flow. - This improvement is applied to all :class:`~BaseSampler` and :class:`~BaseEstimator` subclasses, - if the backend support running the dynamic circuits. + Primitives may now support dynamic circuits with control flow, if the particular + provider's implementation can support them. Previously, the + :class:`~BaseSampler` and :class:`~BaseEstimator` base classes could not correctly + normalize such circuits. This change does not automatically make all + primitives support dynamic circuits, but it does make it possible for them + to be supported by downstream providers.