-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Changes from 1 commit
378eda1
2d07e24
f29ad0d
098e393
0123a8c
a171e71
40089b3
ff16763
f47b061
dc72989
705feab
7fa5dc9
6f3a6fc
3c54e04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,7 @@ | |
|
||
|
||
def random_circuit( | ||
num_qubits, depth, max_operands=4, measure=False, conditional=False, reset=False, seed=None | ||
num_qubits, depth, num_operand_distribution: dict = None, max_operands=None, measure=False, conditional=False, reset=False, seed=None | ||
): | ||
"""Generate random circuit of arbitrary size and form. | ||
|
||
|
@@ -39,6 +39,8 @@ def random_circuit( | |
Args: | ||
num_qubits (int): number of quantum wires | ||
depth (int): layers of operations (i.e. critical path length) | ||
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 | ||
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 | ||
|
@@ -51,11 +53,39 @@ 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 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) | ||
|
@@ -119,47 +149,75 @@ 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=gates.dtype) | ||
gates_1q = np.array(gates_1q, dtype=[("class", object), ("num_qubits", np.int64), ("num_params", np.int64)]) | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does the constant There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This constant |
||
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 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 | ||
|
@@ -202,7 +260,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) | ||
|
||
|
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') | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, it is better not to change the order of the positional arguments. Otherwise, you might accidentally break callers like
random_circuit(2, 4, 10)