Source code for graphix.sim.density_matrix

"""Density matrix simulator.

Simulate MBQC with density matrix representation.
"""

from copy import deepcopy

import numpy as np

import graphix.sim.base_backend
from graphix.channels import KrausChannel
from graphix.clifford import CLIFFORD
from graphix.linalg_validations import check_hermitian, check_square, check_unit_trace
from graphix.ops import Ops
from graphix.sim.statevec import CNOT_TENSOR, CZ_TENSOR, SWAP_TENSOR, meas_op


[docs]class DensityMatrix: """DensityMatrix object."""
[docs] def __init__(self, data=None, plus_state=True, nqubit=1): """ Parameters ---------- data : DensityMatrix, list, tuple, np.ndarray or None Density matrix of shape (2**nqubits, 2**nqubits). nqubit : int Number of qubits. Default is 1. If both `data` and `nqubit` are specified, `nqubit` is ignored. """ if data is None: assert nqubit >= 0 self.Nqubit = nqubit if plus_state: self.rho = np.ones((2**nqubit, 2**nqubit)) / 2**nqubit else: self.rho = np.zeros((2**nqubit, 2**nqubit)) self.rho[0, 0] = 1.0 else: if isinstance(data, DensityMatrix): data = data.rho elif isinstance(data, (list, tuple)): data = np.asarray(data, dtype=complex) elif isinstance(data, np.ndarray): pass else: raise TypeError("data must be DensityMatrix, list, tuple, or np.ndarray.") assert check_square(data) self.Nqubit = len(data).bit_length() - 1 self.rho = data assert check_hermitian(self.rho) assert check_unit_trace(self.rho)
def __repr__(self): return f"DensityMatrix, data={self.rho}, shape={self.dims()}"
[docs] def evolve_single(self, op, i): """Single-qubit operation. Parameters ---------- op : np.ndarray 2*2 matrix. i : int Index of qubit to apply operator. """ assert i >= 0 and i < self.Nqubit if op.shape != (2, 2): raise ValueError("op must be 2*2 matrix.") rho_tensor = self.rho.reshape((2,) * self.Nqubit * 2) rho_tensor = np.tensordot(np.tensordot(op, rho_tensor, axes=[1, i]), op.conj().T, axes=[i + self.Nqubit, 0]) rho_tensor = np.moveaxis(rho_tensor, (0, -1), (i, i + self.Nqubit)) self.rho = rho_tensor.reshape((2**self.Nqubit, 2**self.Nqubit))
[docs] def evolve(self, op, qargs): """Multi-qubit operation Args: op (np.array): 2^n*2^n matrix qargs (list of ints): target qubits' indexes """ d = op.shape # check it is a matrix. if len(d) == 2: # check it is square if d[0] == d[1]: pass else: raise ValueError(f"The provided operator has shape {op.shape} and is not a square matrix.") else: raise ValueError(f"The provided data has incorrect shape {op.shape}.") nqb_op = np.log2(len(op)) if not np.isclose(nqb_op, int(nqb_op)): raise ValueError("Incorrect operator dimension: not consistent with qubits.") nqb_op = int(nqb_op) if nqb_op != len(qargs): raise ValueError("The dimension of the operator doesn't match the number of targets.") if not all(0 <= i < self.Nqubit for i in qargs): raise ValueError("Incorrect target indices.") if len(set(qargs)) != nqb_op: raise ValueError("A repeated target qubit index is not possible.") op_tensor = op.reshape((2,) * 2 * nqb_op) rho_tensor = self.rho.reshape((2,) * self.Nqubit * 2) rho_tensor = np.tensordot( np.tensordot(op_tensor, rho_tensor, axes=[tuple(nqb_op + i for i in range(len(qargs))), tuple(qargs)]), op.conj().T.reshape((2,) * 2 * nqb_op), axes=[tuple(i + self.Nqubit for i in qargs), tuple(i for i in range(len(qargs)))], ) rho_tensor = np.moveaxis( rho_tensor, [i for i in range(len(qargs))] + [-i for i in range(1, len(qargs) + 1)], [i for i in qargs] + [i + self.Nqubit for i in reversed(list(qargs))], ) self.rho = rho_tensor.reshape((2**self.Nqubit, 2**self.Nqubit))
[docs] def expectation_single(self, op, i): """Expectation value of single-qubit operator. Args: op (np.array): 2*2 Hermite operator loc (int): Index of qubit on which to apply operator. Returns: complex: expectation value (real for hermitian ops!). """ if not (0 <= i < self.Nqubit): raise ValueError(f"Wrong target qubit {i}. Must between 0 and {self.Nqubit-1}.") if op.shape != (2, 2): raise ValueError("op must be 2x2 matrix.") st1 = deepcopy(self) st1.normalize() rho_tensor = st1.rho.reshape((2,) * st1.Nqubit * 2) rho_tensor = np.tensordot(op, rho_tensor, axes=[1, i]) rho_tensor = np.moveaxis(rho_tensor, 0, i) st1.rho = rho_tensor.reshape((2**self.Nqubit, 2**self.Nqubit)) return np.trace(st1.rho)
def dims(self): return self.rho.shape
[docs] def tensor(self, other): r"""Tensor product state with other density matrix. Results in self :math:`\otimes` other. Parameters ---------- other : :class: `DensityMatrix` object DensityMatrix object to be tensored with self. """ if not isinstance(other, DensityMatrix): other = DensityMatrix(other) self.rho = np.kron(self.rho, other.rho) self.Nqubit += other.Nqubit
[docs] def cnot(self, edge): """Apply CNOT gate to density matrix. Parameters ---------- edge : (int, int) or [int, int] Edge to apply CNOT gate. """ self.evolve(CNOT_TENSOR.reshape(4, 4), edge)
[docs] def swap(self, edge): """swap qubits Parameters ---------- edge : (int, int) or [int, int] (control, target) qubits indices. """ self.evolve(SWAP_TENSOR.reshape(4, 4), edge)
[docs] def entangle(self, edge): """connect graph nodes Parameters ---------- edge : (int, int) or [int, int] (control, target) qubit indices. """ self.evolve(CZ_TENSOR.reshape(4, 4), edge)
[docs] def normalize(self): """normalize density matrix""" self.rho /= np.trace(self.rho)
[docs] def ptrace(self, qargs): """partial trace Parameters ---------- qargs : list of ints or int Indices of qubit to trace out. """ n = int(np.log2(self.rho.shape[0])) if isinstance(qargs, int): qargs = [qargs] assert isinstance(qargs, (list, tuple)) qargs_num = len(qargs) nqubit_after = n - qargs_num assert n > 0 assert all([qarg >= 0 and qarg < n for qarg in qargs]) rho_res = self.rho.reshape((2,) * n * 2) # ket, bra indices to trace out trace_axes = list(qargs) + [n + qarg for qarg in qargs] rho_res = np.tensordot( np.eye(2**qargs_num).reshape((2,) * qargs_num * 2), rho_res, axes=(list(range(2 * qargs_num)), trace_axes) ) self.rho = rho_res.reshape((2**nqubit_after, 2**nqubit_after)) self.Nqubit = nqubit_after
[docs] def fidelity(self, statevec): """calculate the fidelity against reference statevector. Parameters ---------- statevec : numpy array statevector (flattened numpy array) to compare with """ return np.abs(statevec.transpose().conj() @ self.rho @ statevec)
[docs] def apply_channel(self, channel: KrausChannel, qargs): """Applies a channel to a density matrix. Parameters ---------- :rho: density matrix. channel: :class:`graphix.channel.KrausChannel` object KrausChannel to be applied to the density matrix qargs: target qubit indices Returns ------- nothing Raises ------ ValueError If the final density matrix is not normalized after application of the channel. This shouldn't happen since :class:`graphix.channel.KrausChannel` objects are normalized by construction. .... """ result_array = np.zeros((2**self.Nqubit, 2**self.Nqubit), dtype=np.complex128) tmp_dm = deepcopy(self) if not isinstance(channel, KrausChannel): raise TypeError("Can't apply a channel that is not a Channel object.") for k_op in channel.kraus_ops: tmp_dm.evolve(k_op["operator"], qargs) result_array += k_op["coef"] * np.conj(k_op["coef"]) * tmp_dm.rho # reinitialize to input density matrix tmp_dm = deepcopy(self) # Performance? self.rho = deepcopy(result_array) if not np.allclose(self.rho.trace(), 1.0): raise ValueError("The output density matrix is not normalized, check the channel definition.")
[docs]class DensityMatrixBackend(graphix.sim.base_backend.Backend): """MBQC simulator with density matrix method."""
[docs] def __init__(self, pattern, max_qubit_num=12, pr_calc=True): """ Parameters ---------- pattern : :class:`graphix.pattern.Pattern` object Pattern to be simulated. max_qubit_num : int optional argument specifying the maximum number of qubits to be stored in the statevector at a time. pr_calc : bool whether or not to compute the probability distribution before choosing the measurement result. if False, measurements yield results 0/1 with 50% probabilities each. """ # check that pattern has output nodes configured # assert len(pattern.output_nodes) > 0 self.pattern = pattern self.results = deepcopy(pattern.results) self.state = None self.node_index = [] self.Nqubit = 0 self.max_qubit_num = max_qubit_num if pattern.max_space() > max_qubit_num: raise ValueError("Pattern.max_space is larger than max_qubit_num. Increase max_qubit_num and try again.") super().__init__(pr_calc)
[docs] def add_nodes(self, nodes, qubit_to_add=None): """add new qubit to the internal density matrix and asign the corresponding node number to list self.node_index. Parameters ---------- nodes : list list of node indices qubit_to_add : DensityMatrix object qubit to be added to the graph states """ if not self.state: self.state = DensityMatrix(nqubit=0) n = len(nodes) if qubit_to_add is None: dm_to_add = DensityMatrix(nqubit=n) else: assert isinstance(qubit_to_add, DensityMatrix) assert qubit_to_add.nqubit == 1 dm_to_add = qubit_to_add self.state.tensor(dm_to_add) self.node_index.extend(nodes) self.Nqubit += n
[docs] def entangle_nodes(self, edge): """Apply CZ gate to the two connected nodes. Parameters ---------- edge : tuple (int, int) a pair of node indices """ target = self.node_index.index(edge[0]) control = self.node_index.index(edge[1]) self.state.entangle((target, control))
[docs] def measure(self, cmd): """Perform measurement on the specified node and trase out the qubit. Parameters ---------- cmd : list measurement command : ['M', node, plane, angle, s_domain, t_domain] """ loc = self._perform_measure(cmd) self.state.normalize() # perform ptrace right after the measurement (destructive measurement). self.state.ptrace(loc)
[docs] def correct_byproduct(self, cmd): """Byproduct correction correct for the X or Z byproduct operators, by applying the X or Z gate. """ if np.mod(np.sum([self.results[j] for j in cmd[2]]), 2) == 1: loc = self.node_index.index(cmd[1]) if cmd[0] == "X": op = Ops.x elif cmd[0] == "Z": op = Ops.z self.state.evolve_single(op, loc)
[docs] def sort_qubits(self): """sort the qubit order in internal density matrix""" for i, ind in enumerate(self.pattern.output_nodes): if not self.node_index[i] == ind: move_from = self.node_index.index(ind) self.state.swap((i, move_from)) self.node_index[i], self.node_index[move_from] = ( self.node_index[move_from], self.node_index[i], )
[docs] def apply_clifford(self, cmd): """backend version of apply_channel Parameters ---------- qargs : list of ints. Target qubits """ loc = self.node_index.index(cmd[1]) self.state.evolve_single(CLIFFORD[cmd[2]], loc)
[docs] def apply_channel(self, channel: KrausChannel, qargs): """backend version of apply_channel Parameters ---------- qargs : list of ints. Target qubits """ indices = [self.node_index.index(i) for i in qargs] self.state.apply_channel(channel, indices)
[docs] def finalize(self): """To be run at the end of pattern simulation.""" self.sort_qubits() self.state.normalize()