diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index 9205ab0039..669fe34c6c 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -1420,6 +1420,10 @@ passing a symplectic matrix to the constructor. symplectic_matrix = backend.zero_state(nqubits=3) clifford = Clifford(symplectic_matrix, engine=NumpyBackend()) + # The initialization above is equivalent to the initialization below + circuit = Circuit(nqubits=3) + clifford = Clifford(circuit, engine=NumpyBackend()) + The generators of the stabilizers can be extracted with the :meth:`qibo.quantum_info.clifford.Clifford.generators` method, or the complete set of :math:`d = 2^{n}` stabilizers operators can be extracted through the diff --git a/src/qibo/backends/clifford.py b/src/qibo/backends/clifford.py index 307e6e1cdb..c81f8760da 100644 --- a/src/qibo/backends/clifford.py +++ b/src/qibo/backends/clifford.py @@ -627,13 +627,15 @@ def execute_circuit(self, circuit, initial_state=None, nshots: int = 1000): for gate in circuit.queue: state = gate.apply_clifford(self, state, nqubits) - return Clifford( + clifford = Clifford( state, measurements=circuit.measurements, nshots=nshots, engine=self.engine, ) + return clifford + except self.oom_error: # pragma: no cover raise_error( RuntimeError, diff --git a/src/qibo/quantum_info/_clifford_utils.py b/src/qibo/quantum_info/_clifford_utils.py new file mode 100644 index 0000000000..d3bbc342e4 --- /dev/null +++ b/src/qibo/quantum_info/_clifford_utils.py @@ -0,0 +1,581 @@ +"""Utility functions that support the Clifford submodule.""" + +from functools import reduce +from itertools import product + +import numpy as np + +from qibo import Circuit, gates +from qibo.config import raise_error + + +def _one_qubit_paulis_string_product(pauli_1: str, pauli_2: str): + """Calculate the product of two single-qubit Paulis represented as strings. + + Args: + pauli_1 (str): First Pauli operator. + pauli_2 (str): Second Pauli operator. + + Returns: + (str): Product of the two Pauli operators. + """ + products = { + "XY": "iZ", + "YZ": "iX", + "ZX": "iY", + "YX": "-iZ", + "ZY": "-iX", + "XZ": "iY", + "XX": "I", + "ZZ": "I", + "YY": "I", + "XI": "X", + "IX": "X", + "YI": "Y", + "IY": "Y", + "ZI": "Z", + "IZ": "Z", + } + prod = products[ + "".join([p.replace("i", "").replace("-", "") for p in (pauli_1, pauli_2)]) + ] + # calculate the phase + sign = len([True for p in (pauli_1, pauli_2, prod) if "-" in p]) + n_i = len([True for p in (pauli_1, pauli_2, prod) if "i" in p]) + sign = "-" if sign % 2 == 1 else "" + if n_i == 0: + i = "" + elif n_i == 1: + i = "i" + elif n_i == 2: + i = "" + sign = "-" if sign == "" else "" + elif n_i == 3: + i = "i" + sign = "-" if sign == "" else "" + return "".join([sign, i, prod.replace("i", "").replace("-", "")]) + + +def _string_product(operators: list): + """Calculates the tensor product of a list of operators represented as strings. + + Args: + operators (list): list of operators. + + Returns: + (str): String representing the tensor product of the operators. + """ + # calculate global sign + phases = len([True for op in operators if "-" in op]) + i = len([True for op in operators if "i" in op]) + # remove the - signs and the i + operators = "|".join(operators).replace("-", "").replace("i", "").split("|") + + prod = [] + for op in zip(*operators): + op = [o for o in op if o != "I"] + if len(op) == 0: + tmp = "I" + elif len(op) > 1: + tmp = reduce(_one_qubit_paulis_string_product, op) + else: + tmp = op[0] + # append signs coming from products + if tmp[0] == "-": + phases += 1 + # count i coming from products + if "i" in tmp: + i += 1 + prod.append(tmp.replace("i", "").replace("-", "")) + result = "".join(prod) + + # product of the i-s + if i % 4 == 1 or i % 4 == 3: + result = f"i{result}" + if i % 4 == 2 or i % 4 == 3: + phases += 1 + + phases = "-" if phases % 2 == 1 else "" + + return f"{phases}{result}" + + +def _decomposition_AG04(clifford): + """Returns a Clifford object decomposed into a circuit based on Aaronson-Gottesman method. + + Args: + clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object. + + Returns: + :class:`qibo.models.circuit.Circuit`: Clifford circuit. + + References: + 1. S. Aaronson, D. Gottesman, *Improved Simulation of Stabilizer Circuits*, + Phys. Rev. A 70, 052328 (2004). + `arXiv:quant-ph/0406196 `_ + """ + nqubits = clifford.nqubits + + circuit = Circuit(nqubits) + clifford_copy = clifford.copy(deep=True) + + if nqubits == 1: + return _single_qubit_clifford_decomposition(clifford_copy.symplectic_matrix) + + for k in range(nqubits): + # put a 1 one into position by permuting and using Hadamards(i,i) + _set_qubit_x_to_true(clifford_copy, circuit, k) + # make all entries in row i except ith equal to 0 + # by using phase gate and CNOTS + _set_row_x_to_zero(clifford_copy, circuit, k) + # treat Zs + _set_row_z_to_zero(clifford_copy, circuit, k) + + for k in range(nqubits): + if clifford_copy.symplectic_matrix[:nqubits, -1][k]: + clifford_copy.symplectic_matrix = clifford._backend.clifford_operations.Z( + clifford_copy.symplectic_matrix, k, nqubits + ) + circuit.add(gates.Z(k)) + if clifford_copy.symplectic_matrix[nqubits:-1, -1][k]: + clifford_copy.symplectic_matrix = clifford._backend.clifford_operations.X( + clifford_copy.symplectic_matrix, k, nqubits + ) + circuit.add(gates.X(k)) + + return circuit.invert() + + +def _decomposition_BM20(clifford): + """Optimal CNOT-cost decomposition of a Clifford operator on :math:`n \\in \\{2, 3 \\}` + into a circuit based on Bravyi-Maslov method. + + Args: + clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object. + + Returns: + :class:`qibo.models.circuit.Circuit`: Clifford circuit. + + References: + 1. S. Bravyi, D. Maslov, *Hadamard-free circuits expose the structure of the Clifford group*, + `arXiv:2003.09412 [quant-ph] `_. + """ + nqubits = clifford.nqubits + clifford_copy = clifford.copy(deep=True) + + if nqubits > 3: + raise_error( + ValueError, "This method can only be implemented for ``nqubits <= 3``." + ) + + if nqubits == 1: + return _single_qubit_clifford_decomposition(clifford_copy.symplectic_matrix) + + inverse_circuit = Circuit(nqubits) + + cnot_cost = _cnot_cost(clifford_copy) + + while cnot_cost > 0: + clifford_copy, inverse_circuit, cnot_cost = _reduce_cost( + clifford_copy, inverse_circuit, cnot_cost + ) + + last_row = clifford_copy.engine.cast([False] * 3, dtype=bool) + circuit = Circuit(nqubits) + for qubit in range(nqubits): + position = [qubit, qubit + nqubits] + single_qubit_circuit = _single_qubit_clifford_decomposition( + clifford_copy.engine.np.append( + clifford_copy.symplectic_matrix[position][:, position + [-1]], last_row + ).reshape(3, 3) + ) + if len(single_qubit_circuit.queue) > 0: + for gate in single_qubit_circuit.queue: + gate.init_args = [qubit] + gate.target_qubits = (qubit,) + circuit.queue.extend([gate]) + + if len(inverse_circuit.queue) > 0: + circuit.queue.extend(inverse_circuit.invert().queue) + + return circuit + + +def _single_qubit_clifford_decomposition(symplectic_matrix): + """Decompose symplectic matrix of a single-qubit Clifford into a Clifford circuit. + + Args: + symplectic_matrix (ndarray): Symplectic matrix to be decomposed. + + Returns: + :class:`qibo.models.circuit.Circuit`: Clifford circuit. + """ + circuit = Circuit(nqubits=1) + + destabilizer_phase, stabilizer_phase = symplectic_matrix[:-1, -1] + if destabilizer_phase and not stabilizer_phase: + circuit.add(gates.Z(0)) + elif not destabilizer_phase and stabilizer_phase: + circuit.add(gates.X(0)) + elif destabilizer_phase and stabilizer_phase: + circuit.add(gates.Y(0)) + + destabilizer_x, destabilizer_z = symplectic_matrix[0, 0], symplectic_matrix[0, 1] + stabilizer_x, stabilizer_z = symplectic_matrix[1, 0], symplectic_matrix[1, 1] + + if stabilizer_z and not stabilizer_x: + if destabilizer_z: + circuit.add(gates.S(0)) + elif not stabilizer_z and stabilizer_x: + if destabilizer_x: + circuit.add(gates.SDG(0)) + circuit.add(gates.H(0)) + else: + if not destabilizer_z: + circuit.add(gates.S(0)) + circuit.add(gates.H(0)) + circuit.add(gates.S(0)) + + return circuit + + +def _set_qubit_x_to_true(clifford, circuit: Circuit, qubit: int): + """Set a :math:`X`-destabilizer to ``True``. + + This is done by permuting columns ``l > qubit`` or, if necessary, applying a Hadamard. + + Args: + clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object. + circuit (:class:`qibo.models.circuit.Circuit`): circuit object. + qubit (int): index of the qubit to operate on. + """ + nqubits = clifford.nqubits + + x = clifford.destabilizers(symplectic=True) + x, z = x[:, :nqubits][qubit], x[:, nqubits:-1][qubit] + + if x[qubit]: + return + + for k in range(qubit + 1, nqubits): + if np.all(x[k]): + clifford.symplectic_matrix = clifford._backend.clifford_operations.SWAP( + clifford.symplectic_matrix, k, qubit, nqubits + ) + circuit.add(gates.SWAP(k, qubit)) + return + + for k in range(qubit, nqubits): + if np.all(z[k]): + clifford.symplectic_matrix = clifford._backend.clifford_operations.H( + clifford.symplectic_matrix, k, nqubits + ) + circuit.add(gates.H(k)) + if k != qubit: + clifford.symplectic_matrix = clifford._backend.clifford_operations.SWAP( + clifford.symplectic_matrix, k, qubit, nqubits + ) + circuit.add(gates.SWAP(k, qubit)) + return + + +def _set_row_x_to_zero(clifford, circuit: Circuit, qubit: int): + """Set :math:`X`-destabilizer to ``False`` for all ``k > qubit``. + + This is done by applying CNOTs, assuming ``k <= N`` and ``clifford.symplectic_matrix[k][k]=1``. + + Args: + clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object. + circuit (:class:`qibo.models.circuit.Circuit`): circuit object. + qubit (int): index of the qubit to operate on. + """ + nqubits = clifford.nqubits + + x = clifford.destabilizers(symplectic=True) + x, z = x[:, :nqubits][qubit], x[:, nqubits:-1][qubit] + + # Check X first + for k in range(qubit + 1, nqubits): + if x[k]: + clifford.symplectic_matrix = clifford._backend.clifford_operations.CNOT( + clifford.symplectic_matrix, qubit, k, nqubits + ) + circuit.add(gates.CNOT(qubit, k)) + + if np.any(z[qubit:]): + if not z[qubit]: + # to treat Zs: make sure row.Z[k] to True + clifford.symplectic_matrix = clifford._backend.clifford_operations.S( + clifford.symplectic_matrix, qubit, nqubits + ) + circuit.add(gates.S(qubit)) + + for k in range(qubit + 1, nqubits): + if z[k]: + clifford.symplectic_matrix = clifford._backend.clifford_operations.CNOT( + clifford.symplectic_matrix, k, qubit, nqubits + ) + circuit.add(gates.CNOT(k, qubit)) + + clifford.symplectic_matrix = clifford._backend.clifford_operations.S( + clifford.symplectic_matrix, qubit, nqubits + ) + circuit.add(gates.S(qubit)) + + +def _set_row_z_to_zero(clifford, circuit: Circuit, qubit: int): + """Set :math:`Z`-stabilizer to ``False`` for all ``i > qubit``. + + Implemented by applying (reverse) CNOTs. + It assumes ``qubit < nqubits`` and that ``_set_row_x_to_zero`` has been called first. + + Args: + clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object. + circuit (:class:`qibo.models.circuit.Circuit`): circuit object. + qubit (int): index of the qubit to operate on. + """ + nqubits = clifford.nqubits + + x = clifford.stabilizers(symplectic=True) + x, z = x[:, :nqubits][qubit], x[:, nqubits:-1][qubit] + + if np.any(z[qubit + 1 :]): + for k in range(qubit + 1, nqubits): + if z[k]: + clifford.symplectic_matrix = clifford._backend.clifford_operations.CNOT( + clifford.symplectic_matrix, k, qubit, nqubits + ) + circuit.add(gates.CNOT(k, qubit)) + + if np.any(x[qubit:]): + clifford.symplectic_matrix = clifford._backend.clifford_operations.H( + clifford.symplectic_matrix, qubit, nqubits + ) + circuit.add(gates.H(qubit)) + for k in range(qubit + 1, nqubits): + if x[k]: + clifford.symplectic_matrix = clifford._backend.clifford_operations.CNOT( + clifford.symplectic_matrix, qubit, k, nqubits + ) + circuit.add(gates.CNOT(qubit, k)) + if z[qubit]: + clifford.symplectic_matrix = clifford._backend.clifford_operations.S( + clifford.symplectic_matrix, qubit, nqubits + ) + circuit.add(gates.S(qubit)) + clifford.symplectic_matrix = clifford._backend.clifford_operations.H( + clifford.symplectic_matrix, qubit, nqubits + ) + circuit.add(gates.H(qubit)) + + +def _cnot_cost(clifford): + """Returns the number of CNOT gates required for Clifford decomposition. + + Args: + clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object. + + Returns: + int: Number of CNOT gates required. + """ + if clifford.nqubits > 3: + raise_error(ValueError, "No Clifford CNOT cost function for ``nqubits > 3``.") + + if clifford.nqubits == 3: + return _cnot_cost3(clifford) + + return _cnot_cost2(clifford) + + +def _rank_2(a: bool, b: bool, c: bool, d: bool): + """Returns rank of 2x2 boolean matrix.""" + if (a & d) ^ (b & c): + return 2 + if a or b or c or d: + return 1 + return 0 + + +def _cnot_cost2(clifford): + """Returns CNOT cost of a two-qubit Clifford. + + Args: + clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object. + + Returns: + int: Number of CNOT gates required. + """ + symplectic_matrix = clifford.symplectic_matrix[:-1, :-1] + + r00 = _rank_2( + symplectic_matrix[0, 0], + symplectic_matrix[0, 2], + symplectic_matrix[2, 0], + symplectic_matrix[2, 2], + ) + r01 = _rank_2( + symplectic_matrix[0, 1], + symplectic_matrix[0, 3], + symplectic_matrix[2, 1], + symplectic_matrix[2, 3], + ) + + if r00 == 2: + return r01 + + return r01 + 1 - r00 + + +def _cnot_cost3(clifford): # pragma: no cover + """Return CNOT cost of a 3-qubit clifford. + + Args: + clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object. + + Returns: + int: Number of CNOT gates required. + """ + + symplectic_matrix = clifford.symplectic_matrix[:-1, :-1] + + nqubits = 3 + + R1 = np.zeros((nqubits, nqubits), dtype=int) + R2 = np.zeros((nqubits, nqubits), dtype=int) + for q1 in range(nqubits): + for q2 in range(nqubits): + R2[q1, q2] = _rank_2( + symplectic_matrix[q1, q2], + symplectic_matrix[q1, q2 + nqubits], + symplectic_matrix[q1 + nqubits, q2], + symplectic_matrix[q1 + nqubits, q2 + nqubits], + ) + mask = np.zeros(2 * nqubits, dtype=int) + mask = clifford.engine.cast(mask, dtype=mask.dtype) + mask[[q2, q2 + nqubits]] = 1 + loc_y_x = np.array_equal( + symplectic_matrix[q1, :] & mask, symplectic_matrix[q1, :] + ) + loc_y_z = np.array_equal( + symplectic_matrix[q1 + nqubits, :] & mask, + symplectic_matrix[q1 + nqubits, :], + ) + loc_y_y = np.array_equal( + (symplectic_matrix[q1, :] ^ symplectic_matrix[q1 + nqubits, :]) & mask, + (symplectic_matrix[q1, :] ^ symplectic_matrix[q1 + nqubits, :]), + ) + R1[q1, q2] = 1 * (loc_y_x or loc_y_z or loc_y_y) + 1 * ( + loc_y_x and loc_y_z and loc_y_y + ) + + diag1 = np.sort(np.diag(R1)).tolist() + diag2 = np.sort(np.diag(R2)).tolist() + + nz1 = np.count_nonzero(R1) + nz2 = np.count_nonzero(R2) + + if diag1 == [2, 2, 2]: + return 0 + + if diag1 == [1, 1, 2]: + return 1 + + if ( + diag1 == [0, 1, 1] + or (diag1 == [1, 1, 1] and nz2 < 9) + or (diag1 == [0, 0, 2] and diag2 == [1, 1, 2]) + ): + return 2 + + if ( + (diag1 == [1, 1, 1] and nz2 == 9) + or ( + diag1 == [0, 0, 1] + and (nz1 == 1 or diag2 == [2, 2, 2] or (diag2 == [1, 1, 2] and nz2 < 9)) + ) + or (diag1 == [0, 0, 2] and diag2 == [0, 0, 2]) + or (diag2 == [1, 2, 2] and nz1 == 0) + ): + return 3 + + if diag2 == [0, 0, 1] or ( + diag1 == [0, 0, 0] + and ( + (diag2 == [1, 1, 1] and nz2 == 9 and nz1 == 3) + or (diag2 == [0, 1, 1] and nz2 == 8 and nz1 == 2) + ) + ): + return 5 + + if nz1 == 3 and nz2 == 3: + return 6 + + return 4 + + +def _reduce_cost(clifford, inverse_circuit: Circuit, cost: int): # pragma: no cover + """Step that tries to reduce the two-qubit cost of a Clifford circuit. + + Args: + clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object. + circuit (:class:`qibo.models.circuit.Circuit`): circuit object. + cost (int): initial cost. + """ + nqubits = clifford.nqubits + + for control in range(nqubits): + for target in range(control + 1, nqubits): + for n0, n1 in product(range(3), repeat=2): + reduced = clifford.copy(deep=True) + for qubit, n in [(control, n0), (target, n1)]: + if n == 1: + reduced.symplectic_matrix = ( + reduced._backend.clifford_operations.SDG( + reduced.symplectic_matrix, qubit, nqubits + ) + ) + reduced.symplectic_matrix = ( + reduced._backend.clifford_operations.H( + reduced.symplectic_matrix, qubit, nqubits + ) + ) + elif n == 2: + reduced.symplectic_matrix = ( + reduced._backend.clifford_operations.SDG( + reduced.symplectic_matrix, qubit, nqubits + ) + ) + reduced.symplectic_matrix = ( + reduced._backend.clifford_operations.H( + reduced.symplectic_matrix, qubit, nqubits + ) + ) + reduced.symplectic_matrix = ( + reduced._backend.clifford_operations.SDG( + reduced.symplectic_matrix, qubit, nqubits + ) + ) + reduced.symplectic_matrix = ( + reduced._backend.clifford_operations.H( + reduced.symplectic_matrix, qubit, nqubits + ) + ) + reduced.symplectic_matrix = reduced._backend.clifford_operations.CNOT( + reduced.symplectic_matrix, control, target, nqubits + ) + + new_cost = _cnot_cost(reduced) + + if new_cost == cost - 1: + for qubit, n in [(control, n0), (target, n1)]: + if n == 1: + inverse_circuit.add(gates.SDG(qubit)) + inverse_circuit.add(gates.H(qubit)) + elif n == 2: + inverse_circuit.add(gates.H(qubit)) + inverse_circuit.add(gates.S(qubit)) + inverse_circuit.add(gates.CNOT(control, target)) + + return reduced, inverse_circuit, new_cost + + raise_error(RuntimeError, "Failed to reduce CNOT cost.") diff --git a/src/qibo/quantum_info/clifford.py b/src/qibo/quantum_info/clifford.py index 53075abc9c..9bc52c66b1 100644 --- a/src/qibo/quantum_info/clifford.py +++ b/src/qibo/quantum_info/clifford.py @@ -1,6 +1,6 @@ """Module definig the Clifford object, which allows phase-space representation of Clifford circuits and stabilizer states.""" -from dataclasses import dataclass +from dataclasses import dataclass, field from functools import reduce from itertools import product from typing import Optional, Union @@ -13,13 +13,18 @@ from qibo.gates import M from qibo.measurements import frequencies_to_binary +from ._clifford_utils import _decomposition_AG04, _decomposition_BM20, _string_product + @dataclass class Clifford: """Object storing the results of a circuit execution with the :class:`qibo.backends.clifford.CliffordBackend`. Args: - symplectic_matrix (ndarray): Symplectic matrix of the state in phase-space representation. + data (ndarray or :class:`qibo.models.circuit.Circuit`): If ``ndarray``, it is the + symplectic matrix of the stabilizer state in phase-space representation. + If :class:`qibo.models.circuit.Circuit`, it is a circuit composed only of Clifford + gates and computational-basis measurements. nqubits (int, optional): number of qubits of the state. measurements (list, optional): list of measurements gates :class:`qibo.gates.M`. Defaults to ``None``. @@ -32,7 +37,8 @@ class Clifford: Defaults to ``None``. """ - symplectic_matrix: np.ndarray + symplectic_matrix: np.ndarray = field(init=False) + data: Union[np.ndarray, Circuit] = field(repr=False) nqubits: Optional[int] = None measurements: Optional[list] = None nshots: int = 1000 @@ -43,13 +49,24 @@ class Clifford: _samples: Optional[int] = None def __post_init__(self): - # adding the scratch row if not provided - if self.symplectic_matrix.shape[0] % 2 == 0: - self.symplectic_matrix = np.vstack( - (self.symplectic_matrix, np.zeros(self.symplectic_matrix.shape[1])) - ) - self.nqubits = int((self.symplectic_matrix.shape[1] - 1) / 2) - self._backend = CliffordBackend(self.engine) + if isinstance(self.data, Circuit): + clifford = self.from_circuit(self.data, engine=self.engine) + self.symplectic_matrix = clifford.symplectic_matrix + self.nqubits = clifford.nqubits + self.measurements = clifford.measurements + self.engine = clifford.engine + self._samples = clifford._samples + self._measurement_gate = clifford._measurement_gate + self._backend = clifford._backend + else: + # adding the scratch row if not provided + self.symplectic_matrix = self.data + if self.symplectic_matrix.shape[0] % 2 == 0: + self.symplectic_matrix = np.vstack( + (self.symplectic_matrix, np.zeros(self.symplectic_matrix.shape[1])) + ) + self.nqubits = int((self.symplectic_matrix.shape[1] - 1) / 2) + self._backend = CliffordBackend(self.engine) @classmethod def from_circuit( @@ -59,24 +76,55 @@ def from_circuit( nshots: int = 1000, engine: Optional[Backend] = None, ): - """Allows to create a ``Clifford`` object by executing the input circuit. + """Allows to create a :class:`qibo.quantum_info.clifford.Clifford` object by executing the input circuit. Args: circuit (:class:`qibo.models.circuit.Circuit`): Clifford circuit to run. - initial_state (np.ndarray): The initial tableu state. - nshots (int): The number of shots to perform. + initial_state (ndarray, optional): symplectic matrix of the initial state. + If ``None``, defaults to the symplectic matrix of the zero state. + Defaults to ``None``. + nshots (int, optional): number of measurement shots to perform + if ``circuit`` has measurement gates. Defaults to :math:`10^{3}`. + engine (:class:`qibo.backends.abstract.Backend`, optional): engine to use in the + execution of the :class:`qibo.backends.CliffordBackend`. + It accepts all ``qibo`` backends besides the + :class:`qibo.backends.TensorflowBackend`, which is not supported. + If ``None``, defaults to :class:`qibo.backends.NumpyBackend`. + Defaults to ``None``. Returns: - (:class:`qibo.quantum_info.clifford.Clifford`): The object storing the result of the circuit execution. + (:class:`qibo.quantum_info.clifford.Clifford`): Object storing the result of the circuit execution. """ cls._backend = CliffordBackend(engine) return cls._backend.execute_circuit(circuit, initial_state, nshots) - # TODO: implement this method in separate PR based on `Bravyi & Maslov (2022) `_. - @classmethod - def to_circuit(cls, algorithm=None): # pragma: no cover - raise_error(NotImplementedError, "`to_circuit` method not implemented yet.") + def to_circuit(self, algorithm: Optional[str] = "AG04"): + """Converts symplectic matrix into a Clifford circuit. + + Args: + algorithm (str, optional): If ``AG04``, uses the decomposition algorithm from + `Aaronson & Gottesman (2004) `_. + If ``BM20`` and ``Clifford.nqubits <= 3``, uses the decomposition algorithm from + `Bravyi & Maslov (2020) `_. + Defaults to ``AG04``. + + Returns: + :class:`qibo.models.circuit.Circuit`: circuit composed of Clifford gates. + """ + if not isinstance(algorithm, str): + raise_error( + TypeError, + f"``algorithm`` must be type str, but it is type {type(algorithm)}", + ) + + if algorithm not in ["AG04", "BM20"]: + raise_error(ValueError, f"``algorithm`` {algorithm} not found.") + + if algorithm == "BM20": + return _decomposition_BM20(self) + + return _decomposition_AG04(self) def generators(self, return_array: bool = False): """Extracts the generators of stabilizers and destabilizers. @@ -92,38 +140,53 @@ def generators(self, return_array: bool = False): self.symplectic_matrix, return_array ) - def stabilizers(self, return_array: bool = False): + def stabilizers(self, symplectic: bool = False, return_array: bool = False): """Extracts the stabilizers of the state. Args: - return_array (bool, optional): If ``True`` returns the stabilizers as ``ndarray``. + symplectic (bool, optional): If ``True``, returns the rows of the symplectic matrix + that correspond to the :math:`n` generators of the :math:`2^{n}` total stabilizers, + independently of ``return_array``. + return_array (bool, optional): To be used when ``symplectic = False``. + If ``True`` returns the stabilizers as ``ndarray``. If ``False``, returns stabilizers as strings. Defaults to ``False``. Returns: - (list): Stabilizers of the state. + (ndarray or list): Stabilizers of the state. """ - generators, phases = self.generators(return_array) + if not symplectic: + generators, phases = self.generators(return_array) - return self._construct_operators( - generators[self.nqubits :], - phases[self.nqubits :], - ) + return self._construct_operators( + generators[self.nqubits :], + phases[self.nqubits :], + ) + + return self.symplectic_matrix[self.nqubits : -1, :] - def destabilizers(self, return_array: bool = False): + def destabilizers(self, symplectic: bool = False, return_array: bool = False): """Extracts the destabilizers of the state. Args: - return_array (bool, optional): If ``True`` returns the destabilizers as ``ndarray``. - If ``False``, their representation as strings is returned. Defaults to ``False``. + symplectic (bool, optional): If ``True``, returns the rows of the symplectic matrix + that correspond to the :math:`n` generators of the :math:`2^{n}` total + destabilizers, independently of ``return_array``. + return_array (bool, optional): To be used when ``symplectic = False``. + If ``True`` returns the destabilizers as ``ndarray``. + If ``False``, their representation as strings is returned. + Defaults to ``False``. Returns: - (list): Destabilizers of the state. + (ndarray or list): Destabilizers of the state. """ - generators, phases = self.generators(return_array) + if not symplectic: + generators, phases = self.generators(return_array) - return self._construct_operators( - generators[: self.nqubits], phases[: self.nqubits] - ) + return self._construct_operators( + generators[: self.nqubits], phases[: self.nqubits] + ) + + return self.symplectic_matrix[: self.nqubits, :] def state(self): """Builds the density matrix representation of the state. @@ -134,7 +197,7 @@ def state(self): Returns: (ndarray): Density matrix of the state. """ - stabilizers = self.stabilizers(True) + stabilizers = self.stabilizers(return_array=True) return self.engine.np.sum(stabilizers, axis=0) / len(stabilizers) @@ -304,6 +367,31 @@ def probabilities(self, qubits: Optional[Union[tuple, list]] = None): self.engine.np.sqrt(probs), qubits, len(measured_qubits) ) + def copy(self, deep: bool = False): + """Returns copy of :class:`qibo.quantum_info.clifford.Clifford` object. + + Args: + deep (bool, optional): If ``True``, creates another copy in memory. + Defaults to ``False``. + + Returns: + :class:`qibo.quantum_info.clifford.Clifford`: copy of original ``Clifford`` object. + """ + if not isinstance(deep, bool): + raise_error( + TypeError, f"``deep`` must be type bool, but it is type {type(deep)}." + ) + + symplectic_matrix = ( + self.engine.np.copy(self.symplectic_matrix) + if deep + else self.symplectic_matrix + ) + + return self.__class__( + symplectic_matrix, self.nqubits, self.measurements, self.nshots, self.engine + ) + def _construct_operators(self, generators: list, phases: list): """Helper function to construct all the operators from their generators. @@ -339,94 +427,3 @@ def _construct_operators(self, generators: list, phases: list): operators = [(g, identity) for g in operators] return [_string_product(ops) for ops in product(*operators)] - - -def _one_qubit_paulis_string_product(pauli_1: str, pauli_2: str): - """Calculate the product of two single-qubit Paulis represented as strings. - - Args: - pauli_1 (str): First Pauli operator. - pauli_2 (str): Second Pauli operator. - - Returns: - (str): Product of the two Pauli operators. - """ - products = { - "XY": "iZ", - "YZ": "iX", - "ZX": "iY", - "YX": "-iZ", - "ZY": "-iX", - "XZ": "iY", - "XX": "I", - "ZZ": "I", - "YY": "I", - "XI": "X", - "IX": "X", - "YI": "Y", - "IY": "Y", - "ZI": "Z", - "IZ": "Z", - } - prod = products[ - "".join([p.replace("i", "").replace("-", "") for p in (pauli_1, pauli_2)]) - ] - # calculate the phase - sign = len([True for p in (pauli_1, pauli_2, prod) if "-" in p]) - n_i = len([True for p in (pauli_1, pauli_2, prod) if "i" in p]) - sign = "-" if sign % 2 == 1 else "" - if n_i == 0: - i = "" - elif n_i == 1: - i = "i" - elif n_i == 2: - i = "" - sign = "-" if sign == "" else "" - elif n_i == 3: - i = "i" - sign = "-" if sign == "" else "" - return "".join([sign, i, prod.replace("i", "").replace("-", "")]) - - -def _string_product(operators: list): - """Calculates the tensor product of a list of operators represented as strings. - - Args: - operators (list): list of operators. - - Returns: - (str): String representing the tensor product of the operators. - """ - # calculate global sign - phases = len([True for op in operators if "-" in op]) - i = len([True for op in operators if "i" in op]) - # remove the - signs and the i - operators = "|".join(operators).replace("-", "").replace("i", "").split("|") - - prod = [] - for op in zip(*operators): - op = [o for o in op if o != "I"] - if len(op) == 0: - tmp = "I" - elif len(op) > 1: - tmp = reduce(_one_qubit_paulis_string_product, op) - else: - tmp = op[0] - # append signs coming from products - if tmp[0] == "-": - phases += 1 - # count i coming from products - if "i" in tmp: - i += 1 - prod.append(tmp.replace("i", "").replace("-", "")) - result = "".join(prod) - - # product of the i-s - if i % 4 == 1 or i % 4 == 3: - result = f"i{result}" - if i % 4 == 2 or i % 4 == 3: - phases += 1 - - phases = "-" if phases % 2 == 1 else "" - - return f"{phases}{result}" diff --git a/tests/test_quantum_info_clifford.py b/tests/test_quantum_info_clifford.py index 1f8f18ac6e..3290aefc31 100644 --- a/tests/test_quantum_info_clifford.py +++ b/tests/test_quantum_info_clifford.py @@ -6,11 +6,12 @@ from qibo import Circuit, gates from qibo.backends import CliffordBackend, TensorflowBackend -from qibo.quantum_info.clifford import ( - Clifford, +from qibo.quantum_info._clifford_utils import ( + _cnot_cost, _one_qubit_paulis_string_product, _string_product, ) +from qibo.quantum_info.clifford import Clifford from qibo.quantum_info.random_ensembles import random_clifford @@ -61,12 +62,89 @@ def test_clifford_from_circuit(backend, measurement): backend.assert_allclose(obj.probabilities(), result.probabilities()) +@pytest.mark.parametrize("algorithm", ["AG04", "BM20"]) +@pytest.mark.parametrize("nqubits", [1, 2, 3, 10, 50]) +def test_clifford_to_circuit(backend, nqubits, algorithm): + if backend.__class__.__name__ == "TensorflowBackend": + pytest.skip("CliffordBackend not defined for Tensorflow engine.") + + clifford = random_clifford(nqubits, backend=backend) + + symplectic_matrix_original = Clifford.from_circuit( + clifford, engine=backend + ).symplectic_matrix + + symplectic_matrix_from_symplectic = Clifford( + symplectic_matrix_original, engine=backend + ) + + symplectic_matrix_compiled = Clifford.from_circuit(clifford, engine=backend) + + if algorithm == "BM20" and nqubits > 3: + with pytest.raises(ValueError): + symplectic_matrix_compiled = symplectic_matrix_compiled.to_circuit( + algorithm=algorithm + ) + with pytest.raises(ValueError): + _cnot_cost(symplectic_matrix_compiled) + else: + with pytest.raises(TypeError): + symplectic_matrix_compiled.to_circuit(algorithm=True) + with pytest.raises(ValueError): + symplectic_matrix_compiled.to_circuit(algorithm="BM21") + + symplectic_matrix_from_symplectic = ( + symplectic_matrix_from_symplectic.to_circuit(algorithm=algorithm) + ) + symplectic_matrix_from_symplectic = Clifford.from_circuit( + symplectic_matrix_from_symplectic, engine=backend + ).symplectic_matrix + + symplectic_matrix_compiled = symplectic_matrix_compiled.to_circuit( + algorithm=algorithm + ) + symplectic_matrix_compiled = Clifford.from_circuit( + symplectic_matrix_compiled, engine=backend + ).symplectic_matrix + + backend.assert_allclose( + symplectic_matrix_from_symplectic, symplectic_matrix_original + ) + backend.assert_allclose(symplectic_matrix_compiled, symplectic_matrix_original) + + +@pytest.mark.parametrize("nqubits", [1, 10, 50]) +def test_clifford_initialization(backend, nqubits): + if backend.__class__.__name__ == "TensorflowBackend": + pytest.skip("CliffordBackend not defined for Tensorflow engine.") + + clifford_backend = construct_clifford_backend(backend) + + circuit = random_clifford(nqubits, backend=backend) + symplectic_matrix = clifford_backend.execute_circuit(circuit).symplectic_matrix + + clifford_from_symplectic = Clifford(symplectic_matrix, engine=backend) + clifford_from_circuit = Clifford.from_circuit(circuit, engine=backend) + clifford_from_initialization = Clifford(circuit, engine=backend) + + backend.assert_allclose( + clifford_from_symplectic.symplectic_matrix, symplectic_matrix + ) + backend.assert_allclose(clifford_from_circuit.symplectic_matrix, symplectic_matrix) + backend.assert_allclose( + clifford_from_initialization.symplectic_matrix, symplectic_matrix + ) + + @pytest.mark.parametrize("return_array", [True, False]) -def test_clifford_stabilizers(backend, return_array): +@pytest.mark.parametrize("symplectic", [True, False]) +def test_clifford_stabilizers(backend, symplectic, return_array): clifford_backend = construct_clifford_backend(backend) if not clifford_backend: return - c = Circuit(3) + + nqubits = 3 + c = Circuit(nqubits) c.add(gates.X(2)) c.add(gates.H(0)) obj = Clifford.from_circuit(c, engine=backend) @@ -78,7 +156,7 @@ def test_clifford_stabilizers(backend, return_array): else: true_generators = ["XII", "IZI", "IIZ"] true_phases = [1, 1, -1] - generators, phases = obj.generators(return_array) + generators, phases = obj.generators(return_array=return_array) if return_array: backend.assert_allclose(generators[3:], true_generators) @@ -87,7 +165,9 @@ def test_clifford_stabilizers(backend, return_array): assert generators[3:] == true_generators assert phases.tolist()[3:] == true_phases - if return_array: + if symplectic: + true_stabilizers = obj.symplectic_matrix[nqubits:-1, :] + elif not symplectic and return_array: true_stabilizers = [] for stab in [ "-XZZ", @@ -105,7 +185,7 @@ def test_clifford_stabilizers(backend, return_array): if "-" in stab: tmp *= -1 true_stabilizers.append(tmp) - else: + elif not symplectic and not return_array: true_stabilizers = [ "-XZZ", "XZI", @@ -116,19 +196,23 @@ def test_clifford_stabilizers(backend, return_array): "-IIZ", "III", ] - stabilizers = obj.stabilizers(return_array) - if return_array: + + stabilizers = obj.stabilizers(symplectic, return_array) + if symplectic or (not symplectic and return_array): backend.assert_allclose(stabilizers, true_stabilizers) else: assert stabilizers, true_stabilizers @pytest.mark.parametrize("return_array", [True, False]) -def test_clifford_destabilizers(backend, return_array): +@pytest.mark.parametrize("symplectic", [True, False]) +def test_clifford_destabilizers(backend, symplectic, return_array): clifford_backend = construct_clifford_backend(backend) if not clifford_backend: return - c = Circuit(3) + + nqubits = 3 + c = Circuit(nqubits) c.add(gates.X(2)) c.add(gates.H(0)) obj = Clifford.from_circuit(c, engine=backend) @@ -141,17 +225,18 @@ def test_clifford_destabilizers(backend, return_array): else: true_generators = ["ZII", "IXI", "IIX"] true_phases = [1, 1, 1] - generators, phases = obj.generators(return_array) + generators, phases = obj.generators(return_array=return_array) if return_array: - print(type(generators), type(true_generators)) backend.assert_allclose(generators[:3], true_generators) backend.assert_allclose(phases.tolist()[:3], true_phases) else: assert generators[:3] == true_generators assert phases.tolist()[:3] == true_phases - if return_array: + if symplectic: + true_destabilizers = obj.symplectic_matrix[:nqubits, :] + elif not symplectic and return_array: true_destabilizers = [] for destab in [ "ZXX", @@ -170,7 +255,7 @@ def test_clifford_destabilizers(backend, return_array): if "-" in destab: tmp *= -1 true_destabilizers.append(tmp) - else: + elif not symplectic and not return_array: true_destabilizers = [ "ZXX", "ZXI", @@ -181,8 +266,8 @@ def test_clifford_destabilizers(backend, return_array): "IIX", "III", ] - destabilizers = obj.destabilizers(return_array) - if return_array: + destabilizers = obj.destabilizers(symplectic, return_array) + if symplectic or (not symplectic and return_array): backend.assert_allclose(destabilizers, true_destabilizers) else: assert destabilizers, true_destabilizers @@ -237,6 +322,27 @@ def test_clifford_samples_error(backend): assert str(excinfo.value) == "No measurement provided." +@pytest.mark.parametrize("deep", [False, True]) +@pytest.mark.parametrize("nqubits", [1, 10, 100]) +def test_clifford_copy(backend, nqubits, deep): + if backend.__class__.__name__ == "TensorflowBackend": + pytest.skip("CliffordBackend not defined for Tensorflow engine.") + + circuit = random_clifford(nqubits, backend=backend) + clifford = Clifford.from_circuit(circuit, engine=backend) + + with pytest.raises(TypeError): + clifford.copy(deep="True") + + copy = clifford.copy(deep=deep) + + backend.assert_allclose(copy.symplectic_matrix, clifford.symplectic_matrix) + assert copy.nqubits == clifford.nqubits + assert copy.measurements == clifford.measurements + assert copy.nshots == clifford.nshots + assert copy.engine == clifford.engine + + @pytest.mark.parametrize("pauli_2", ["Z", "Y", "Y"]) @pytest.mark.parametrize("pauli_1", ["X", "Y", "Z"]) def test_one_qubit_paulis_string_product(backend, pauli_1, pauli_2):