Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speed up random_circuit #8983

Merged
merged 8 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 95 additions & 65 deletions qiskit/circuit/random/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,13 +29,13 @@
RXGate,
RYGate,
RZGate,
UGate,
CXGate,
CYGate,
CZGate,
CHGate,
CUGate,
CRZGate,
CU1Gate,
CU3Gate,
SwapGate,
RZZGate,
CCXGate,
Expand Down Expand Up @@ -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),
jakelishman marked this conversation as resolved.
Show resolved Hide resolved
]
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)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the first time I've ever seen field dtypes used in practice before. I had completely forgotten you could even do this in numpy. This is a cool application for it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I only very rarely use them. For posterity: the reasons for the choice here is that I wanted the most efficient access from rng.choice (which also maintains dtype in its output), and for the subsequent cumulative sums over num_qubits and num_params to have defined strides in their access patterns, so the Numpy vectorisation after the rng choice would all work as expected.

)
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)
jakelishman marked this conversation as resolved.
Show resolved Hide resolved

# 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)
jakelishman marked this conversation as resolved.
Show resolved Hide resolved
)
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
10 changes: 10 additions & 0 deletions releasenotes/notes/speedup-random-circuits-8d3b724cce1faaad.yaml
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +12 to +14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need a release note? In general, I don't think we hold any of our functionality to seed-reproducible output across different Terra verions (or numpy versions, platforms, etc.). It would be good to have canonical documentation one way or the other though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have done in the past - we certainly did it for the switch of SabreSwap to Rust (and my intent was to write another of those notes when I break the RNG compat in my new version of Sabre too).

I'm approximately in favour of mentioning it in the release notes - we use seed stability in our tests (e.g. the SabreLayout tests), so it's not inconceivable that others are doing similar things.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I would agree we're not committed to ensuring seed stability across releases, but it's still good to document it as an upgrade note, especially for functions like this (and sabre) where the output is used for testing. Just to document it'll be different for users when they upgrade from the previous release to the new one. We have done release notes like this in the past for random_circuit too: https://qiskit.org/documentation/release_notes.html#release-notes-0-19-0-upgrade-notes (it's in there a bit down the list).