Source code for thewalrus.fock_gradients
# Copyright 2019 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Fock representations
====================
**Module name:** :mod:`thewalrus.fock_gradients`
.. currentmodule:: thewalrus.fock_gradients
This module contains the Fock representation of the standard Gaussian gates as well as their gradients.
Summary
-------
.. autosummary::
displacement
squeezing
beamsplitter
two_mode_squeezing
mzgate
grad_displacement
grad_squeezing
grad_beamsplitter
grad_two_mode_squeezing
grad_mzgate
Code details
------------
"""
import numpy as np
from numba import jit
[docs]@jit(nopython=True)
def displacement(r, phi, cutoff, dtype=np.complex128): # pragma: no cover
r"""Calculates the matrix elements of the displacement gate using a recurrence relation.
Args:
r (float): displacement magnitude
phi (float): displacement angle
cutoff (int): Fock ladder cutoff
dtype (data type): Specifies the data type used for the calculation
Returns:
array[complex]: matrix representing the displacement operation.
"""
D = np.zeros((cutoff, cutoff), dtype=dtype)
sqrt = np.sqrt(np.arange(cutoff, dtype=dtype))
mu = np.array([r * np.exp(1j * phi), -r * np.exp(-1j * phi)])
D[0, 0] = np.exp(-0.5 * r**2)
for m in range(1, cutoff):
D[m, 0] = mu[0] / sqrt[m] * D[m - 1, 0]
for m in range(cutoff):
for n in range(1, cutoff):
D[m, n] = mu[1] / sqrt[n] * D[m, n - 1] + sqrt[m] / sqrt[n] * D[m - 1, n - 1]
return D
[docs]@jit(nopython=True)
def grad_displacement(T, r, phi): # pragma: no cover
r"""Calculates the gradients of the displacement gate with respect to the displacement magnitude and angle.
Args:
T (array[complex]): array representing the gate
r (float): displacement magnitude
phi (float): displacement angle
Returns:
tuple[array[complex], array[complex]]: The gradient of the displacement gate with respect to r and phi
"""
cutoff = T.shape[0]
dtype = T.dtype
ei = np.exp(1j * phi)
eic = np.exp(-1j * phi)
alpha = r * ei
alphac = r * eic
sqrt = np.sqrt(np.arange(cutoff, dtype=dtype))
grad_r = np.zeros((cutoff, cutoff), dtype=dtype)
grad_phi = np.zeros((cutoff, cutoff), dtype=dtype)
for m in range(cutoff):
for n in range(cutoff):
grad_r[m, n] = -r * T[m, n] + sqrt[m] * ei * T[m - 1, n] - sqrt[n] * eic * T[m, n - 1]
grad_phi[m, n] = (
sqrt[m] * 1j * alpha * T[m - 1, n] + sqrt[n] * 1j * alphac * T[m, n - 1]
)
return grad_r, grad_phi
[docs]@jit(nopython=True)
def squeezing(r, theta, cutoff, dtype=np.complex128): # pragma: no cover
r"""Calculates the matrix elements of the squeezing gate using a recurrence relation.
Args:
r (float): squeezing magnitude
theta (float): squeezing angle
cutoff (int): Fock ladder cutoff
dtype (data type): Specifies the data type used for the calculation
Returns:
array[complex]: matrix representing the squeezing gate.
"""
S = np.zeros((cutoff, cutoff), dtype=dtype)
sqrt = np.sqrt(np.arange(cutoff, dtype=dtype))
eitheta_tanhr = np.exp(1j * theta) * np.tanh(r)
sechr = 1.0 / np.cosh(r)
R = np.array(
[
[-eitheta_tanhr, sechr],
[sechr, np.conj(eitheta_tanhr)],
]
)
S[0, 0] = np.sqrt(sechr)
for m in range(2, cutoff, 2):
S[m, 0] = sqrt[m - 1] / sqrt[m] * R[0, 0] * S[m - 2, 0]
for m in range(0, cutoff):
for n in range(1, cutoff):
if (m + n) % 2 == 0:
S[m, n] = (
sqrt[n - 1] / sqrt[n] * R[1, 1] * S[m, n - 2]
+ sqrt[m] / sqrt[n] * R[0, 1] * S[m - 1, n - 1]
)
return S
[docs]@jit(nopython=True)
def grad_squeezing(T, r, phi): # pragma: no cover
r"""Calculates the gradients of the squeezing gate with respect to the squeezing magnitude and angle
Args:
T (array[complex]): array representing the gate
r (float): squeezing magnitude
phi (float): squeezing angle
Returns:
tuple[array[complex], array[complex]]: The gradient of the squeezing gate with respect to the r and phi
"""
cutoff = T.shape[0]
dtype = T.dtype
sqrt = np.sqrt(np.arange(cutoff, dtype=dtype))
grad_r = np.zeros((cutoff, cutoff), dtype=dtype)
grad_phi = np.zeros((cutoff, cutoff), dtype=dtype)
sechr = 1.0 / np.cosh(r)
tanhr = np.tanh(r)
eiphi = np.exp(1j * phi)
eiphiconj = np.exp(-1j * phi)
for m in range(cutoff):
for n in range(cutoff):
grad_r[m, n] = (
-0.5 * tanhr * T[m, n]
- sechr * tanhr * sqrt[m] * sqrt[n] * T[m - 1, n - 1]
- 0.5 * eiphi * sechr**2 * sqrt[m] * sqrt[m - 1] * T[m - 2, n]
+ 0.5 * eiphiconj * sechr**2 * sqrt[n] * sqrt[n - 1] * T[m, n - 2]
)
grad_phi[m, n] = (
-0.5j * eiphi * tanhr * sqrt[m] * sqrt[m - 1] * T[m - 2, n]
- 0.5j * eiphiconj * tanhr * sqrt[n] * sqrt[n - 1] * T[m, n - 2]
)
return grad_r, grad_phi
[docs]@jit(nopython=True)
def two_mode_squeezing(r, theta, cutoff, dtype=np.complex128): # pragma: no cover
"""Calculates the matrix elements of the two-mode squeezing gate recursively.
Args:
r (float): squeezing magnitude
theta (float): squeezing angle
cutoff (int): Fock ladder cutoff
dtype (data type): Specifies the data type used for the calculation
Returns:
array[float]: The Fock representation of the gate
"""
sqrt = np.sqrt(np.arange(cutoff, dtype=dtype))
Z = np.zeros((cutoff, cutoff, cutoff, cutoff), dtype=dtype)
sc = 1.0 / np.cosh(r)
eiptr = np.exp(-1j * theta) * np.tanh(r)
R = -np.array(
[
[0, -np.conj(eiptr), -sc, 0],
[-np.conj(eiptr), 0, 0, -sc],
[-sc, 0, 0, eiptr],
[0, -sc, eiptr, 0],
]
)
Z[0, 0, 0, 0] = sc
# rank 2
for n in range(1, cutoff):
Z[n, n, 0, 0] = R[0, 1] * Z[n - 1, n - 1, 0, 0]
# rank 3
for m in range(cutoff):
for n in range(m):
p = m - n
if 0 < p < cutoff:
Z[m, n, p, 0] = R[0, 2] * sqrt[m] / sqrt[p] * Z[m - 1, n, p - 1, 0]
# rank 4
for m in range(cutoff):
for n in range(cutoff):
for p in range(cutoff):
q = p - (m - n)
if 0 < q < cutoff:
Z[m, n, p, q] = (
R[1, 3] * sqrt[n] / sqrt[q] * Z[m, n - 1, p, q - 1]
+ R[2, 3] * sqrt[p] / sqrt[q] * Z[m, n, p - 1, q - 1]
)
return Z
[docs]@jit(nopython=True)
def grad_two_mode_squeezing(T, r, theta): # pragma: no cover
"""Calculates the gradients of the two-mode squeezing gate with respect to the squeezing magnitude and angle
Args:
T (array[complex]): array representing the gate
r (float): squeezing magnitude
theta (float): squeezing angle
Returns:
tuple[array[complex], array[complex]]: The gradient of the two-mode squeezing gate with respect to r and phi
"""
cutoff = T.shape[0]
dtype = T.dtype
sechr = 1.0 / np.cosh(r)
tanhr = np.tanh(r)
ei = np.exp(1j * theta)
eic = np.exp(-1j * theta)
sqrt = np.sqrt(np.arange(cutoff, dtype=dtype))
grad_r = np.zeros((cutoff, cutoff, cutoff, cutoff), dtype=dtype)
grad_theta = np.zeros((cutoff, cutoff, cutoff, cutoff), dtype=dtype)
grad_r[0, 0, 0, 0] = -sechr * tanhr
# rank 2
for n in range(1, cutoff):
grad_r[n, n, 0, 0] = (
-tanhr * T[n, n, 0, 0] + sqrt[n] * sqrt[n] * ei * sechr**2 * T[n - 1, n - 1, 0, 0]
)
grad_theta[n, n, 0, 0] = 1j * ei * tanhr * sqrt[n] * sqrt[n] * T[n - 1, n - 1, 0, 0]
# rank 3
for m in range(cutoff):
for n in range(m):
p = m - n
if 0 < p < cutoff:
grad_r[m, n, p, 0] = (
-tanhr * T[m, n, p, 0]
+ sqrt[m] * sqrt[n] * ei * sechr**2 * T[m - 1, n - 1, p, 0]
- tanhr * sechr * sqrt[m] * sqrt[p] * T[m - 1, n, p - 1, 0]
)
grad_theta[m, n, p, 0] = 1j * ei * tanhr * sqrt[m] * sqrt[n] * T[m - 1, n - 1, p, 0]
# rank 4
for m in range(cutoff):
for n in range(cutoff):
for p in range(cutoff):
for q in range(cutoff):
grad_r[m, n, p, q] = (
-tanhr * T[m, n, p, q]
+ sqrt[m] * sqrt[n] * ei * sechr**2 * T[m - 1, n - 1, p, q]
- tanhr * sechr * sqrt[m] * sqrt[p] * T[m - 1, n, p - 1, q]
- tanhr * sechr * sqrt[n] * sqrt[q] * T[m, n - 1, p, q - 1]
- sqrt[p] * sqrt[q] * eic * sechr**2 * T[m, n, p - 1, q - 1]
)
grad_theta[m, n, p, q] = (
1j * ei * tanhr * sqrt[m] * sqrt[n] * T[m - 1, n - 1, p, q]
+ 1j * eic * tanhr * sqrt[p] * sqrt[q] * T[m, n, p - 1, q - 1]
)
return grad_r, grad_theta
[docs]@jit(nopython=True)
def beamsplitter(theta, phi, cutoff, dtype=np.complex128): # pragma: no cover
r"""Calculates the Fock representation of the beamsplitter.
Args:
theta (float): transmissivity angle of the beamsplitter. The transmissivity is :math:`t=\cos(\theta)`
phi (float): reflection phase of the beamsplitter
cutoff (int): Fock ladder cutoff
dtype (data type): Specifies the data type used for the calculation
Returns:
array[float]: The Fock representation of the gate
"""
sqrt = np.sqrt(np.arange(cutoff, dtype=dtype))
ct = np.cos(theta)
st = np.sin(theta) * np.exp(1j * phi)
R = np.array(
[
[0, 0, ct, -np.conj(st)],
[0, 0, st, ct],
[ct, st, 0, 0],
[-np.conj(st), ct, 0, 0],
]
)
Z = np.zeros((cutoff, cutoff, cutoff, cutoff), dtype=dtype)
Z[0, 0, 0, 0] = 1.0
# rank 3
for m in range(cutoff):
for n in range(cutoff - m):
p = m + n
if 0 < p < cutoff:
Z[m, n, p, 0] = (
R[0, 2] * sqrt[m] / sqrt[p] * Z[m - 1, n, p - 1, 0]
+ R[1, 2] * sqrt[n] / sqrt[p] * Z[m, n - 1, p - 1, 0]
)
# rank 4
for m in range(cutoff):
for n in range(cutoff):
for p in range(cutoff):
q = m + n - p
if 0 < q < cutoff:
Z[m, n, p, q] = (
R[0, 3] * sqrt[m] / sqrt[q] * Z[m - 1, n, p, q - 1]
+ R[1, 3] * sqrt[n] / sqrt[q] * Z[m, n - 1, p, q - 1]
)
return Z
[docs]@jit(nopython=True)
def grad_beamsplitter(T, theta, phi): # pragma: no cover
r"""Calculates the gradients of the beamsplitter gate with respect to the transmissivity angle and reflection phase
Args:
T (array[complex]): array representing the gate
theta (float): transmissivity angle of the beamsplitter. The transmissivity is :math:`t=\cos(\theta)`
phi (float): reflection phase of the beamsplitter
Returns:
tuple[array[complex], array[complex]]: The gradient of the beamsplitter gate with respect to theta and phi
"""
cutoff = T.shape[0]
dtype = T.dtype
sqrt = np.sqrt(np.arange(cutoff, dtype=dtype))
grad_theta = np.zeros((cutoff, cutoff, cutoff, cutoff), dtype=dtype)
grad_phi = np.zeros((cutoff, cutoff, cutoff, cutoff), dtype=dtype)
ct = np.cos(theta)
st = np.sin(theta)
ei = np.exp(1j * phi)
eic = np.exp(-1j * phi)
# rank 3
for m in range(cutoff):
for n in range(cutoff - m):
p = m + n
if 0 < p < cutoff:
grad_theta[m, n, p, 0] = (
-sqrt[m] * sqrt[p] * st * T[m - 1, n, p - 1, 0]
+ sqrt[n] * sqrt[p] * ei * ct * T[m, n - 1, p - 1, 0]
)
grad_phi[m, n, p, 0] = 1j * sqrt[n] * sqrt[p] * ei * st * T[m, n - 1, p - 1, 0]
for m in range(cutoff):
for n in range(cutoff):
for p in range(cutoff):
q = m + n - p
if 0 < q < cutoff:
grad_theta[m, n, p, q] = (
-sqrt[m] * sqrt[p] * st * T[m - 1, n, p - 1, q]
- sqrt[n] * sqrt[q] * st * T[m, n - 1, p, q - 1]
+ sqrt[n] * sqrt[p] * ei * ct * T[m, n - 1, p - 1, q]
- sqrt[m] * sqrt[q] * eic * ct * T[m - 1, n, p, q - 1]
)
grad_phi[m, n, p, q] = (
1j * sqrt[n] * sqrt[p] * ei * st * T[m, n - 1, p - 1, q]
+ 1j * sqrt[m] * sqrt[q] * eic * st * T[m - 1, n, p, q - 1]
)
return grad_theta, grad_phi
[docs]@jit(nopython=True)
def mzgate(theta, phi, cutoff, dtype=np.complex128): # pragma: no cover
r"""Calculates the Fock representation of the Mach-Zehnder interferometer.
Args:
theta (float): internal phase of the Mach-Zehnder interferometer
phi (float): external phase of the Mach-Zehnder interferometer
cutoff (int): Fock ladder cutoff
dtype (data type): Specifies the data type used for the calculation
Returns:
array[float]: The Fock representation of the gate
"""
sqrt = np.sqrt(np.arange(cutoff, dtype=dtype))
v = np.exp(1j * theta)
u = np.exp(1j * phi)
R = 0.5 * np.array(
[
[0, 0, u * (v - 1), 1j * (1 + v)],
[0, 0, 1j * u * (1 + v), 1 - v],
[u * (v - 1), 1j * u * (1 + v), 0, 0],
[1j * (1 + v), 1 - v, 0, 0],
]
)
Z = np.zeros((cutoff, cutoff, cutoff, cutoff), dtype=dtype)
Z[0, 0, 0, 0] = 1.0
# rank 3
for m in range(cutoff):
for n in range(cutoff - m):
p = m + n
if 0 < p < cutoff:
Z[m, n, p, 0] = (
R[0, 2] * sqrt[m] / sqrt[p] * Z[m - 1, n, p - 1, 0]
+ R[1, 2] * sqrt[n] / sqrt[p] * Z[m, n - 1, p - 1, 0]
)
# rank 4
for m in range(cutoff):
for n in range(cutoff):
for p in range(cutoff):
q = m + n - p
if 0 < q < cutoff:
Z[m, n, p, q] = (
R[0, 3] * sqrt[m] / sqrt[q] * Z[m - 1, n, p, q - 1]
+ R[1, 3] * sqrt[n] / sqrt[q] * Z[m, n - 1, p, q - 1]
)
return Z
[docs]@jit(nopython=True)
def grad_mzgate(T, theta, phi): # pragma: no cover
r"""Calculates the gradients of the Mach-Zehnder interferometer with respect to the transmissivity angle and reflection phase
Args:
T (array[complex]): array representing the gate
theta (float): internal of the mzgate
phi (float): external phase of the mzgate
Returns:
tuple[array[complex], array[complex]]: The gradient of the mzgate gate with respect to theta and phi
"""
cutoff = T.shape[0]
dtype = T.dtype
sqrt = np.sqrt(np.arange(cutoff, dtype=dtype))
grad_theta = np.zeros((cutoff, cutoff, cutoff, cutoff), dtype=dtype)
grad_phi = np.zeros((cutoff, cutoff, cutoff, cutoff), dtype=dtype)
v = np.exp(1j * theta)
u = np.exp(1j * phi)
# rank 3
for m in range(cutoff):
for n in range(cutoff - m):
p = m + n
if 0 < p < cutoff:
grad_theta[m, n, p, 0] = (
1j * 0.5 * u * v * sqrt[m] * sqrt[p] * T[m - 1, n, p - 1, 0]
- 0.5 * u * v * sqrt[n] * sqrt[p] * T[m, n - 1, p - 1, 0]
)
grad_phi[m, n, p, 0] = (1j * 0.5 * u * v - 1j * 0.5 * u) * sqrt[m] * sqrt[p] * T[
m - 1, n, p - 1, 0
] - (0.5 * u + 0.5 * u * v) * sqrt[n] * sqrt[p] * T[m, n - 1, p - 1, 0]
for m in range(cutoff):
for n in range(cutoff):
for p in range(cutoff):
q = m + n - p
if 0 < q < cutoff:
grad_theta[m, n, p, q] = (
1j * 0.5 * u * v * sqrt[m] * sqrt[p] * T[m - 1, n, p - 1, q]
- 0.5 * u * v * sqrt[n] * sqrt[p] * T[m, n - 1, p - 1, q]
- 0.5 * v * sqrt[m] * sqrt[q] * T[m - 1, n, p, q - 1]
- 1j * 0.5 * v * sqrt[n] * sqrt[q] * T[m, n - 1, p, q - 1]
)
grad_phi[m, n, p, q] = (1j * 0.5 * u * v - 1j * 0.5 * u) * sqrt[m] * sqrt[
p
] * T[m - 1, n, p - 1, q] - (0.5 * u + 0.5 * u * v) * sqrt[n] * sqrt[p] * T[
m, n - 1, p - 1, q
]
return grad_theta, grad_phi
_modules/thewalrus/fock_gradients
Download Python script
Download Notebook
View on GitHub