From b351dfa9e0de6307693a364c977487fb2ad41593 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 21 Oct 2022 16:43:30 +0100 Subject: [PATCH 1/5] Speed up `random_circuit` There is no need to use the slower `append` (which adds a lot of checking overhead for large circuits) since we fully control the construction of the circuit. This also overhauls all the randomisation components to produce all the necessary randomness for a given layer in a single go. This massively reduces the Python-space and Numpy-dispatch overhead of the nested loop. The changes to the randomisation in this new form mean that on average, more gates will be generated per circuit, because the output will choose 1q gates more frequently than it chooses 2q, which in turn will be more frequent than 3q, but each layer of the "depth" still has to involve every qubit. The change is because the likelihood of choosing a gate with a given number of qubits is now proportional to how many different gates of that number of qubits there are in the options. For sample timings, the call `random_circuit(433, 1000, seed=1)` went from 15.2s on my machine (macOS Python 3.10, Intel i7 @ 2.3 GHz) down to 2.3s, so a speed-up of about 6-7x. --- qiskit/circuit/random/utils.py | 160 +++++++++++------- ...edup-random-circuits-8d3b724cce1faaad.yaml | 10 ++ 2 files changed, 105 insertions(+), 65 deletions(-) create mode 100644 releasenotes/notes/speedup-random-circuits-8d3b724cce1faaad.yaml diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index ca144cb05f2e..da11d4b74b5f 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -14,13 +14,10 @@ import numpy as np -from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit +from qiskit.circuit import ClassicalRegister, QuantumCircuit, CircuitInstruction from qiskit.circuit import Reset from qiskit.circuit.library.standard_gates import ( IGate, - U1Gate, - U2Gate, - U3Gate, XGate, YGate, ZGate, @@ -32,13 +29,13 @@ RXGate, RYGate, RZGate, + UGate, CXGate, CYGate, CZGate, CHGate, + CUGate, CRZGate, - CU1Gate, - CU3Gate, SwapGate, RZZGate, CCXGate, @@ -77,81 +74,114 @@ def random_circuit( Raises: CircuitError: when invalid options given """ + if num_qubits == 0: + return QuantumCircuit() if max_operands < 1 or max_operands > 3: raise CircuitError("max_operands must be between 1 and 3") - - one_q_ops = [ - IGate, - U1Gate, - U2Gate, - U3Gate, - XGate, - YGate, - ZGate, - HGate, - SGate, - SdgGate, - TGate, - TdgGate, - RXGate, - RYGate, - RZGate, + max_operands = max_operands if num_qubits > max_operands else num_qubits + + gates_1q = [ + # (Gate class, number of qubits, number of parameters) + (IGate, 1, 0), + (XGate, 1, 0), + (YGate, 1, 0), + (ZGate, 1, 0), + (HGate, 1, 0), + (SGate, 1, 0), + (SdgGate, 1, 0), + (TGate, 1, 0), + (TdgGate, 1, 0), + (RXGate, 1, 1), + (RYGate, 1, 1), + (RZGate, 1, 1), + (UGate, 1, 3), + ] + if reset: + gates_1q.append((Reset, 1, 0)) + gates_2q = [ + (CXGate, 2, 0), + (CYGate, 2, 0), + (CZGate, 2, 0), + (CHGate, 2, 0), + (CRZGate, 2, 1), + (SwapGate, 2, 0), + (RZZGate, 2, 1), + (CUGate, 2, 4), ] - one_param = [U1Gate, RXGate, RYGate, RZGate, RZZGate, CU1Gate, CRZGate] - two_param = [U2Gate] - three_param = [U3Gate, CU3Gate] - two_q_ops = [CXGate, CYGate, CZGate, CHGate, CRZGate, CU1Gate, CU3Gate, SwapGate, RZZGate] - three_q_ops = [CCXGate, CSwapGate] + gates_3q = [(CCXGate, 3, 0), (CSwapGate, 3, 0)] + + gates = gates_1q.copy() + if max_operands >= 2: + gates.extend(gates_2q) + if max_operands >= 3: + gates.extend(gates_3q) + gates = np.array( + gates, dtype=[("class", object), ("num_qubits", np.int64), ("num_params", np.int64)] + ) + gates_1q = np.array(gates_1q, dtype=gates.dtype) - qr = QuantumRegister(num_qubits, "q") qc = QuantumCircuit(num_qubits) if measure or conditional: cr = ClassicalRegister(num_qubits, "c") qc.add_register(cr) - if reset: - one_q_ops += [Reset] - if seed is None: seed = np.random.randint(0, np.iinfo(np.int32).max) rng = np.random.default_rng(seed) - # apply arbitrary random operations at every depth + qubits = np.array(qc.qubits, dtype=object, copy=True) + + # Apply arbitrary random operations in layers across all qubits. for _ in range(depth): - # choose either 1, 2, or 3 qubits for the operation - remaining_qubits = list(range(num_qubits)) - rng.shuffle(remaining_qubits) - while remaining_qubits: - max_possible_operands = min(len(remaining_qubits), max_operands) - num_operands = rng.choice(range(max_possible_operands)) + 1 - operands = [remaining_qubits.pop() for _ in range(num_operands)] - if num_operands == 1: - operation = rng.choice(one_q_ops) - elif num_operands == 2: - operation = rng.choice(two_q_ops) - elif num_operands == 3: - operation = rng.choice(three_q_ops) - if operation in one_param: - num_angles = 1 - elif operation in two_param: - num_angles = 2 - elif operation in three_param: - num_angles = 3 - else: - num_angles = 0 - angles = [rng.uniform(0, 2 * np.pi) for x in range(num_angles)] - register_operands = [qr[i] for i in operands] - op = operation(*angles) - - # with some low probability, condition on classical bit values - if conditional and rng.choice(range(10)) == 0: - value = rng.integers(0, np.power(2, num_qubits)) - op.condition = (cr, value) - - qc.append(op, register_operands) + # We generate all the randomness for the layer in one go, to avoid many separate calls to + # the randomisation routines, which can be fairly slow. + + # This reliably draws too much randomness, but it's less expensive than looping over more + # calls to the rng. After, trim it down by finding the point when we've used all the qubits. + gate_specs = rng.choice(gates, size=len(qubits)) + cumulative_qubits = np.cumsum(gate_specs["num_qubits"], dtype=np.int64) + max_index = np.searchsorted(cumulative_qubits, num_qubits, side="right") + gate_specs = gate_specs[:max_index] + slack = num_qubits - cumulative_qubits[max_index - 1] + if slack: + gate_specs = np.hstack((gate_specs, rng.choice(gates_1q, size=slack))) + q_indices = np.empty(len(gate_specs) + 1, dtype=np.int64) + p_indices = np.empty(len(gate_specs) + 1, dtype=np.int64) + q_indices[0] = p_indices[0] = 0 + np.cumsum(gate_specs["num_qubits"], out=q_indices[1:]) + np.cumsum(gate_specs["num_params"], out=p_indices[1:]) + parameters = rng.uniform(0, 2 * np.pi, size=p_indices[-1]) + rng.shuffle(qubits) + + # We've now generated everything we're going to need. Now just to add everything. + if conditional: + is_conditional = rng.random(size=len(gate_specs)) < 0.1 + condition_values = rng.integers( + 0, 1 << min(num_qubits, 63), size=np.count_nonzero(is_conditional) + ) + c_ptr = 0 + for gate, q_start, q_end, p_start, p_end, is_cond in zip( + gate_specs["class"], + q_indices[:-1], + q_indices[1:], + p_indices[:-1], + p_indices[1:], + is_conditional, + ): + operation = gate(*parameters[p_start:p_end]) + if is_cond: + operation.condition = (cr, condition_values[c_ptr]) + c_ptr += 1 + qc._append(CircuitInstruction(operation=operation, qubits=qubits[q_start:q_end])) + else: + for gate, q_start, q_end, p_start, p_end in zip( + gate_specs["class"], q_indices[:-1], q_indices[1:], p_indices[:-1], p_indices[1:] + ): + operation = gate(*parameters[p_start:p_end]) + qc._append(CircuitInstruction(operation=operation, qubits=qubits[q_start:q_end])) if measure: - qc.measure(qr, cr) + qc.measure(qc.qubits, cr) return qc diff --git a/releasenotes/notes/speedup-random-circuits-8d3b724cce1faaad.yaml b/releasenotes/notes/speedup-random-circuits-8d3b724cce1faaad.yaml new file mode 100644 index 000000000000..a8943e851f17 --- /dev/null +++ b/releasenotes/notes/speedup-random-circuits-8d3b724cce1faaad.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Random-circuit generation with ``qiskit.circuit.random.random_circuit`` is + now significantly faster for large circuits. +upgrade: + - | + The exact circuit returned by ``qiskit.circuit.random.random_circuit`` for a + given seed has changed. This is due to efficiency improvements in the + internal random-number generation for the function. From 205c0768df3206724c26b420e10aac8f343e9e83 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 27 Oct 2022 13:56:59 +0100 Subject: [PATCH 2/5] Add test of large-circuit condition generation --- test/python/circuit/test_random_circuit.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/python/circuit/test_random_circuit.py b/test/python/circuit/test_random_circuit.py index 93ac4f6de729..db5acb851012 100644 --- a/test/python/circuit/test_random_circuit.py +++ b/test/python/circuit/test_random_circuit.py @@ -52,3 +52,14 @@ def test_random_circuit_conditional_reset(self): circ = random_circuit(num_qubits, depth, conditional=True, reset=True, seed=5) self.assertEqual(circ.width(), 2 * num_qubits) self.assertIn("reset", circ.count_ops()) + + def test_large_conditional(self): + """Test that conditions do not fail with large conditionals. Regression test of gh-6994.""" + # The main test is that this call actually returns without raising an exception. + circ = random_circuit(64, 2, conditional=True, seed=0) + # Test that at least one instruction had a condition generated. It's possible that this + # fails due to very bad luck with the random seed - if so, change the seed to ensure that a + # condition _is_ generated, because we need to test that generation doesn't error. + conditions = (getattr(instruction.operation, "condition", None) for instruction in circ) + conditions = [x for x in conditions if x is not None] + self.assertNotEqual(conditions, []) From 8efbb6fde5e0fb004136803a8291f7f5a58747fd Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 27 Oct 2022 18:22:02 +0100 Subject: [PATCH 3/5] Use all gates in the standard library This deliberately does not use the `get_standard_gate_name_mapping` function, in order to avoid changes to that function breaking RNG compatibility in this unrelated function. --- qiskit/circuit/random/utils.py | 110 ++++++++++-------- ...edup-random-circuits-8d3b724cce1faaad.yaml | 4 + 2 files changed, 64 insertions(+), 50 deletions(-) diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index da11d4b74b5f..729d2fb78621 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -16,36 +16,12 @@ from qiskit.circuit import ClassicalRegister, QuantumCircuit, CircuitInstruction from qiskit.circuit import Reset -from qiskit.circuit.library.standard_gates import ( - IGate, - XGate, - YGate, - ZGate, - HGate, - SGate, - SdgGate, - TGate, - TdgGate, - RXGate, - RYGate, - RZGate, - UGate, - CXGate, - CYGate, - CZGate, - CHGate, - CUGate, - CRZGate, - SwapGate, - RZZGate, - CCXGate, - CSwapGate, -) +from qiskit.circuit.library import standard_gates from qiskit.circuit.exceptions import CircuitError def random_circuit( - num_qubits, depth, max_operands=3, measure=False, conditional=False, reset=False, seed=None + num_qubits, depth, max_operands=4, measure=False, conditional=False, reset=False, seed=None ): """Generate random circuit of arbitrary size and form. @@ -76,45 +52,79 @@ def random_circuit( """ if num_qubits == 0: return QuantumCircuit() - if max_operands < 1 or max_operands > 3: - raise CircuitError("max_operands must be between 1 and 3") + if max_operands < 1 or max_operands > 4: + raise CircuitError("max_operands must be between 1 and 4") max_operands = max_operands if num_qubits > max_operands else num_qubits gates_1q = [ # (Gate class, number of qubits, number of parameters) - (IGate, 1, 0), - (XGate, 1, 0), - (YGate, 1, 0), - (ZGate, 1, 0), - (HGate, 1, 0), - (SGate, 1, 0), - (SdgGate, 1, 0), - (TGate, 1, 0), - (TdgGate, 1, 0), - (RXGate, 1, 1), - (RYGate, 1, 1), - (RZGate, 1, 1), - (UGate, 1, 3), + (standard_gates.IGate, 1, 0), + (standard_gates.SXGate, 1, 0), + (standard_gates.XGate, 1, 0), + (standard_gates.RZGate, 1, 1), + (standard_gates.RGate, 1, 2), + (standard_gates.HGate, 1, 0), + (standard_gates.PhaseGate, 1, 1), + (standard_gates.RXGate, 1, 1), + (standard_gates.RYGate, 1, 1), + (standard_gates.SGate, 1, 0), + (standard_gates.SdgGate, 1, 0), + (standard_gates.SXdgGate, 1, 0), + (standard_gates.TGate, 1, 0), + (standard_gates.TdgGate, 1, 0), + (standard_gates.UGate, 1, 3), + (standard_gates.U1Gate, 1, 1), + (standard_gates.U2Gate, 1, 2), + (standard_gates.U3Gate, 1, 3), + (standard_gates.YGate, 1, 0), + (standard_gates.ZGate, 1, 0), ] if reset: gates_1q.append((Reset, 1, 0)) gates_2q = [ - (CXGate, 2, 0), - (CYGate, 2, 0), - (CZGate, 2, 0), - (CHGate, 2, 0), - (CRZGate, 2, 1), - (SwapGate, 2, 0), - (RZZGate, 2, 1), - (CUGate, 2, 4), + (standard_gates.CXGate, 2, 0), + (standard_gates.DCXGate, 2, 0), + (standard_gates.CHGate, 2, 0), + (standard_gates.CPhaseGate, 2, 1), + (standard_gates.CRXGate, 2, 1), + (standard_gates.CRYGate, 2, 1), + (standard_gates.CRZGate, 2, 1), + (standard_gates.CSXGate, 2, 0), + (standard_gates.CUGate, 2, 4), + (standard_gates.CU1Gate, 2, 1), + (standard_gates.CU3Gate, 2, 3), + (standard_gates.CYGate, 2, 0), + (standard_gates.CZGate, 2, 0), + (standard_gates.RXXGate, 2, 1), + (standard_gates.RYYGate, 2, 1), + (standard_gates.RZZGate, 2, 1), + (standard_gates.RZXGate, 2, 1), + (standard_gates.XXMinusYYGate, 2, 2), + (standard_gates.XXPlusYYGate, 2, 2), + (standard_gates.ECRGate, 2, 0), + (standard_gates.CSGate, 2, 0), + (standard_gates.CSdgGate, 2, 0), + (standard_gates.SwapGate, 2, 0), + (standard_gates.iSwapGate, 2, 0), + ] + gates_3q = [ + (standard_gates.CCXGate, 3, 0), + (standard_gates.CSwapGate, 3, 0), + (standard_gates.CCZGate, 3, 0), + (standard_gates.RCCXGate, 3, 0), + ] + gates_4q = [ + (standard_gates.C3SXGate, 4, 0), + (standard_gates.RC3XGate, 4, 0), ] - gates_3q = [(CCXGate, 3, 0), (CSwapGate, 3, 0)] gates = gates_1q.copy() if max_operands >= 2: gates.extend(gates_2q) if max_operands >= 3: gates.extend(gates_3q) + if max_operands >= 4: + gates.extend(gates_4q) gates = np.array( gates, dtype=[("class", object), ("num_qubits", np.int64), ("num_params", np.int64)] ) diff --git a/releasenotes/notes/speedup-random-circuits-8d3b724cce1faaad.yaml b/releasenotes/notes/speedup-random-circuits-8d3b724cce1faaad.yaml index a8943e851f17..d1e7696b9c1b 100644 --- a/releasenotes/notes/speedup-random-circuits-8d3b724cce1faaad.yaml +++ b/releasenotes/notes/speedup-random-circuits-8d3b724cce1faaad.yaml @@ -3,6 +3,10 @@ features: - | Random-circuit generation with ``qiskit.circuit.random.random_circuit`` is now significantly faster for large circuits. + - Random-circuit generation with ``qiskit.circuit.random.random_circuit`` will + now output all "standard" gates in Qiskit's circuit library (:mod:`.circuit.library`). + This includes two 4-qubit gates :class:`.C3SXGate` and :class:`.RC3XGate`, and the + allowed values of ``max_operands`` have been expanded accordingly. upgrade: - | The exact circuit returned by ``qiskit.circuit.random.random_circuit`` for a From dfb07d389161a16b957181bf1e1c1b21fac843de Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 27 Oct 2022 21:50:50 +0100 Subject: [PATCH 4/5] Fix docstring --- qiskit/circuit/random/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index 729d2fb78621..db7f9a260fb5 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -38,7 +38,7 @@ def random_circuit( Args: num_qubits (int): number of quantum wires depth (int): layers of operations (i.e. critical path length) - max_operands (int): maximum operands of each gate (between 1 and 3) + max_operands (int): maximum qubit operands of each gate (between 1 and 4) measure (bool): if True, measure all qubits at the end conditional (bool): if True, insert middle measurements and conditionals reset (bool): if True, insert middle resets From a05a17f2a6a7acd53a68a2f2733f6940042b38ed Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 27 Oct 2022 21:54:53 +0100 Subject: [PATCH 5/5] Add comments on algorithm --- qiskit/circuit/random/utils.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index db7f9a260fb5..b63f04c746a8 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -151,11 +151,18 @@ def random_circuit( # calls to the rng. After, trim it down by finding the point when we've used all the qubits. gate_specs = rng.choice(gates, size=len(qubits)) cumulative_qubits = np.cumsum(gate_specs["num_qubits"], dtype=np.int64) + # Efficiently find the point in the list where the total gates would use as many as + # possible of, but not more than, the number of qubits in the layer. If there's slack, fill + # it with 1q gates. max_index = np.searchsorted(cumulative_qubits, num_qubits, side="right") gate_specs = gate_specs[:max_index] slack = num_qubits - cumulative_qubits[max_index - 1] if slack: gate_specs = np.hstack((gate_specs, rng.choice(gates_1q, size=slack))) + + # For efficiency in the Python loop, this uses Numpy vectorisation to pre-calculate the + # indices into the lists of qubits and parameters for every gate, and then suitably + # randomises those lists. q_indices = np.empty(len(gate_specs) + 1, dtype=np.int64) p_indices = np.empty(len(gate_specs) + 1, dtype=np.int64) q_indices[0] = p_indices[0] = 0 @@ -164,7 +171,9 @@ def random_circuit( parameters = rng.uniform(0, 2 * np.pi, size=p_indices[-1]) rng.shuffle(qubits) - # We've now generated everything we're going to need. Now just to add everything. + # We've now generated everything we're going to need. Now just to add everything. The + # conditional check is outside the two loops to make the more common case of no conditionals + # faster, since in Python we don't have a compiler to do this for us. if conditional: is_conditional = rng.random(size=len(gate_specs)) < 0.1 condition_values = rng.integers(