diff --git a/qiskit/circuit/library/generalized_gates/linear_function.py b/qiskit/circuit/library/generalized_gates/linear_function.py index f1a3c25c1ee0..e6697726c028 100644 --- a/qiskit/circuit/library/generalized_gates/linear_function.py +++ b/qiskit/circuit/library/generalized_gates/linear_function.py @@ -14,9 +14,10 @@ from __future__ import annotations import numpy as np -from qiskit.circuit import QuantumCircuit, Gate +from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate from qiskit.circuit.exceptions import CircuitError from qiskit.synthesis.linear import check_invertible_binary_matrix +from qiskit.circuit.library.generalized_gates.permutation import PermutationGate class LinearFunction(Gate): @@ -61,42 +62,36 @@ class LinearFunction(Gate): `Online at umich.edu. `_ """ - def __init__( - self, - linear: list[list[int]] | np.ndarray | QuantumCircuit, - validate_input: bool | None = False, - ) -> None: + def __init__(self, linear, validate_input=False): """Create a new linear function. Args: - linear (list[list] or ndarray[bool] or QuantumCircuit): - either an n x n matrix, describing the linear function, - or a quantum circuit composed of linear gates only - (currently supported gates are CX and SWAP). + linear (list[list] or ndarray[bool] or QuantumCircuit or LinearFunction + or PermutationGate or Clifford): data from which a linear function + can be constructed. It can be either a nxn matrix (describing the + linear transformation), a permutation (which is a special case of + a linear function), another linear function, a clifford (when it + corresponds to a linear function), or a quantum circuit composed of + linear gates (CX and SWAP) and other objects described above, including + nested subcircuits. validate_input: if True, performs more expensive input validation checks, such as checking that a given n x n matrix is invertible. Raises: CircuitError: if the input is invalid: - either a matrix is non {square, invertible}, - or a quantum circuit contains non-linear gates. - + either the input matrix is not square or not invertible, + or the input quantum circuit contains non-linear objects + (for example, a Hadamard gate, or a Clifford that does + not correspond to a linear function). """ - if not isinstance(linear, (list, np.ndarray, QuantumCircuit)): - raise CircuitError( - "A linear function must be represented either by a list, " - "a numpy array, or a quantum circuit with linear gates." - ) - if isinstance(linear, QuantumCircuit): - # The following function will raise a CircuitError if there are nonlinear gates. - original_circuit = linear - linear = _linear_quantum_circuit_to_mat(linear) + # pylint: disable=cyclic-import + from qiskit.quantum_info import Clifford - else: - original_circuit = None + original_circuit = None + if isinstance(linear, (list, np.ndarray)): # Normalize to numpy array (coercing entries to 0s and 1s) try: linear = np.array(linear, dtype=bool, copy=True) @@ -116,10 +111,100 @@ def __init__( "A linear function must be represented by an invertible matrix." ) + elif isinstance(linear, QuantumCircuit): + # The following function will raise a CircuitError if there are nonlinear gates. + original_circuit = linear + linear = LinearFunction._circuit_to_mat(linear) + + elif isinstance(linear, LinearFunction): + linear = linear.linear.copy() + + elif isinstance(linear, PermutationGate): + linear = LinearFunction._permutation_to_mat(linear) + + elif isinstance(linear, Clifford): + # The following function will raise a CircuitError if clifford does not correspond + # to a linear function. + linear = LinearFunction._clifford_to_mat(linear) + + # Note: if we wanted, we could also try to construct a linear function from a + # general operator, by first attempting to convert it to clifford, and then to + # a linear function. + + else: + raise CircuitError("A linear function cannot be successfully constructed.") + super().__init__( name="linear_function", num_qubits=len(linear), params=[linear, original_circuit] ) + @staticmethod + def _circuit_to_mat(qc: QuantumCircuit): + """This creates a nxn matrix corresponding to the given quantum circuit.""" + nq = qc.num_qubits + mat = np.eye(nq, nq, dtype=bool) + + for instruction in qc.data: + if instruction.operation.name in ("barrier", "delay"): + # can be ignored + continue + if instruction.operation.name == "cx": + # implemented directly + cb = qc.find_bit(instruction.qubits[0]).index + tb = qc.find_bit(instruction.qubits[1]).index + mat[tb, :] = (mat[tb, :]) ^ (mat[cb, :]) + continue + if instruction.operation.name == "swap": + # implemented directly + cb = qc.find_bit(instruction.qubits[0]).index + tb = qc.find_bit(instruction.qubits[1]).index + mat[[cb, tb]] = mat[[tb, cb]] + continue + + # In all other cases, we construct the linear function for the operation. + # and compose (multiply) linear matrices. + + if getattr(instruction.operation, "definition", None) is not None: + other = LinearFunction(instruction.operation.definition) + else: + other = LinearFunction(instruction.operation) + + positions = [qc.find_bit(q).index for q in instruction.qubits] + other = other.extend_with_identity(len(mat), positions) + mat = np.dot(other.linear.astype(int), mat.astype(int)) % 2 + mat = mat.astype(bool) + + return mat + + @staticmethod + def _clifford_to_mat(cliff): + """This creates a nxn matrix corresponding to the given Clifford, when Clifford + can be converted to a linear function. This is possible when the clifford has + tableau of the form [[A, B], [C, D]], with B = C = 0 and D = A^{-1}^t, and zero + phase vector. In this case, the required matrix is A^t. + Raises an error otherwise. + """ + # Note: since cliff is a valid Clifford, then the condition D = A^{-1}^t + # holds automatically once B = C = 0. + if cliff.phase.any() or cliff.destab_z.any() or cliff.stab_x.any(): + raise CircuitError("The given clifford does not correspond to a linear function.") + return np.transpose(cliff.destab_x) + + @staticmethod + def _permutation_to_mat(perm): + """This creates a nxn matrix from a given permutation gate.""" + nq = len(perm.pattern) + mat = np.zeros((nq, nq), dtype=bool) + for i, j in enumerate(perm.pattern): + mat[i, j] = True + return mat + + def __eq__(self, other): + """Check if two linear functions represent the same matrix.""" + if not isinstance(other, LinearFunction): + return False + return (self.linear == other.linear).all() + def validate_parameter(self, parameter): """Parameter validation""" return parameter @@ -140,7 +225,7 @@ def synthesize(self): @property def linear(self): - """Returns the n x n matrix representing this linear function""" + """Returns the n x n matrix representing this linear function.""" return self.params[0] @property @@ -171,22 +256,47 @@ def permutation_pattern(self): locs = np.where(linear == 1) return locs[1] + def extend_with_identity(self, num_qubits: int, positions: list[int]) -> LinearFunction: + """Extend linear function to a linear function over nq qubits, + with identities on other subsystems. -def _linear_quantum_circuit_to_mat(qc: QuantumCircuit): - """This creates a n x n matrix corresponding to the given linear quantum circuit.""" - nq = qc.num_qubits - mat = np.eye(nq, nq, dtype=bool) - - for instruction in qc.data: - if instruction.operation.name == "cx": - cb = qc.find_bit(instruction.qubits[0]).index - tb = qc.find_bit(instruction.qubits[1]).index - mat[tb, :] = (mat[tb, :]) ^ (mat[cb, :]) - elif instruction.operation.name == "swap": - cb = qc.find_bit(instruction.qubits[0]).index - tb = qc.find_bit(instruction.qubits[1]).index - mat[[cb, tb]] = mat[[tb, cb]] - else: - raise CircuitError("A linear quantum circuit can include only CX and SWAP gates.") + Args: + num_qubits: number of qubits of the extended function. - return mat + positions: describes the positions of original qubits in the extended + function's qubits. + + Returns: + LinearFunction: extended linear function. + """ + extended_mat = np.eye(num_qubits, dtype=bool) + + for i, pos in enumerate(positions): + extended_mat[positions, pos] = self.linear[:, i] + + return LinearFunction(extended_mat) + + def mat_str(self): + """Return string representation of the linear function + viewed as a matrix with 0/1 entries. + """ + return str(self.linear.astype(int)) + + def function_str(self): + """Return string representation of the linear function + viewed as a linear transformation. + """ + out = "(" + mat = self.linear + for row in range(self.num_qubits): + first_entry = True + for col in range(self.num_qubits): + if mat[row, col]: + if not first_entry: + out += " + " + out += "x_" + str(col) + first_entry = False + if row != self.num_qubits - 1: + out += ", " + out += ")\n" + return out diff --git a/releasenotes/notes/linear-functions-usability-45265f293a80a6e5.yaml b/releasenotes/notes/linear-functions-usability-45265f293a80a6e5.yaml new file mode 100644 index 000000000000..ffffff32f169 --- /dev/null +++ b/releasenotes/notes/linear-functions-usability-45265f293a80a6e5.yaml @@ -0,0 +1,28 @@ +--- +features: + - | + Allowing to construct a :class:`.LinearFunction` object from more general quantum circuits, + that may contain: + + * Barriers (of type :class:`~qiskit.circuit.Barrier`) and delays (:class:`~qiskit.circuit.Delay`), + which are simply ignored + * Permutations (of type :class:`~qiskit.circuit.library.PermutationGate`) + * Other linear functions + * Cliffords (of type :class:`.Clifford`), when the Clifford represents a linear function + (and a ``CircuitError`` exception is raised if not) + * Nested quantum circuits of this form + + - | + Added :meth:`.LinearFunction.__eq__` method. Two objects of type :class:`.LinearFunction` + are considered equal when their representations as binary invertible matrices are equal. + - | + Added :meth:`.LinearFunction.extend_with_identity` method, which allows to extend + a linear function over ``k`` qubits to a linear function over ``n >= k`` qubits, + specifying the new positions of the original qubits and padding with identities on the + remaining qubits. + - | + Added two methods for pretty-printing :class:`.LinearFunction` objects: + :meth:`.LinearFunction.mat_str`, which returns the string representation of the linear + function viewed as a matrix with 0/1 entries, and + :meth:`.LinearFunction.function_str`, which returns the string representation of the + linear function viewed as a linear transformation. diff --git a/test/python/circuit/library/test_linear_function.py b/test/python/circuit/library/test_linear_function.py index f9b7e5dce21e..4209d1a4e4a0 100644 --- a/test/python/circuit/library/test_linear_function.py +++ b/test/python/circuit/library/test_linear_function.py @@ -18,36 +18,117 @@ from qiskit.test import QiskitTestCase from qiskit.circuit import QuantumCircuit +from qiskit.quantum_info import Clifford from qiskit.circuit.library.standard_gates import CXGate, SwapGate -from qiskit.circuit.library.generalized_gates import LinearFunction +from qiskit.circuit.library.generalized_gates import LinearFunction, PermutationGate from qiskit.circuit.exceptions import CircuitError from qiskit.synthesis.linear import random_invertible_binary_matrix from qiskit.quantum_info.operators import Operator -def random_linear_circuit(num_qubits, num_gates, seed=None): +def random_linear_circuit( + num_qubits, + num_gates, + seed=None, + barrier=False, + delay=False, + permutation=False, + linear=False, + clifford=False, + recursion_depth=0, +): + """Generate a pseudo random linear circuit.""" - instructions = { - "cx": (CXGate(), 2), - "swap": (SwapGate(), 2), - } + if num_qubits == 0: + raise CircuitError("Cannot construct a random linear circuit with 0 qubits.") + + circ = QuantumCircuit(num_qubits) + + instructions = ["cx", "swap"] if num_qubits >= 2 else [] + if barrier: + instructions.append("barrier") + if delay: + instructions.append("delay") + if permutation: + instructions.append("permutation") + if linear: + instructions.append("linear") + if clifford: + instructions.append("clifford") + if recursion_depth > 0: + instructions.append("nested") + + if not instructions: + # Return the empty circuit if there are no instructions to choose from. + return circ if isinstance(seed, np.random.Generator): rng = seed else: rng = np.random.default_rng(seed) - name_samples = rng.choice(tuple(instructions), num_gates) - - circ = QuantumCircuit(num_qubits) + name_samples = rng.choice(instructions, num_gates) for name in name_samples: - gate, nqargs = instructions[name] - qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() - circ.append(gate, qargs) + if name == "cx": + qargs = rng.choice(range(num_qubits), 2, replace=False).tolist() + circ.cx(*qargs) + elif name == "swap": + qargs = rng.choice(range(num_qubits), 2, replace=False).tolist() + circ.swap(*qargs) + elif name == "barrier": + nqargs = rng.choice(range(1, num_qubits + 1)) + qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() + circ.barrier(qargs) + elif name == "delay": + qarg = rng.choice(range(num_qubits)) + circ.delay(100, qarg) + elif name == "linear": + nqargs = rng.choice(range(1, num_qubits + 1)) + qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() + mat = random_invertible_binary_matrix(nqargs, seed=rng) + circ.append(LinearFunction(mat), qargs) + elif name == "permutation": + nqargs = rng.choice(range(1, num_qubits + 1)) + pattern = list(np.random.permutation(range(nqargs))) + qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() + circ.append(PermutationGate(pattern), qargs) + elif name == "clifford": + # In order to construct a Clifford that corresponds to a linear function, + # we construct a small random linear circuit, and convert it to Clifford. + nqargs = rng.choice(range(1, num_qubits + 1)) + qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() + subcirc = random_linear_circuit( + nqargs, + num_gates=5, + seed=rng, + barrier=False, + delay=False, + permutation=False, + linear=False, + clifford=False, + recursion_depth=0, + ) + cliff = Clifford(subcirc) + circ.append(cliff, qargs) + elif name == "nested": + nqargs = rng.choice(range(1, num_qubits + 1)) + qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() + subcirc = random_linear_circuit( + nqargs, + num_gates=5, + seed=rng, + barrier=False, + delay=False, + permutation=False, + linear=False, + clifford=False, + recursion_depth=recursion_depth - 1, + ) + circ.append(subcirc, qargs) return circ @@ -239,6 +320,189 @@ def test_no_original_definition(self): linear_function = LinearFunction(mat) self.assertIsNone(linear_function.original_circuit) + def test_barriers(self): + """Test constructing linear functions from circuits with barriers.""" + linear_circuit_1 = QuantumCircuit(4) + linear_circuit_1.cx(0, 1) + linear_circuit_1.cx(1, 2) + linear_circuit_1.cx(2, 3) + linear_function_1 = LinearFunction(linear_circuit_1) + + linear_circuit_2 = QuantumCircuit(4) + linear_circuit_2.barrier() + linear_circuit_2.cx(0, 1) + linear_circuit_2.cx(1, 2) + linear_circuit_2.barrier() + linear_circuit_2.cx(2, 3) + linear_circuit_2.barrier() + linear_function_2 = LinearFunction(linear_circuit_2) + + self.assertTrue(np.all(linear_function_1.linear == linear_function_2.linear)) + self.assertEqual(linear_function_1, linear_function_2) + + def test_delays(self): + """Test constructing linear functions from circuits with delays.""" + linear_circuit_1 = QuantumCircuit(4) + linear_circuit_1.cx(0, 1) + linear_circuit_1.cx(1, 2) + linear_circuit_1.cx(2, 3) + linear_function_1 = LinearFunction(linear_circuit_1) + + linear_circuit_2 = QuantumCircuit(4) + linear_circuit_2.delay(500, 1) + linear_circuit_2.cx(0, 1) + linear_circuit_2.cx(1, 2) + linear_circuit_2.delay(100, 0) + linear_circuit_2.cx(2, 3) + linear_circuit_2.delay(200, 2) + linear_function_2 = LinearFunction(linear_circuit_2) + + self.assertTrue(np.all(linear_function_1.linear == linear_function_2.linear)) + self.assertEqual(linear_function_1, linear_function_2) + + def test_eq(self): + """Test that checking equality between two linear functions only depends on matrices.""" + linear_circuit_1 = QuantumCircuit(3) + linear_circuit_1.cx(0, 1) + linear_circuit_1.cx(0, 2) + linear_function_1 = LinearFunction(linear_circuit_1) + + linear_circuit_2 = QuantumCircuit(3) + linear_circuit_2.cx(0, 2) + linear_circuit_2.cx(0, 1) + linear_function_2 = LinearFunction(linear_circuit_1) + + self.assertTrue(np.all(linear_function_1.linear == linear_function_2.linear)) + self.assertEqual(linear_function_1, linear_function_2) + + def test_extend_with_identity(self): + """Test extending linear function with identity.""" + lf = LinearFunction([[1, 1, 1], [0, 1, 1], [0, 0, 1]]) + + extended1 = lf.extend_with_identity(4, [0, 1, 2]) + expected1 = LinearFunction([[1, 1, 1, 0], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + self.assertEqual(extended1, expected1) + + extended2 = lf.extend_with_identity(4, [1, 2, 3]) + expected2 = LinearFunction([[1, 0, 0, 0], [0, 1, 1, 1], [0, 0, 1, 1], [0, 0, 0, 1]]) + self.assertEqual(extended2, expected2) + + extended3 = lf.extend_with_identity(4, [3, 2, 1]) + expected3 = LinearFunction([[1, 0, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 1, 1, 1]]) + self.assertEqual(extended3, expected3) + + def test_from_nested_quantum_circuit(self): + """Test constructing a linear function from a quantum circuit with + nested linear quantum circuits.""" + + qc1 = QuantumCircuit(3) + qc1.swap(1, 2) + + qc2 = QuantumCircuit(3) + qc2.append(qc1, [2, 1, 0]) # swaps 0 and 1 + qc2.swap(1, 2) # cycles 0->2->1->0 + + qc3 = QuantumCircuit(4) + qc3.append(qc2, [0, 1, 3]) # cycles 0->3->1->0, 2 untouched + + linear_function = LinearFunction(qc3) + expected = LinearFunction([[0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [1, 0, 0, 0]]) + self.assertEqual(linear_function, expected) + + def test_from_clifford_when_possible(self): + """Test constructing a linear function from a clifford which corresponds to a valid + linear function.""" + + qc = QuantumCircuit(3) + qc.cx(0, 1) + qc.swap(1, 2) + + # Linear function constructed from qc + linear_from_qc = LinearFunction(qc) + + # Linear function constructed from clifford + cliff = Clifford(qc) + linear_from_clifford = LinearFunction(cliff) + + self.assertEqual(linear_from_qc, linear_from_clifford) + + def test_to_clifford_and_back(self): + """Test converting linear function to clifford and back.""" + linear = LinearFunction([[1, 1, 1], [0, 1, 1], [0, 0, 1]]) + cliff = Clifford(linear) + linear_from_clifford = LinearFunction(cliff) + self.assertEqual(linear, linear_from_clifford) + + def test_from_clifford_when_impossible(self): + """Test that constructing a linear function from a clifford that does not correspond + to a linear function produces a circuit error.""" + qc = QuantumCircuit(3) + qc.cx(0, 1) + qc.h(0) + qc.swap(1, 2) + with self.assertRaises(CircuitError): + LinearFunction(qc) + + def test_from_permutation_gate(self): + """Test constructing a linear function from a permutation gate.""" + pattern = [1, 2, 0, 3] + perm_gate = PermutationGate(pattern) + linear_from_perm = LinearFunction(perm_gate) + self.assertTrue(linear_from_perm.is_permutation()) + extracted_pattern = linear_from_perm.permutation_pattern() + self.assertTrue(np.all(pattern == extracted_pattern)) + + def test_from_linear_function(self): + """Test constructing a linear function from another linear function.""" + linear_function1 = LinearFunction([[1, 1, 1], [0, 1, 1], [0, 0, 1]]) + linear_function2 = LinearFunction(linear_function1) + self.assertEqual(linear_function1, linear_function2) + + def test_from_quantum_circuit_with_linear_functions(self): + """Test constructing a linear function from a quantum circuit with + linear functions.""" + + qc1 = QuantumCircuit(3) + qc1.swap(1, 2) + linear1 = LinearFunction(qc1) + + qc2 = QuantumCircuit(2) + qc2.swap(0, 1) + linear2 = LinearFunction(qc2) + + qc3 = QuantumCircuit(4) + qc3.append(linear1, [0, 1, 2]) + qc3.append(linear2, [2, 3]) + linear3 = LinearFunction(qc3) + # linear3 is a permutation: 1->3->2, 0 unchanged + + expected = LinearFunction([[1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [0, 1, 0, 0]]) + self.assertEqual(linear3, expected) + + @data(2, 3, 4, 5, 6, 7, 8) + def test_clifford_linear_function_equivalence(self, num_qubits): + """Pseudo-random tests for constructing a random linear circuit, + converting this circuit both to a linear function and to a clifford, + and checking that the two are equivalent as Cliffords and as LinearFunctions. + """ + + # Note: Cliffords cannot be yet constructed from PermutationGate objects + qc = random_linear_circuit( + num_qubits, + 100, + seed=0, + barrier=True, + delay=True, + permutation=False, + linear=True, + clifford=True, + recursion_depth=2, + ) + qc_to_linear_function = LinearFunction(qc) + qc_to_clifford = Clifford(qc) + self.assertEqual(Clifford(qc_to_linear_function), qc_to_clifford) + self.assertEqual(qc_to_linear_function, LinearFunction(qc_to_clifford)) + if __name__ == "__main__": unittest.main()