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

[unitaryHACK] Controlling the Insertion of Multi-Qubit Gates in the Generation of Random Circuits #12059 #12483

Merged
merged 14 commits into from
Jun 7, 2024
Merged
124 changes: 101 additions & 23 deletions qiskit/circuit/random/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@


def random_circuit(
num_qubits, depth, max_operands=4, measure=False, conditional=False, reset=False, seed=None
num_qubits,
depth,
max_operands=None,
num_operand_distribution: dict = None,
shravanpatel30 marked this conversation as resolved.
Show resolved Hide resolved
measure=False,
conditional=False,
reset=False,
seed=None,
):
"""Generate random circuit of arbitrary size and form.

Expand All @@ -40,6 +47,8 @@ def random_circuit(
num_qubits (int): number of quantum wires
depth (int): layers of operations (i.e. critical path length)
max_operands (int): maximum qubit operands of each gate (between 1 and 4)
num_operand_distribution (dict): a distribution of gates that specifies the ratio
of 1-qubit, 2-qubit, 3-qubit,...,n-qubit gates in the random circuit
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
Expand All @@ -51,11 +60,46 @@ def random_circuit(
Raises:
CircuitError: when invalid options given
"""
if num_operand_distribution and max_operands:
raise CircuitError(
"Both 'num_operand_distribution' and 'max_operands' cannot be specified together."
)

if not num_operand_distribution and max_operands is None:
# Set default max_operands to 4 if not specified
max_operands = 4

if seed is None:
seed = np.random.randint(0, np.iinfo(np.int32).max)
rng = np.random.default_rng(seed)

if num_operand_distribution:
if min(num_operand_distribution.keys()) < 1 or max(num_operand_distribution.keys()) > 4:
raise CircuitError("'num_operand_distribution' must have keys between 1 and 4")
for key, prob in num_operand_distribution.items():
if key > num_qubits and prob != 0.0:
raise CircuitError(
f"'num_operand_distribution' cannot have {key}-qubit gates"
f" for circuit with {num_qubits} qubits"
)
num_operand_distribution = dict(sorted(num_operand_distribution.items()))

if not num_operand_distribution and max_operands:
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
sbrandhsn marked this conversation as resolved.
Show resolved Hide resolved
rand_dist = rng.dirichlet(
np.ones(max_operands)
) # This will create a random distribution that sums to 1
num_operand_distribution = {i + 1: rand_dist[i] for i in range(max_operands)}
num_operand_distribution = dict(sorted(num_operand_distribution.items()))

# Here we will use np.isclose() because very rarely there might be floating point precision errors
if not np.isclose(sum(num_operand_distribution.values()), 1):
raise CircuitError("The sum of all the values in 'num_operand_distribution' is not 1.")

if num_qubits == 0:
return QuantumCircuit()
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)
Expand Down Expand Up @@ -119,47 +163,82 @@ def random_circuit(
(standard_gates.RC3XGate, 4, 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)]
gates_1q = np.array(
gates_1q, dtype=[("class", object), ("num_qubits", np.int64), ("num_params", np.int64)]
)
gates_1q = np.array(gates_1q, dtype=gates.dtype)
gates_2q = np.array(gates_2q, dtype=gates_1q.dtype)
gates_3q = np.array(gates_3q, dtype=gates_1q.dtype)
gates_4q = np.array(gates_4q, dtype=gates_1q.dtype)

all_gate_lists = [gates_1q, gates_2q, gates_3q, gates_4q]

# Here we will create a list 'gates_to_consider' that will have a subset of different n-qubit gates
# and will also create a list for ratio (or probability) for each gates
gates_to_consider = []
distribution = []
for n_qubits, ratio in num_operand_distribution.items():
gate_list = all_gate_lists[n_qubits - 1]
gates_to_consider.extend(gate_list)
distribution.extend([ratio / len(gate_list)] * len(gate_list))

gates = np.array(gates_to_consider, dtype=gates_1q.dtype)

qc = QuantumCircuit(num_qubits)

if measure or conditional:
cr = ClassicalRegister(num_qubits, "c")
qc.add_register(cr)

if seed is None:
seed = np.random.randint(0, np.iinfo(np.int32).max)
rng = np.random.default_rng(seed)

qubits = np.array(qc.qubits, dtype=object, copy=True)

counter = np.zeros(5, dtype=np.int64) # Counter to keep track of number of different gate types
Copy link
Contributor

Choose a reason for hiding this comment

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

What does the constant 5 refer to here? Can you derive the value for that constant from all_gate_lists?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This constant 5 here refers to an array that will have total length len(all_gate_lists) + 1, so I can just derive this value from all_gate_lists.

total_gates = 0

# Apply arbitrary random operations in layers across all qubits.
for layer_number in range(depth):
# 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))

gate_specs = rng.choice(gates, size=len(qubits), p=distribution)
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)))

# Updating the counter for 1-qubit, 2-qubit, 3-qubit and 4-qubit gates
gate_qubits = gate_specs["num_qubits"]
counter += np.bincount(gate_qubits, minlength=5)

total_gates += len(gate_specs)

current_distribution = {
gate_type: counter[gate_type] / total_gates for gate_type in range(1, 5)
}

# Slack handling loop, this loop will add gates to fill
# the slack while respecting the 'num_operand_distribution'
while slack > 0:
gate_added_flag = False

for key, dist in sorted(num_operand_distribution.items(), reverse=True):
if slack >= key and current_distribution[key] < dist:
gate_to_add = np.array(
all_gate_lists[key - 1][rng.integers(0, len(all_gate_lists[key - 1]))]
)
gate_specs = np.hstack((gate_specs, gate_to_add))
counter[key] += 1
total_gates += 1
slack -= key
gate_added_flag = True
if not gate_added_flag:
break

# 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
Expand Down Expand Up @@ -202,7 +281,6 @@ def random_circuit(
):
operation = gate(*parameters[p_start:p_end])
qc._append(CircuitInstruction(operation=operation, qubits=qubits[q_start:q_end]))

if measure:
qc.measure(qc.qubits, cr)

Expand Down
20 changes: 20 additions & 0 deletions releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
features_circuits:
- |
The `random_circuit` function from `qiskit.circuit.random.utils` has a new feature where
users can specify a distribution `num_operand_distribution` (a dict) that specifies the
ratio of 1-qubit, 2-qubit, 3-qubit, and 4-qubit gates in the random circuit. For example,
if `num_operand_distribution = {1: 0.25, 2: 0.25, 3: 0.25, 4: 0.25}` is passed to the function
then the generated circuit will have approximately 25% of 1-qubit, 2-qubit, 3-qubit, and
4-qubit gates (The order in which the dictionary is passed does not matter i.e. you can specify
`num_operand_distribution = {3: 0.5, 1: 0.0, 4: 0.3, 2: 0.2}` and the function will still work
as expected). Also it should be noted that the if `num_operand_distribution` is not specified
then `max_operands` will default to 4 and a random gate distribution will be generated but
users cannot specify both `num_operand_distribution` and `max_operands` at the same time.
Example usage::

from qiskit.circuit.random import random_circuit

circ = random_circuit(num_qubits=6, depth=5, num_operand_distribution = {1: 0.25, 2: 0.25, 3: 0.25, 4: 0.25})
circ.draw(output='mpl')

25 changes: 25 additions & 0 deletions test/python/circuit/test_random_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@


"""Test random circuit generation utility."""
import numpy as np
from qiskit.circuit import QuantumCircuit, ClassicalRegister, Clbit
from qiskit.circuit import Measure
from qiskit.circuit.random import random_circuit
from qiskit.converters import circuit_to_dag
from qiskit.circuit.exceptions import CircuitError
from test import QiskitTestCase # pylint: disable=wrong-import-order


Expand Down Expand Up @@ -81,3 +83,26 @@ def test_random_mid_circuit_measure_conditional(self):
bool(getattr(instruction.operation, "condition", None)) for instruction in circ
]
self.assertEqual([False, False, False, True], conditions)

def test_random_circuit_num_operand_distribution(self):
"""Test that num_operand_distribution argument generates gates in correct proportion"""
num_qubits = depth = 8
num_op_dist = {2: 0.25, 3: 0.25, 1: 0.25, 4: 0.25}
circ = random_circuit(num_qubits, depth, num_operand_distribution=num_op_dist, seed=5555555)
total_gates = circ.size()
self.assertEqual(circ.width(), num_qubits)
self.assertEqual(circ.depth(), depth)
gate_qubits = [instruction.operation.num_qubits for instruction in circ]
gate_type_counter = np.bincount(gate_qubits, minlength=5)
for gate_type, prob in sorted(num_op_dist.items()):
self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1)

def test_random_circuit_with_max_operands_and_num_op_dist(self):
"""
Test that when num_operand_distribution and max_operands are specified
together the function raises an error
"""
num_qubits = depth = 4
num_op_dist = {2: 0.25, 3: 0.25, 1: 0.25, 4: 0.25}
max_op = 4
self.assertRaises(CircuitError, random_circuit, num_qubits, depth, num_op_dist, max_op)
Loading