diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index bb9291e78985..2f32a3257c98 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -223,6 +223,10 @@ def entanglement(layer_index: int) -> list[list[int]]: Returns: An n-local circuit. """ + if reps < 0: + # this is an important check, since we cast this to an unsigned integer Rust-side + raise ValueError(f"reps must be non-negative, but is {reps}") + supported_gates = get_standard_gate_name_mapping() rotation_blocks = _normalize_blocks( rotation_blocks, supported_gates, overwrite_block_parameters @@ -1295,7 +1299,8 @@ def _normalize_entanglement( ) -> list[str | list[tuple[int]]] | Callable[[int], list[str | list[tuple[int]]]]: """If the entanglement is Iterable[Iterable], normalize to list[tuple].""" if isinstance(entanglement, str): - return [entanglement] + return [entanglement] * num_entanglement_blocks + elif isinstance(entanglement, Iterable): # handle edge cases when entanglement is set to an empty list if len(entanglement) == 0: @@ -1335,11 +1340,20 @@ def _normalize_blocks( supported_gates: dict[str, Gate], overwrite_block_parameters: bool, ) -> list[Block]: - if not isinstance(blocks, Iterable): + if not isinstance(blocks, Iterable) or isinstance(blocks, str): blocks = [blocks] normalized = [] for block in blocks: + # since the NLocal circuit accepted circuits as inputs, we raise a warning here + # to simplify the transition (even though, strictly speaking, quantum circuits are + # not a supported input type) + if isinstance(block, QuantumCircuit): + raise ValueError( + "The blocks should be of type Gate or str, but you passed a QuantumCircuit. " + "You can call .to_gate() on the circuit to turn it into a Gate object." + ) + is_standard = False if isinstance(block, str): if block not in supported_gates: diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py index c2a425d4a9e7..43e2785912c2 100644 --- a/test/python/circuit/library/test_nlocal.py +++ b/test/python/circuit/library/test_nlocal.py @@ -20,9 +20,13 @@ from ddt import ddt, data, unpack from qiskit import transpile -from qiskit.circuit import QuantumCircuit, Parameter, ParameterVector, ParameterExpression +from qiskit.circuit import QuantumCircuit, Parameter, ParameterVector, ParameterExpression, Gate from qiskit.circuit.library import ( n_local, + efficient_su2, + real_amplitudes, + excitation_preserving, + pauli_two_design, NLocal, TwoLocal, RealAmplitudes, @@ -50,6 +54,18 @@ from test import QiskitTestCase # pylint: disable=wrong-import-order +class Gato(Gate): + """A custom gate.""" + + def __init__(self, x, y): + super().__init__("meow", 1, [x, y]) + + def _define(self): + x, y = self.params + self.definition = QuantumCircuit(1) + self.definition.p(x + y, 0) + + @ddt class TestNLocal(QiskitTestCase): """Test the n-local circuit class.""" @@ -471,9 +487,129 @@ def test_initial_state_as_circuit_object(self): self.assertCircuitEqual(ref, expected) +@ddt class TestNLocalFunction(QiskitTestCase): """Test the n_local circuit library function.""" + def test_empty_blocks(self): + """Test passing no rotation and entanglement blocks.""" + circuit = n_local(2, rotation_blocks=[], entanglement_blocks=[]) + expected = QuantumCircuit(2) + + self.assertEqual(expected, circuit) + + def test_str_blocks(self): + """Test passing blocks as strings.""" + circuit = n_local(2, "h", "ecr", reps=2) + expected = QuantumCircuit(2) + for _ in range(2): + expected.h([0, 1]) + expected.ecr(0, 1) + expected.h([0, 1]) + + self.assertEqual(expected, circuit) + + def test_gate_blocks(self): + """Test passing blocks as gates.""" + x, y = Parameter("x"), Parameter("y") + my_gate = Gato(x, y) + + circuit = n_local(4, my_gate, "cx", "linear", reps=3) + print(circuit.draw()) + + expected_cats = 4 * (3 + 1) # num_qubits * (reps + 1) + expected_cx = 3 * 3 # gates per block * reps + expected_num_params = expected_cats * 2 + + self.assertEqual(expected_cats, circuit.count_ops().get("meow", 0)) + self.assertEqual(expected_cx, circuit.count_ops().get("cx", 0)) + self.assertEqual(expected_num_params, circuit.num_parameters) + + def test_gate_lists(self): + """Test passing a list of strings and gates.""" + reps = 2 + circuit = n_local(4, [XGate(), "ry", SXGate()], ["ryy", CCXGate()], "full", reps) + expected_1q = 4 * (reps + 1) # num_qubits * (reps + 1) + expected_2q = 4 * 3 / 2 * reps # 4 choose 2 * reps + expected_3q = 4 * reps # 4 choose 3 * reps + + ops = circuit.count_ops() + for gate in ["x", "ry", "sx"]: + with self.subTest(gate=gate): + self.assertEqual(expected_1q, ops.get(gate, 0)) + + with self.subTest(gate="ryy"): + self.assertEqual(expected_2q, ops.get("ryy", 0)) + + with self.subTest(gate="ccx"): + self.assertEqual(expected_3q, ops.get("ccx", 0)) + + def test_reps(self): + """Test setting the repetitions.""" + all_reps = [0, 1, 2, 10] + for reps in all_reps: + circuit = n_local(2, rotation_blocks="rx", entanglement_blocks="cz", reps=reps) + expected_rx = (reps + 1) * 2 + expected_cz = reps + + with self.subTest(reps=reps): + self.assertEqual(expected_rx, circuit.count_ops().get("rx", 0)) + self.assertEqual(expected_cz, circuit.count_ops().get("cz", 0)) + + def test_negative_reps(self): + """Test negative reps raises.""" + with self.assertRaises(ValueError): + _ = n_local(1, [], [], reps=-1) + + def test_barrier(self): + """Test setting barriers.""" + circuit = n_local(2, "ry", "cx", reps=2, insert_barriers=True) + values = np.ones(circuit.num_parameters) + + expected = QuantumCircuit(2) + expected.ry(1, [0, 1]) + expected.barrier() + expected.cx(0, 1) + expected.barrier() + expected.ry(1, [0, 1]) + expected.barrier() + expected.cx(0, 1) + expected.barrier() + expected.ry(1, [0, 1]) + + self.assertEqual(expected, circuit.assign_parameters(values)) + + def test_parameter_prefix(self): + """Test setting the parameter prefix.""" + circuit = n_local(2, "h", "crx", parameter_prefix="x") + prefixes = [p.name[0] for p in circuit.parameters] + self.assertTrue(all(prefix == "x" for prefix in prefixes)) + + @data(True, False) + def test_overwrite_block_parameters(self, overwrite): + """Test overwriting the block parameters.""" + x = Parameter("x") + block = QuantumCircuit(2) + block.rxx(x, 0, 1) + + reps = 3 + circuit = n_local( + 4, [], [block.to_gate()], "linear", reps, overwrite_block_parameters=overwrite + ) + + expected_num_params = reps * 3 if overwrite else 1 + self.assertEqual(expected_num_params, circuit.num_parameters) + + @data(True, False) + def test_skip_final_rotation_layer(self, skip): + """Test skipping the final rotation layer.""" + reps = 5 + num_qubits = 2 + circuit = n_local(num_qubits, "rx", "ch", reps=reps, skip_final_rotation_layer=skip) + expected_rx = num_qubits * (reps + (0 if skip else 1)) + + self.assertEqual(expected_rx, circuit.count_ops().get("rx", 0)) + def test_skip_unentangled_qubits(self): """Test skipping the unentangled qubits.""" num_qubits = 6 @@ -501,6 +637,21 @@ def entanglement_2(layer): idle = set(dag.idle_wires()) self.assertEqual(skipped_set, idle) + def test_mismatching_entanglement_blocks(self): + """Test an error is raised if the number of entanglements does not match the blocks.""" + entanglement = ["full", "linear", "pairwise"] + blocks = ["ryy", "iswap"] + + with self.assertRaises(QiskitError): + _ = n_local(3, [], blocks, entanglement=entanglement) + + def test_mismatching_entanglement_indices(self): + """Test an error is raised if the entanglement does not match the blocksize.""" + entanglement = [[0, 1], [2]] + + with self.assertRaises(QiskitError): + _ = n_local(3, "ry", "cx", entanglement) + def test_entanglement_by_callable(self): """Test setting the entanglement by callable. @@ -549,6 +700,73 @@ def test_entanglement_by_callable(self): self.assertEqual(nlocal, circuit) + def test_nice_error_if_circuit_passed(self): + """Check the transition-helper error.""" + block = QuantumCircuit(1) + + with self.assertRaisesRegex(ValueError, "but you passed a QuantumCircuit"): + _ = n_local(3, block, "cz") + + +@ddt +class TestNLocalFamily(QiskitTestCase): + """Test the derived circuit functions.""" + + def test_real_amplitudes(self): + """Test the real amplitudes circuit.""" + circuit = real_amplitudes(4) + expected = n_local(4, "ry", "cx", "reverse_linear", reps=3) + self.assertEqual(expected.assign_parameters(circuit.parameters), circuit) + + def test_efficient_su2(self): + """Test the efficient SU(2) circuit.""" + circuit = efficient_su2(4) + expected = n_local(4, ["ry", "rz"], "cx", "reverse_linear", reps=3) + self.assertEqual(expected.assign_parameters(circuit.parameters), circuit) + + @data("fsim", "iswap") + def test_excitation_preserving(self, mode): + """Test the excitation preserving circuit.""" + circuit = excitation_preserving(4, mode=mode) + + x = Parameter("x") + block = QuantumCircuit(2) + block.rxx(x, 0, 1) + block.ryy(x, 0, 1) + if mode == "fsim": + y = Parameter("y") + block.cp(y, 0, 1) + + expected = n_local(4, "rz", block.to_gate(), "full", reps=3) + self.assertEqual( + expected.assign_parameters(circuit.parameters).decompose(), circuit.decompose() + ) + + def test_two_design(self): + """Test the Pauli 2-design circuit.""" + circuit = pauli_two_design(3) + expected_ops = {"rx", "ry", "rz", "cz"} + circuit_ops = set(circuit.count_ops().keys()) + + self.assertTrue(circuit_ops.issubset(expected_ops)) + + def test_two_design_seed(self): + """Test the seed""" + seed1 = 123 + seed2 = 321 + + with self.subTest(msg="same circuit with same seed"): + first = pauli_two_design(3, seed=seed1) + second = pauli_two_design(3, seed=seed1) + + self.assertEqual(first.assign_parameters(second.parameters), second) + + with self.subTest(msg="different circuit with different seed"): + first = pauli_two_design(3, seed=seed1) + second = pauli_two_design(3, seed=seed2) + + self.assertNotEqual(first.assign_parameters(second.parameters), second) + @ddt class TestTwoLocal(QiskitTestCase):