"""Tools for converting common gates to unitary matrices."""
import numpy as np
from typing import Union
from .gate import Gate, GateGenerator, collect_operands
PAULI_OPERATORS = [[[1, 0], [0, 1]], [[0, 1], [1, 0]], [[0, -1j], [1j, 0]],
[[1, 0], [0, -1]]]
def rxy(phase, amount):
x = np.array(PAULI_OPERATORS[1])
y = np.array(PAULI_OPERATORS[2])
return (np.cos(amount / 2) * np.eye(2) - 1j * np.sin(amount / 2) *
(np.cos(phase) * x + np.sin(phase) * y))
LOCAL_MATRICES = {
#: Rotation around x axis of the bloch sphere.
"rx": (lambda amount: [[np.cos(amount / 2), -1j * np.sin(amount / 2)],
[-1j * np.sin(amount / 2),
np.cos(amount / 2)]]),
#: Rotation around y axis of the bloch sphere.
"ry": (lambda amount: [[np.cos(amount / 2), -np.sin(amount / 2)],
[np.sin(amount / 2), np.cos(amount / 2)]]),
#: Rotation around z axis of the bloch sphere.
"rz":
(lambda amount: [[np.exp(-1j * amount / 2), 0], [0, np.exp(1j * amount / 2)]]),
#: Arbitrary single-qubit rotation around axis in the xy plane.
"rxy": (rxy),
#: Hadamard gate.
"h": (lambda: np.array([[1, 1], [1, -1]]) / np.sqrt(2)),
#: Conditional X gate.
"cx": (lambda: [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]),
#: Conditional Z gate.
"cz": (lambda: [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]]),
#: XX entangling gate (e.g. MS gate), i.e. exp(-i / 2 * π / 2 * X⊗X).
"xx": (lambda: np.array([[1, 0, 0, -1j], [0, 1, -1j, 0], [0, -1j, 1, 0],
[-1j, 0, 0, 1]]) / np.sqrt(2)),
#: Minus-XX entangling gate, i.e. exp(i / 2 * π / 2 * X⊗X).
"mxx": (lambda: np.array([[1, 0, 0, 1j], [0, 1, 1j, 0], [0, 1j, 1, 0],
[1j, 0, 0, 1]]) / np.sqrt(2)),
#: Two-loop Z⊗Z wobble gate with spin-echo π pulse in the middle, but no surrounding
#: gates to fix up the inversion.
"zzw": (lambda: [[0, 0, 0, 1], [0, 0, -1j, 0], [0, -1j, 0, 0], [1, 0, 0, 0]]),
#: Scheduling barrier; no action on quantum state.
"barrier": (None),
}
[docs]
def local_matrix(gate: Gate) -> np.ndarray:
r"""Return the unitary matrix that describes ``gate`` on its operands only.
The resulting matrix is the same no matter which target qubit(s) the gate operates
on; the first copy of :math:`\mathbb{C}^2` corresponds to the first operand, etc.
See :meth:`single_gate_matrix` for creating the unitary that acts on the given
operands in a larger system.
"""
try:
mat_fun = LOCAL_MATRICES[gate.kind]
if mat_fun is None:
# No-op.
return None
except KeyError:
raise ValueError("Unsupported gate of kind '{}'".format(gate.kind))
return np.array(mat_fun(*gate.parameters))
def _num_qubits_from_state_dimension(dim):
return dim.bit_length() - 1
[docs]
def single_gate_matrix(gate: Gate, num_qubits: int) -> np.ndarray:
"""Return the unitary matrix that describes the action of the given gate.
:param num_qubits: The number of qubits of the target Hilbert space.
:return: A unitary matrix acting on the `2^num_qubits`-dimensional Hilbert space.
"""
u_local = local_matrix(gate)
if u_local is not None:
num_unitary_targets = len(gate.operands)
assert num_unitary_targets == _num_qubits_from_state_dimension(u_local.shape[0])
else:
# No-op (barrier, ...).
num_unitary_targets = 1
u_local = np.eye(2)
# Construct global unitary by first tensoring with enough copies of the
# identity and then permuting the bases.
u_permuted = u_local
for _ in range(num_qubits - num_unitary_targets):
u_permuted = np.kron(u_permuted, np.eye(2))
system_perm = np.array(range(num_qubits), dtype=int)
for i, o in enumerate(gate.operands):
system_perm[i] = o
system_perm[o] = i
# Shuffle around the axis indices. I wish numpy had a built-in method for this,
# but just writing down the nested form manually isn't too bad for only qubits.
# Dragging in qutip just for this seems wrong too.
u = u_permuted.reshape([2, 2] * num_qubits)
u = np.transpose(u, axes=np.concatenate((system_perm, system_perm + num_qubits)))
return np.array(u.reshape(u_permuted.shape))
[docs]
def gate_sequence_matrix(gates: GateGenerator,
num_qubits: Union[int, None] = None) -> np.ndarray:
"""Return the unitary matrix that describes the action of the given gate sequence.
:param num_qubits: The number of qubits of the target Hilbert space. If ``None``,
the number of qubits is inferred from the gate sequence (i.e., from the largest
qubit index used).
:return: A unitary matrix acting on the `2^num_qubits`-dimensional Hilbert space.
"""
if num_qubits is None:
num_qubits = max(collect_operands(gates)) + 1
u = np.eye(2**num_qubits, dtype=np.complex64)
for g in gates:
u = single_gate_matrix(g, num_qubits) @ u
return u
[docs]
def apply_gate_sequence(gates: GateGenerator, initial_state: np.ndarray) -> np.ndarray:
"""Apply a gate sequence to the given initial state and return the resulting state
vector.
"""
num_qubits = _num_qubits_from_state_dimension(initial_state.shape[0])
state = np.array(initial_state)
for g in gates:
state = single_gate_matrix(g, num_qubits) @ state
return state