import numpy as np
from graphix.ops import Ops
from graphix.clifford import CLIFFORD_CONJ, CLIFFORD, CLIFFORD_MUL
from copy import deepcopy
from scipy.linalg import norm
[docs]class StatevectorBackend:
"""MBQC simulator with statevector method."""
[docs] def __init__(self, pattern, max_qubit_num=20):
"""
Parameteres
-----------
pattern: :class:`graphix.pattern.Pattern` object
MBQC pattern to be simulated.
backend: str, 'statevector'
optional argument for simulation.
max_qubit_num : int
optional argument specifying the maximum number of qubits
to be stored in the statevector at a time.
"""
# 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.to_trace = []
self.to_trace_loc = []
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")
[docs] def qubit_dim(self):
"""Returns the qubit number in the internal statevector
Returns
-------
n_qubit : int
"""
return len(self.state.dims())
[docs] def add_nodes(self, nodes):
"""add new qubit to internal statevector
and assign the corresponding node number
to list self.node_index.
Parameters
----------
nodes : list of node indices
"""
if not self.state:
self.state = Statevec(nqubit=0)
n = len(nodes)
sv_to_add = Statevec(nqubit=n)
self.state.tensor(sv_to_add)
self.node_index.extend(nodes)
self.Nqubit += 1
if self.Nqubit == self.max_qubit_num:
self.trace_out()
[docs] def entangle_nodes(self, edge):
"""Apply CZ gate to two connected nodes
Parameters
----------
edge : tuple (i, j)
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 of a node in the internal statevector and trace out the qubit
Parameters
----------
cmd : list
measurement command : ['M', node, plane angle, s_domain, t_domain]
"""
# choose the measurement result randomly
result = np.random.choice([0, 1])
self.results[cmd[1]] = result
# extract signals for adaptive angle
s_signal = np.sum([self.results[j] for j in cmd[4]])
t_signal = np.sum([self.results[j] for j in cmd[5]])
angle = cmd[3] * np.pi
if len(cmd) == 7:
vop = cmd[6]
else:
vop = 0
if int(s_signal % 2) == 1:
vop = CLIFFORD_MUL[1, vop]
if int(t_signal % 2) == 1:
vop = CLIFFORD_MUL[3, vop]
m_op = meas_op(angle, vop=vop, plane=cmd[2], choice=result)
loc = self.node_index.index(cmd[1])
self.state.evolve_single(m_op, loc)
self.to_trace.append(cmd[1])
self.to_trace_loc.append(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 apply_clifford(self, cmd):
"""Apply single-qubit Clifford gate,
specified by vop index specified in graphix.clifford.CLIFFORD
"""
loc = self.node_index.index(cmd[1])
self.state.evolve_single(CLIFFORD[cmd[2]], loc)
[docs] def finalize(self):
"""to be run at the end of pattern simulation."""
self.trace_out()
self.sort_qubits()
self.state.normalize()
[docs] def trace_out(self):
"""trace out the qubits buffered in self.to_trace from self.state"""
self.state.normalize()
self.state.ptrace(self.to_trace_loc)
for node in self.to_trace:
self.node_index.remove(node)
self.Nqubit -= len(self.to_trace)
self.to_trace = []
self.to_trace_loc = []
[docs] def sort_qubits(self):
"""sort the qubit order in internal statevector"""
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 meas_op(angle, vop=0, plane="XY", choice=0):
"""Returns the projection operator for given measurement angle and local Clifford op (VOP).
.. seealso:: :mod:`graphix.clifford`
Parameters
----------
angle: float
original measurement angle in radian
vop : int
index of local Clifford (vop), see graphq.clifford.CLIFFORD
plane : 'XY', 'YZ' or 'ZX'
measurement plane on which angle shall be defined
choice : 0 or 1
choice of measurement outcome. measured eigenvalue would be (-1)**choice.
Returns
-------
op : numpy array
projection operator
"""
assert vop in np.arange(24)
assert choice in [0, 1]
assert plane in ["XY", "YZ", "XZ"]
if plane == "XY":
vec = (np.cos(angle), np.sin(angle), 0)
elif plane == "YZ":
vec = (0, np.cos(angle), np.sin(angle))
elif plane == "XZ":
vec = (np.cos(angle), 0, np.sin(angle))
op_mat = np.eye(2, dtype=np.complex128) / 2
for i in range(3):
op_mat += (-1) ** (choice) * vec[i] * CLIFFORD[i + 1] / 2
op_mat = CLIFFORD[CLIFFORD_CONJ[vop]] @ op_mat @ CLIFFORD[vop]
return op_mat
CZ_TENSOR = np.array(
[[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, -1]]]],
dtype=np.complex128,
)
CNOT_TENSOR = np.array(
[[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [0, 1]], [[0, 0], [1, 0]]]],
dtype=np.complex128,
)
SWAP_TENSOR = np.array(
[[[[1, 0], [0, 0]], [[0, 0], [1, 0]]], [[[0, 1], [0, 0]], [[0, 0], [0, 1]]]],
dtype=np.complex128,
)
[docs]class Statevec:
"""Simple statevector simulator"""
[docs] def __init__(self, plus_states=True, nqubit=1):
"""Initialize statevector
Args:
plus_states (bool, optional): whether or not to start all qubits in + state or 0 state. Defaults to +
nqubit (int, optional): number of qubits. Defaults to 1.
"""
if plus_states:
self.psi = np.ones((2,) * nqubit) / np.sqrt(2**nqubit)
else:
self.psi = np.zeros((2,) * nqubit)
self.psi[(0,) * nqubit] = 1
def __repr__(self):
return f"Statevec, data={self.psi}, shape={self.dims()}"
[docs] def evolve_single(self, op, i):
"""Single-qubit operation
Args:
op (np.array): 2*2 matrix
i (int): qubit index
"""
self.psi = np.tensordot(op, self.psi, (1, i))
self.psi = np.moveaxis(self.psi, 0, i)
[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
"""
op_dim = int(np.log2(len(op)))
shape = [2 for _ in range(2 * op_dim)]
op_tensor = op.reshape(shape)
self.psi = np.tensordot(
op_tensor,
self.psi,
(tuple(op_dim + i for i in range(len(qargs))), tuple(qargs)),
)
self.psi = np.moveaxis(self.psi, [i for i in range(len(qargs))], qargs)
def dims(self):
return self.psi.shape
[docs] def ptrace(self, qargs):
"""partial trace
Args:
qargs (list of ints): qubit inices to trace over
"""
nqubit_after = len(self.psi.shape) - len(qargs)
psi = self.psi
rho = np.tensordot(psi, psi.conj(), axes=(qargs, qargs)) # density matrix
rho = np.reshape(rho, (2**nqubit_after, 2**nqubit_after))
evals, evecs = np.linalg.eig(rho) # back to statevector
self.psi = np.reshape(evecs[:, np.argmax(evals)], (2,) * nqubit_after)
[docs] def entangle(self, edge):
"""connect graph nodes
Args:
edge (tuple of ints): (control, target) qubit indices
"""
# contraction: 2nd index - control index, and 3rd index - target index.
self.psi = np.tensordot(CZ_TENSOR, self.psi, ((2, 3), edge))
# sort back axes
self.psi = np.moveaxis(self.psi, (0, 1), edge)
[docs] def tensor(self, other):
r"""Tensor product state with other qubits.
Results in self :math:`\otimes` other.
Args:
other (_type_): graphix.sim.statevec.Statevec instance
"""
psi_self = self.psi.flatten()
psi_other = other.psi.flatten()
total_num = len(self.dims()) + len(other.dims())
self.psi = np.kron(psi_self, psi_other).reshape((2,) * total_num)
[docs] def CNOT(self, qubits):
"""apply CNOT
Args:
qubits (tuple of ints): (control, target) qubit indices
"""
# contraction: 2nd index - control index, and 3rd index - target index.
self.psi = np.tensordot(CNOT_TENSOR, self.psi, ((2, 3), qubits))
# sort back axes
self.psi = np.moveaxis(self.psi, (0, 1), qubits)
[docs] def swap(self, qubits):
"""swap qubits
Args:
qubits (tuple of ints): (control, target) qubit indices
"""
# contraction: 2nd index - control index, and 3rd index - target index.
self.psi = np.tensordot(SWAP_TENSOR, self.psi, ((2, 3), qubits))
# sort back axes
self.psi = np.moveaxis(self.psi, (0, 1), qubits)
[docs] def normalize(self):
"""normalize the state"""
self.psi = self.psi / norm(self.psi)
[docs] def flatten(self):
"""returns flattened statevector"""
return self.psi.flatten()
[docs] def expectation_single(self, op, loc):
"""Expectation value of single-qubit operator.
Args:
op (np.array): 2*2 Hermite operator
loc (int): target qubit
Returns:
complex: expectation value.
"""
st1 = deepcopy(self)
st1.normalize()
st2 = st1.deepcopy(st1)
st1.evolve_single(op, loc)
return np.dot(st2.psi.flatten().conjugate(), st1.psi.flatten())
[docs] def expectation_value(self, op, qargs):
"""Expectation value of multi-qubit operator.
Args:
op (np.array): 2^n*2^n operator
qargs (list of ints): target qubit
Returns:
complex: expectation value
"""
st1 = deepcopy(self)
st1.normalize()
st2 = deepcopy(st1)
st1.evolve(op, qargs)
return np.dot(st2.psi.flatten().conjugate(), st1.psi.flatten())