From 378eda190f50d64de3cd375b12fd9afb7eb93420 Mon Sep 17 00:00:00 2001 From: shravanpatel30 <78003234+shravanpatel30@users.noreply.github.com> Date: Thu, 30 May 2024 18:55:05 -0500 Subject: [PATCH 1/9] unitaryHACK Controlling the Insertion of Multi-Qubit Gates in the Generation of Random Circuits #12059 --- qiskit/circuit/random/utils.py | 105 ++++++++++++++---- ...nded-random-circuits-049b67cce39003f4.yaml | 20 ++++ test/python/circuit/test_random_circuit.py | 23 ++++ 3 files changed, 124 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index fc497a300cba..5ee1dd8e7206 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -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 + 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,17 +149,23 @@ 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) @@ -137,29 +173,51 @@ def random_circuit( 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 + 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) diff --git a/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml b/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml new file mode 100644 index 000000000000..c5a7dc6ed5df --- /dev/null +++ b/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml @@ -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') + diff --git a/test/python/circuit/test_random_circuit.py b/test/python/circuit/test_random_circuit.py index deadcd09d692..c0924726c56a 100644 --- a/test/python/circuit/test_random_circuit.py +++ b/test/python/circuit/test_random_circuit.py @@ -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 @@ -81,3 +83,24 @@ 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 + circ = random_circuit(num_qubits, depth) + self.assertRaises(CircuitError, random_circuit, num_qubits, depth, num_op_dist, max_op) From 2d07e24f191a7daa00835c952caaafa2a79d5dd5 Mon Sep 17 00:00:00 2001 From: shravanpatel30 <78003234+shravanpatel30@users.noreply.github.com> Date: Fri, 31 May 2024 09:47:42 -0500 Subject: [PATCH 2/9] Fixed linting issues --- qiskit/circuit/random/utils.py | 37 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index 5ee1dd8e7206..dc973d82108f 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -21,7 +21,14 @@ def random_circuit( - num_qubits, depth, num_operand_distribution: dict = None, max_operands=None, measure=False, conditional=False, reset=False, seed=None + num_qubits, + depth, + max_operands=None, + num_operand_distribution: dict = None, + measure=False, + conditional=False, + reset=False, + seed=None, ): """Generate random circuit of arbitrary size and form. @@ -39,9 +46,9 @@ 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 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 - 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 @@ -54,7 +61,9 @@ def random_circuit( 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.") + 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 @@ -69,15 +78,19 @@ def random_circuit( 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") + 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 - 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)} + 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 @@ -149,7 +162,9 @@ def random_circuit( (standard_gates.RC3XGate, 4, 0), ] - gates_1q = np.array(gates_1q, 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_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) @@ -202,7 +217,9 @@ def random_circuit( total_gates += len(gate_specs) - current_distribution = {gate_type: counter[gate_type] / total_gates for gate_type in range(1, 5)} + 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: @@ -210,7 +227,9 @@ def random_circuit( 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_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 From 0123a8caf87bdcbdaecc70e24ff86dde46e1ec83 Mon Sep 17 00:00:00 2001 From: shravanpatel30 <78003234+shravanpatel30@users.noreply.github.com> Date: Fri, 31 May 2024 20:25:00 -0500 Subject: [PATCH 3/9] Fixed long lines and unused variable --- qiskit/circuit/random/utils.py | 6 ++++-- test/python/circuit/test_random_circuit.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index dc973d82108f..eb672fb2162c 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -79,7 +79,8 @@ def random_circuit( 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" + 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())) @@ -221,7 +222,8 @@ def random_circuit( 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' + # 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 diff --git a/test/python/circuit/test_random_circuit.py b/test/python/circuit/test_random_circuit.py index c0924726c56a..b230ea06f5f8 100644 --- a/test/python/circuit/test_random_circuit.py +++ b/test/python/circuit/test_random_circuit.py @@ -98,9 +98,11 @@ def test_random_circuit_num_operand_distribution(self): 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""" + """ + 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 - circ = random_circuit(num_qubits, depth) self.assertRaises(CircuitError, random_circuit, num_qubits, depth, num_op_dist, max_op) From 40089b3b2d4bb01a99911321ba289e783dd14d64 Mon Sep 17 00:00:00 2001 From: shravanpatel30 <78003234+shravanpatel30@users.noreply.github.com> Date: Tue, 4 Jun 2024 19:12:09 -0500 Subject: [PATCH 4/9] Added requested changes --- qiskit/circuit/random/utils.py | 36 ++++++--------- test/python/circuit/test_random_circuit.py | 53 ++++++++++++++++++---- 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index eb672fb2162c..6991e94184b3 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -23,12 +23,12 @@ def random_circuit( num_qubits, depth, - max_operands=None, - num_operand_distribution: dict = None, + max_operands=4, measure=False, conditional=False, reset=False, seed=None, + num_operand_distribution: dict = None, ): """Generate random circuit of arbitrary size and form. @@ -47,12 +47,12 @@ 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 seed (int): sets random seed (optional) + 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 (optional) Returns: QuantumCircuit: constructed circuit @@ -60,15 +60,6 @@ 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) @@ -94,7 +85,8 @@ def random_circuit( 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 + # 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.") @@ -172,8 +164,9 @@ def random_circuit( 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 + # 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(): @@ -191,7 +184,8 @@ def random_circuit( 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 + # Counter to keep track of number of different gate types + counter = np.zeros(len(all_gate_lists) + 1, dtype=np.int64) total_gates = 0 # Apply arbitrary random operations in layers across all qubits. @@ -214,21 +208,17 @@ def random_circuit( # 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) + counter += np.bincount(gate_qubits, minlength=len(all_gate_lists) + 1) 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: + if slack >= key and counter[key] / total_gates < dist: gate_to_add = np.array( all_gate_lists[key - 1][rng.integers(0, len(all_gate_lists[key - 1]))] ) diff --git a/test/python/circuit/test_random_circuit.py b/test/python/circuit/test_random_circuit.py index b230ea06f5f8..143c3b66f11b 100644 --- a/test/python/circuit/test_random_circuit.py +++ b/test/python/circuit/test_random_circuit.py @@ -73,7 +73,7 @@ def test_large_conditional(self): def test_random_mid_circuit_measure_conditional(self): """Test random circuit with mid-circuit measurements for conditionals.""" num_qubits = depth = 2 - circ = random_circuit(num_qubits, depth, conditional=True, seed=4) + circ = random_circuit(num_qubits, depth, conditional=True, seed=16) self.assertEqual(circ.width(), 2 * num_qubits) op_names = [instruction.operation.name for instruction in circ] # Before a condition, there needs to be measurement in all the qubits. @@ -86,9 +86,10 @@ def test_random_mid_circuit_measure_conditional(self): def test_random_circuit_num_operand_distribution(self): """Test that num_operand_distribution argument generates gates in correct proportion""" - num_qubits = depth = 8 + num_qubits = 50 + depth = 300 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) + circ = random_circuit(num_qubits, depth, num_operand_distribution=num_op_dist) total_gates = circ.size() self.assertEqual(circ.width(), num_qubits) self.assertEqual(circ.depth(), depth) @@ -97,12 +98,44 @@ def test_random_circuit_num_operand_distribution(self): 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): + def test_random_circuit_2and3_qubit_gates_only(self): """ - Test that when num_operand_distribution and max_operands are specified - together the function raises an error + Test that the generated random circuit only has 2 and 3 qubit gates, + while disallowing 1-qubit and 4-qubit gates if + num_operand_distribution = {2: some_prob, 3: some_prob} """ - 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) + num_qubits = 10 + depth = 200 + num_op_dist = {2: 0.5, 3: 0.5} + circ = random_circuit(num_qubits, depth, num_operand_distribution=num_op_dist, seed=200) + total_gates = circ.size() + gate_qubits = [instruction.operation.num_qubits for instruction in circ] + gate_type_counter = np.bincount(gate_qubits, minlength=5) + # Testing that the distribution of 2 and 3 qubit gate matches with given distribution + for gate_type, prob in sorted(num_op_dist.items()): + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1) + # Testing that there are no 1-qubit gate and 4-qubit in the generated random circuit + self.assertEqual(gate_type_counter[1], 0.0) + self.assertEqual(gate_type_counter[4], 0.0) + + def test_random_circuit_3and4_qubit_gates_only(self): + """ + Test that the generated random circuit only has 3 and 4 qubit gates, + while disallowing 1-qubit and 2-qubit gates if + num_operand_distribution = {3: some_prob, 4: some_prob} + """ + num_qubits = 10 + depth = 200 + num_op_dist = {3: 0.5, 4: 0.5} + circ = random_circuit( + num_qubits, depth, num_operand_distribution=num_op_dist, seed=11111111 + ) + total_gates = circ.size() + gate_qubits = [instruction.operation.num_qubits for instruction in circ] + gate_type_counter = np.bincount(gate_qubits, minlength=5) + # Testing that the distribution of 3 and 4 qubit gate matches with given distribution + for gate_type, prob in sorted(num_op_dist.items()): + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1) + # Testing that there are no 1-qubit gate and 2-qubit in the generated random circuit + self.assertEqual(gate_type_counter[1], 0.0) + self.assertEqual(gate_type_counter[2], 0.0) From ff16763dd8e0399e3d24964bf3fe1f08b199e461 Mon Sep 17 00:00:00 2001 From: shravanpatel30 <78003234+shravanpatel30@users.noreply.github.com> Date: Tue, 4 Jun 2024 19:46:08 -0500 Subject: [PATCH 5/9] Removed unused imports --- test/python/circuit/test_random_circuit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/python/circuit/test_random_circuit.py b/test/python/circuit/test_random_circuit.py index 143c3b66f11b..cb4f3124e985 100644 --- a/test/python/circuit/test_random_circuit.py +++ b/test/python/circuit/test_random_circuit.py @@ -17,7 +17,6 @@ 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 From f47b06173aec52dc132618551f1c84e612910eb2 Mon Sep 17 00:00:00 2001 From: shravanpatel30 <78003234+shravanpatel30@users.noreply.github.com> Date: Tue, 4 Jun 2024 21:53:46 -0500 Subject: [PATCH 6/9] Added a test --- test/python/circuit/test_random_circuit.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/python/circuit/test_random_circuit.py b/test/python/circuit/test_random_circuit.py index cb4f3124e985..9f92ae4056f4 100644 --- a/test/python/circuit/test_random_circuit.py +++ b/test/python/circuit/test_random_circuit.py @@ -138,3 +138,23 @@ def test_random_circuit_3and4_qubit_gates_only(self): # Testing that there are no 1-qubit gate and 2-qubit in the generated random circuit self.assertEqual(gate_type_counter[1], 0.0) self.assertEqual(gate_type_counter[2], 0.0) + + def test_random_circuit_with_zero_distribution(self): + """ + Test that the generated random circuit only has 3 and 4 qubit gates, + while disallowing 1-qubit and 2-qubit gates if + num_operand_distribution = {1: 0.0, 2: 0.0, 3: some_prob, 4: some_prob} + """ + num_qubits = 10 + depth = 200 + num_op_dist = {1: 0.0, 2: 0.0, 3: 0.5, 4: 0.5} + circ = random_circuit(num_qubits, depth, num_operand_distribution=num_op_dist) + total_gates = circ.size() + gate_qubits = [instruction.operation.num_qubits for instruction in circ] + gate_type_counter = np.bincount(gate_qubits, minlength=5) + # Testing that the distribution of 3 and 4 qubit gate matches with given distribution + for gate_type, prob in sorted(num_op_dist.items()): + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.15) + # Testing that there are no 1-qubit gate and 2-qubit in the generated random circuit + self.assertEqual(gate_type_counter[1], 0.0) + self.assertEqual(gate_type_counter[2], 0.0) From 705feab352002bd085ef839d60f203ec5fdeb692 Mon Sep 17 00:00:00 2001 From: shravanpatel30 <78003234+shravanpatel30@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:36:21 -0500 Subject: [PATCH 7/9] Added the stochastic process comment and edited releasenotes --- qiskit/circuit/random/utils.py | 7 +++++++ .../notes/extended-random-circuits-049b67cce39003f4.yaml | 5 +++-- test/python/circuit/test_random_circuit.py | 8 +++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index 6991e94184b3..9d56b6359894 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -195,6 +195,13 @@ def random_circuit( # 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. + # Due to the stochastic nature of generating a random circuit, the resulting ratios + # may not precisely match the specified values from `num_operand_distribution`. Expect + # greater deviations from the target ratios in quantum circuits with fewer qubits and + # shallower depths, and smaller deviations in larger and deeper quantum circuits. + # For more information on how the distribution changes with number of qubits and depth + # refer to the pull request #12483 on Qiskit GitHub. + gate_specs = rng.choice(gates, size=len(qubits), p=distribution) cumulative_qubits = np.cumsum(gate_specs["num_qubits"], dtype=np.int64) diff --git a/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml b/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml index c5a7dc6ed5df..f4bb585053bc 100644 --- a/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml +++ b/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml @@ -9,8 +9,9 @@ features_circuits: 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. + then `max_operands` will default to 4 and a random circuit with a random gate distribution will + be generated. If both `num_operand_distribution` and `max_operands` are specified at the same + time then `num_operand_distribution` will be used to generate the random circuit. Example usage:: from qiskit.circuit.random import random_circuit diff --git a/test/python/circuit/test_random_circuit.py b/test/python/circuit/test_random_circuit.py index 9f92ae4056f4..ebbdfd28d648 100644 --- a/test/python/circuit/test_random_circuit.py +++ b/test/python/circuit/test_random_circuit.py @@ -88,7 +88,9 @@ def test_random_circuit_num_operand_distribution(self): num_qubits = 50 depth = 300 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) + circ = random_circuit( + num_qubits, depth, num_operand_distribution=num_op_dist, seed=123456789 + ) total_gates = circ.size() self.assertEqual(circ.width(), num_qubits) self.assertEqual(circ.depth(), depth) @@ -148,13 +150,13 @@ def test_random_circuit_with_zero_distribution(self): num_qubits = 10 depth = 200 num_op_dist = {1: 0.0, 2: 0.0, 3: 0.5, 4: 0.5} - circ = random_circuit(num_qubits, depth, num_operand_distribution=num_op_dist) + circ = random_circuit(num_qubits, depth, num_operand_distribution=num_op_dist, seed=12) total_gates = circ.size() gate_qubits = [instruction.operation.num_qubits for instruction in circ] gate_type_counter = np.bincount(gate_qubits, minlength=5) # Testing that the distribution of 3 and 4 qubit gate matches with given distribution for gate_type, prob in sorted(num_op_dist.items()): - self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.15) + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1) # Testing that there are no 1-qubit gate and 2-qubit in the generated random circuit self.assertEqual(gate_type_counter[1], 0.0) self.assertEqual(gate_type_counter[2], 0.0) From 7fa5dc9d26dbe3142658b91de82f9267030eadbe Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:41:20 +0200 Subject: [PATCH 8/9] Update qiskit/circuit/random/utils.py --- qiskit/circuit/random/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index 9d56b6359894..5543940208d6 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -52,7 +52,9 @@ def random_circuit( reset (bool): if True, insert middle resets seed (int): sets random seed (optional) 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 (optional) + of 1-qubit, 2-qubit, 3-qubit, ..., n-qubit gates in the random circuit. Expect a + deviation from the specified ratios that depends on the size of the requested + random circuit. (optional) Returns: QuantumCircuit: constructed circuit From 6f3a6fc6b96e9d1a2aab27ce011d2ea89768932c Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:01:28 +0200 Subject: [PATCH 9/9] lint... --- 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 5543940208d6..f27cbfbfca88 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -52,7 +52,7 @@ def random_circuit( reset (bool): if True, insert middle resets seed (int): sets random seed (optional) 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. Expect a + of 1-qubit, 2-qubit, 3-qubit, ..., n-qubit gates in the random circuit. Expect a deviation from the specified ratios that depends on the size of the requested random circuit. (optional)