# Copyright 2022 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
# 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.
"""Continuous-variable operations, states, and noise models."""
# pylint: disable=import-outside-toplevel,too-many-instance-attributes
import numpy as np
from scipy.linalg import block_diag
import scipy.sparse as sp
from thewalrus.symplectic import expand, beam_splitter
"""Invert the permutation associated with p."""
p_inverted = np.empty(p.size, p.dtype)
p_inverted[p] = np.arange(p.size)
[docs]def SCZ_mat(adj, sparse=True):
"""Return a symplectic matrix corresponding to CZ gate application.
Give the 2N by 2N symplectic matrix for CZ gate application based on the
adjacency matrix adj. Assumes quadrature-like convention:
(q1, ..., qN, p_1, ..., p_N).
adj (array): N by N binary symmetric matrix. If modes i and j are
linked by a CZ, then entry ij and ji is equal to the weight of the
edge (1 by default); otherwise 0.
sparse (bool): whether to return a sparse or dense array when adj
input is a sparse array.
np.array or sp.sparse.csr_matrix: 2N by 2N symplectic matrix.
sparse if the adjacency matrix is sparse.
# Number of modes
N = adj.shape
if isinstance(adj, np.ndarray):
identity = np.eye(N, dtype=np.int8)
zeros = np.zeros((N, N), dtype=np.int8)
block_func = np.block
identity = sp.identity(N, dtype=np.int8)
zeros = sp.csr_matrix((N, N), dtype=np.int8)
block_func = sp.bmat
# Construct symplectic
symplectic = block_func([[identity, zeros], [adj, identity]])
if not sparse and isinstance(symplectic, sp.coo_matrix):
[docs]def SCZ_apply(adj, quads, one_shot=True):
"""Apply SCZ matrix to one- or two-dimensional array quads.
If one-shot is True, use SCZ_mat to apply a symplectic CZ matrix to
a matrix or vector of quadratures. Otherwise, take advantage of the
block structure of a symplectic SCZ matrix for a more memory-
efficient matrix multiplication.
N = quads.shape // 2
if len(quads.shape) == 1:
new_quads = SCZ_mat(adj).dot(quads)
old_qs = quads[:N]
old_ps = quads[N:]
new_quads = np.empty(2 * N, quads.dtype)
new_quads[:N] = old_qs
new_quads[N:] = adj.dot(old_qs) + old_ps
if len(quads.shape) == 2:
SCZ = SCZ_mat(adj)
new_quads = SCZ.dot(SCZ.dot(quads).T).T
c1, c2, c3, c4 = quads[:N, :N], quads[:N, N:], quads[N:, :N], quads[N:, N:]
block2 = (adj.dot(c1.T)).T + c2
block3 = adj.dot(c1) + c3
block4 = c4 + adj.dot(c2) + (adj.dot(c3.T)).T + adj.dot(adj.dot(c1).T).T
new_quads = np.block([[c1, block2], [block3, block4]])
"""Return the symplectic matrix of a four-splitter.
Return the symplectic matrix of the beamsplitters connecting the four
micronodes in each macronode. `n` refers to the total number of modes
(so n >= 4). If n = 4, return the matrix in the 'all q's first' convention;
otherwise, return a large block-diagonal matrix in the 'q1p1, ..., qnpn'
n (int, optional): the total number of modes on which the beamsplitters
apply (n must be >= 4).
numpy.array: the sympletic matrix of the four-splitter.
# 50/50 beamsplitter in the 'all q's first' convention.
bs5050 = beam_splitter(np.pi / 4, 0)
bs1 = expand(bs5050, [1, 0], 4)
bs2 = expand(bs5050, [3, 2], 4)
bs3 = expand(bs5050, [2, 0], 4)
bs4 = expand(bs5050, [3, 1], 4)
bs_network = (bs4 @ bs3 @ bs2 @ bs1).astype(np.single)
if n == 4:
if n > 4:
# Permutation away from 'all q's first' convention for matrices of
# with dimension 4 and the network spanning all the macronoes.
perm_out_4 = [0, 4, 1, 5, 2, 6, 3, 7]
bs_perm = bs_network[:, perm_out_4][perm_out_4, :]
# Symplectic corresponding to the beasmplitter network spanning
# the whole lattice.
bs_full = block_diag(*[bs_perm] * (n // 4))
print("Total number of modes cannot be less than 4.")