def test_symplectic(self, tol): """Test that the interferometer is symplectic""" # random interferometer # fmt:off U = np.array([[ -0.06658906 - 0.36413058j, 0.07229868 + 0.65935896j, 0.59094625 - 0.17369183j, -0.18254686 - 0.10140904j ], [ 0.53854866 + 0.36529723j, 0.61152793 + 0.15022026j, 0.05073631 + 0.32624882j, -0.17482023 - 0.20103772j ], [ 0.34818923 + 0.51864844j, -0.24334624 + 0.0233729j, 0.3625974 - 0.4034224j, 0.10989667 + 0.49366039j ], [ 0.16548085 + 0.14792642j, -0.3012549 - 0.11387682j, -0.12731847 - 0.44851389j, -0.55816075 - 0.5639976j ]]) # fmt on U = symplectic.interferometer(U) # the symplectic matrix O = np.block([[np.zeros([4, 4]), np.identity(4)], [-np.identity(4), np.zeros([4, 4])]]) assert np.allclose(U @ O @ U.T, O, atol=tol, rtol=0)
def random_cov(num_modes=200, pure=False, nonclassical=True): r"""Creates a random covariance matrix for testing. Args: num_modes (int): number of modes pure (bool): Gaussian state is pure nonclassical (bool): Gaussian state is nonclassical Returns: (array): a covariance matrix """ M = num_modes O = interferometer(random_interferometer(M)) eta = 1 if pure else 0.123 r = 1.234 if nonclassical: # squeezed inputs cov_in = np.diag(np.concatenate([np.exp(2 * r) * np.ones(M), np.exp(-2 * r) * np.ones(M)])) elif not nonclassical and pure: # vacuum inputs cov_in = np.eye(2 * M) elif not nonclassical and not pure: # squashed inputs cov_in = np.diag(np.concatenate([np.exp(2 * r) * np.ones(M), np.ones(M)])) cov_out = O @ cov_in @ O.T _, cov = passive_transformation( np.zeros([len(cov_out)]), cov_out, np.sqrt(eta) * np.identity(len(cov_out) // 2) ) return cov
def test_beamsplitter(self, tol): """Test that an interferometer returns correct symplectic for an arbitrary beamsplitter""" theta = 0.98 phi = 0.41 U = symplectic.beam_splitter(theta, phi) S = symplectic.interferometer(U) expected = np.block([[U.real, -U.imag], [U.imag, U.real]]) np.allclose(S, expected, atol=tol, rtol=0)
def test_50_50_beamsplitter(self, tol): """Test that an interferometer returns correct symplectic for a 50-50 beamsplitter""" U = np.array([[1, -1], [1, 1]]) / np.sqrt(2) S = symplectic.interferometer(U) B = np.array([[1, -1, 0, 0], [1, 1, 0, 0], [0, 0, 1, -1], [0, 0, 1, 1] ]) / np.sqrt(2) assert np.allclose(S, B, atol=tol, rtol=0)
def apply_u(self, U): r"""Transforms the state according to the linear optical unitary that maps a[i] \to U[i, j]^*a[j]. Args: U (array): linear opical unitary matrix """ Us = symp.interferometer(U) self.means = update_means(self.means, Us, self.from_xp) self.covs = update_covs(self.covs, Us, self.from_xp)
def test_all_passive_gates(hbar, tol): """test that all gates run and do not cause anything to crash""" eng = sf.LocalEngine(backend="gaussian") circuit = sf.Program(4) with circuit.context as q: for i in range(4): ops.Sgate(1, 0.3) | q[i] ops.Rgate(np.pi) | q[0] ops.PassiveChannel(np.ones((2, 2))) | (q[1], q[2]) ops.LossChannel(0.9) | q[1] ops.MZgate(0.25 * np.pi, 0) | (q[2], q[3]) ops.PassiveChannel(np.array([[0.83]])) | q[0] ops.sMZgate(0.11, -2.1) | (q[0], q[3]) ops.Interferometer(np.array([[np.exp(1j * 2)]])) | q[1] ops.BSgate(0.8, 0.4) | (q[1], q[3]) ops.Interferometer(0.5**0.5 * np.fft.fft(np.eye(2))) | (q[0], q[2]) ops.PassiveChannel(0.1 * np.ones((3, 3))) | (q[3], q[1], q[0]) cov = eng.run(circuit).state.cov() circuit = sf.Program(4) with circuit.context as q: ops.Rgate(np.pi) | q[0] ops.PassiveChannel(np.ones((2, 2))) | (q[1], q[2]) ops.LossChannel(0.9) | q[1] ops.MZgate(0.25 * np.pi, 0) | (q[2], q[3]) ops.PassiveChannel(np.array([[0.83]])) | q[0] ops.sMZgate(0.11, -2.1) | (q[0], q[3]) ops.Interferometer(np.array([[np.exp(1j * 2)]])) | q[1] ops.BSgate(0.8, 0.4) | (q[1], q[3]) ops.Interferometer(0.5**0.5 * np.fft.fft(np.eye(2))) | (q[0], q[2]) ops.PassiveChannel(0.1 * np.ones((3, 3))) | (q[3], q[1], q[0]) compiled_circuit = circuit.compile(compiler="passive") T = compiled_circuit.circuit[0].op.p[0] S_sq = np.eye(8, dtype=np.complex128) r = 1 phi = 0.3 for i in range(4): S_sq[i, i] = np.cosh(r) - np.sinh(r) * np.cos(phi) S_sq[i, i + 4] = -np.sinh(r) * np.sin(phi) S_sq[i + 4, i] = -np.sinh(r) * np.sin(phi) S_sq[i + 4, i + 4] = np.cosh(r) + np.sinh(r) * np.cos(phi) cov_sq = (hbar / 2) * S_sq @ S_sq.T mu = np.zeros(8) P = interferometer(T) L = (hbar / 2) * (np.eye(P.shape[0]) - P @ P.T) cov2 = P @ cov_sq @ P.T + L assert np.allclose(cov, cov2, atol=tol, rtol=0)
def test_interferometer(self, tol): """Test that an interferometer returns correct symplectic""" # fmt:off U = np.array([[0.83645892 - 0.40533293j, -0.20215326 + 0.30850569j], [-0.23889780 - 0.28101519j, -0.88031770 - 0.29832709j]]) # fmt:on S = symplectic.interferometer(U) expected = np.block([[U.real, -U.imag], [U.imag, U.real]]) assert np.allclose(S, expected, atol=tol, rtol=0)
def reference_covariance(system): squeezing = numpy.concatenate( [system.squeezing, numpy.zeros(system.modes - system.inputs)]) cov = numpy.diag( numpy.concatenate([ numpy.exp(2 * squeezing), numpy.exp(-2 * squeezing) ])) # covariance matrix before interferometer interferom = interferometer(system.unitary) cov = interferom @ cov @ interferom.T # covariance matrix after interferometer return cov
def test_one_mode_gate_expand(M, tol): """test _apply_symp_one_mode_gate applies correctly on a larger matrices""" S = np.random.random((2 * M, 2 * M)) r = np.random.random(2 * M) S_G = interferometer(np.exp(1j * 0.3)) S1, r1 = _apply_symp_one_mode_gate(S_G, S.copy(), r.copy(), 1) S_G_expand = expand(S_G, [1], M) S2 = S_G_expand @ S r2 = S_G_expand @ r assert np.allclose(S1, S2, atol=tol, rtol=0) assert np.allclose(r1, r2, atol=tol, rtol=0)
def test_two_mode_gate_expand(M, tol): """test _apply_symp_two_mode_gate applies correctly""" S = np.random.random((2 * M, 2 * M)) r = np.random.random(2 * M) S_G = interferometer(0.5**0.5 * np.fft.fft(np.eye(2))) S1, r1 = _apply_symp_two_mode_gate(S_G, S.copy(), r.copy(), 1, 3) S_G_expand = expand(S_G, [1, 3], M) S2 = S_G_expand @ S r2 = S_G_expand @ r assert np.allclose(S1, S2, atol=tol, rtol=0) assert np.allclose(r1, r2, atol=tol, rtol=0)
def test_four_modes(hbar): """Test that probabilities are correctly updates for a four modes system under loss""" # All this block is to generate the correct covariance matrix. # It correnponds to num_modes=4 modes that undergo two mode squeezing between modes i and i + (num_modes / 2). # Then they undergo displacement. # The signal and idlers see and interferometer with unitary matrix u2x2. # And then they see loss by amount etas[i]. num_modes = 4 theta = 0.45 phi = 0.7 u2x2 = np.array([ [np.cos(theta / 2), np.exp(1j * phi) * np.sin(theta / 2)], [-np.exp(-1j * phi) * np.sin(theta / 2), np.cos(theta / 2)], ]) u4x4 = block_diag(u2x2, u2x2) cov = np.identity(2 * num_modes) * hbar / 2 means = 0.5 * np.random.rand(2 * num_modes) * np.sqrt(hbar / 2) rs = [0.1, 0.9] n_half = num_modes // 2 for i, r_val in enumerate(rs): Sexpanded = expand(two_mode_squeezing(r_val, 0.0), [i, n_half + i], num_modes) cov = Sexpanded @ cov @ (Sexpanded.T) Su = expand(interferometer(u4x4), range(num_modes), num_modes) cov = Su @ cov @ (Su.T) cov_lossless = np.copy(cov) means_lossless = np.copy(means) etas = [0.9, 0.7, 0.9, 0.1] for i, eta in enumerate(etas): means, cov = loss(means, cov, eta, i, hbar=hbar) cutoff = 3 probs_lossless = probabilities(means_lossless, cov_lossless, 4 * cutoff, hbar=hbar) probs = probabilities(means, cov, cutoff, hbar=hbar) probs_updated = update_probabilities_with_loss(etas, probs_lossless) assert np.allclose(probs, probs_updated[:cutoff, :cutoff, :cutoff, :cutoff], atol=1e-6)
def test_interferometer_selection_rules(choi_r, nmodes, tol): r"""Test the selection rules of an interferometer. If one writes the interferometer gate of k modes as :math:`U` and its matrix elements as :math:`\langle p_0 p_1 \ldots p_{k-1} |U|q_0 q_1 \ldots q_{k-1}\rangle` then these elements are nonzero if and only if :math:`\sum_{i=0}^k p_i = \sum_{i=0}^k q_i`. This test checks that this selection rule holds. """ U = random_interferometer(nmodes) S = interferometer(U) alphas = np.zeros([nmodes]) cutoff = 4 T = fock_tensor(S, alphas, cutoff, choi_r=choi_r) for p in product(list(range(cutoff)), repeat=nmodes): for q in product(list(range(cutoff)), repeat=nmodes): if sum(p) != sum(q): # Check that there are the same total number of photons in the bra and the ket r = tuple(list(p) + list(q)) assert np.allclose(T[r], 0.0, atol=tol, rtol=0)
def test_unitary(self, M, tol): """ test that the outputs agree with the interferometer class when transformation is unitary """ a = np.arange(4 * M**2, dtype=np.float64).reshape((2 * M, 2 * M)) cov = a @ a.T + np.eye(2 * M) mu = np.arange(2 * M, dtype=np.float64) U = M**(-0.5) * np.fft.fft(np.eye(M)) S_U = symplectic.interferometer(U) cov_U = S_U @ cov @ S_U.T mu_U = S_U @ mu mu_T, cov_T = symplectic.passive_transformation(mu, cov, U) assert np.allclose(mu_U, mu_T, atol=tol, rtol=0) assert np.allclose(cov_U, cov_T, atol=tol, rtol=0)
def test_interferometer_single_excitation(choi_r, nmodes, tol): r"""Test that the representation of an interferometer in the single excitation manifold is precisely the unitary matrix that represents it mode in space. Let :math:`V` be a unitary matrix in N modes and let :math:`U` be its Fock representation Also let :math:`|i \rangle = |0_0,\ldots, 1_i, 0_{N-1} \rangle`, i.e a single photon in mode :math:`i`. Then it must hold that :math:`V_{i,j} = \langle i | U | j \rangle`. """ U = random_interferometer(nmodes) S = interferometer(U) alphas = np.zeros([nmodes]) cutoff = 2 T = fock_tensor(S, alphas, cutoff, choi_r=choi_r) # Construct a list with all the indices corresponding to |i \rangle vec_list = np.identity(nmodes, dtype=int).tolist() # Calculate the matrix \langle i | U | j \rangle = T[i+j] U_rec = np.empty([nmodes, nmodes], dtype=complex) for i, vec_i in enumerate(vec_list): for j, vec_j in enumerate(vec_list): U_rec[i, j] = T[tuple(vec_i + vec_j)] assert np.allclose(U_rec, U, atol=tol, rtol=0)
def compile(self, seq, registers): """Try to arrange a quantum circuit into the canonical Symplectic form. This method checks whether the circuit can be implemented as a sequence of Gaussian operations. If the answer is yes it arranges them in the canonical order with displacement at the end. Args: seq (Sequence[Command]): quantum circuit to modify registers (Sequence[RegRefs]): quantum registers Returns: List[Command]: modified circuit Raises: CircuitError: the circuit does not correspond to a Gaussian unitary """ # Check which modes are actually being used used_modes = [] for operations in seq: modes = [modes_label.ind for modes_label in operations.reg] used_modes.append(modes) # pylint: disable=consider-using-set-comprehension used_modes = list( set([item for sublist in used_modes for item in sublist])) # dictionary mapping the used modes to consecutive non-negative integers dict_indices = {used_modes[i]: i for i in range(len(used_modes))} nmodes = len(used_modes) # This is the identity transformation in phase-space, multiply by the identity and add zero Snet = np.identity(2 * nmodes) rnet = np.zeros(2 * nmodes) # Now we will go through each operation in the sequence `seq` and apply it in quadrature space # We will keep track of the net transforation in the Symplectic matrix `Snet` and the quadrature # vector `rnet`. for operations in seq: name = operations.op.__class__.__name__ params = par_evaluate(operations.op.p) modes = [modes_label.ind for modes_label in operations.reg] if name == "Dgate": rnet = rnet + expand_vector( params[0] * (np.exp(1j * params[1])), dict_indices[modes[0]], nmodes) else: if name == "Rgate": S = expand(rotation(params[0]), dict_indices[modes[0]], nmodes) elif name == "Sgate": S = expand(squeezing(params[0], params[1]), dict_indices[modes[0]], nmodes) elif name == "S2gate": S = expand( two_mode_squeezing(params[0], params[1]), [dict_indices[modes[0]], dict_indices[modes[1]]], nmodes, ) elif name == "Interferometer": S = expand(interferometer(params[0]), [dict_indices[mode] for mode in modes], nmodes) elif name == "GaussianTransform": S = expand(params[0], [dict_indices[mode] for mode in modes], nmodes) elif name == "BSgate": S = expand( beam_splitter(params[0], params[1]), [dict_indices[modes[0]], dict_indices[modes[1]]], nmodes, ) elif name == "MZgate": v = np.exp(1j * params[0]) u = np.exp(1j * params[1]) U = 0.5 * np.array([[u * (v - 1), 1j * (1 + v)], [1j * u * (1 + v), 1 - v]]) S = expand( interferometer(U), [dict_indices[modes[0]], dict_indices[modes[1]]], nmodes, ) Snet = S @ Snet rnet = S @ rnet # Having obtained the net displacement we simply convert it into complex notation alphas = 0.5 * (rnet[0:nmodes] + 1j * rnet[nmodes:2 * nmodes]) # And now we just pass the net transformation as a big Symplectic operation plus displacements ord_reg = [r for r in list(registers) if r.ind in used_modes] ord_reg = sorted(list(ord_reg), key=lambda x: x.ind) if np.allclose(Snet, np.identity(2 * nmodes)): A = [] else: A = [Command(ops.GaussianTransform(Snet), ord_reg)] B = [ Command(ops.Dgate(np.abs(alphas[i]), np.angle(alphas[i])), ord_reg[i]) for i in range(len(ord_reg)) if not np.allclose(alphas[i], 0.0) ] return A + B
def test_inverse_ops_cancel(self, hbar, tol): """Test that applying squeezing and interferometers to a four mode circuit, followed by applying the inverse operations, return the state to the vacuum""" # the symplectic matrix O = np.block([[np.zeros([4, 4]), np.identity(4)], [-np.identity(4), np.zeros([4, 4])]]) # begin in the vacuum state mu_init, cov_init = symplectic.vacuum_state(4, hbar=hbar) # add displacement alpha = np.random.random(size=[4]) + np.random.random(size=[4]) * 1j D = np.concatenate([alpha.real, alpha.imag]) mu = mu_init + D cov = cov_init.copy() # random squeezing r = np.random.random() phi = np.random.random() S = symplectic.expand(symplectic.two_mode_squeezing(r, phi), modes=[0, 1], N=4) # check symplectic assert np.allclose(S @ O @ S.T, O, atol=tol, rtol=0) # random interferometer # fmt:off u = np.array([[ -0.06658906 - 0.36413058j, 0.07229868 + 0.65935896j, 0.59094625 - 0.17369183j, -0.18254686 - 0.10140904j ], [ 0.53854866 + 0.36529723j, 0.61152793 + 0.15022026j, 0.05073631 + 0.32624882j, -0.17482023 - 0.20103772j ], [ 0.34818923 + 0.51864844j, -0.24334624 + 0.0233729j, 0.3625974 - 0.4034224j, 0.10989667 + 0.49366039j ], [ 0.16548085 + 0.14792642j, -0.3012549 - 0.11387682j, -0.12731847 - 0.44851389j, -0.55816075 - 0.5639976j ]]) # fmt on U = symplectic.interferometer(u) # check unitary assert np.allclose(u @ u.conj().T, np.identity(4), atol=tol, rtol=0) # check symplectic assert np.allclose(U @ O @ U.T, O, atol=tol, rtol=0) # apply squeezing and interferometer cov = U @ S @ cov @ S.T @ U.T mu = U @ S @ mu # check we are no longer in the vacuum state assert not np.allclose(mu, mu_init, atol=tol, rtol=0) assert not np.allclose(cov, cov_init, atol=tol, rtol=0) # return the inverse operations Sinv = symplectic.expand(symplectic.two_mode_squeezing(-r, phi), modes=[0, 1], N=4) Uinv = symplectic.interferometer(u.conj().T) # check inverses assert np.allclose(Uinv, np.linalg.inv(U), atol=tol, rtol=0) assert np.allclose(Sinv, np.linalg.inv(S), atol=tol, rtol=0) # apply the inverse operations cov = Sinv @ Uinv @ cov @ Uinv.T @ Sinv.T mu = Sinv @ Uinv @ mu # inverse displacement mu -= D # check that we return to the vacuum state assert np.allclose(mu, mu_init, atol=tol, rtol=0) assert np.allclose(cov, cov_init, atol=tol, rtol=0)
def test_cumulants_three_mode_random_state(hbar): # pylint: disable=too-many-statements """Tests third order cumulants for a random state""" M = 3 O = interferometer(random_interferometer(3)) mu = np.random.rand(2 * M) - 0.5 hbar = 2 cov = 0.5 * hbar * O @ squeezing(np.random.rand(M)) @ O.T cutoff = 50 probs = probabilities(mu, cov, cutoff, hbar=hbar) n = np.arange(cutoff) probs0 = np.sum(probs, axis=(1, 2)) probs1 = np.sum(probs, axis=(0, 2)) probs2 = np.sum(probs, axis=(0, 1)) # Check one body cumulants n0_1 = n @ probs0 n1_1 = n @ probs1 n2_1 = n @ probs2 assert np.allclose(photon_number_cumulant(mu, cov, [0], hbar=hbar), n0_1) assert np.allclose(photon_number_cumulant(mu, cov, [1], hbar=hbar), n1_1) assert np.allclose(photon_number_cumulant(mu, cov, [2], hbar=hbar), n2_1) n0_2 = n**2 @ probs0 n1_2 = n**2 @ probs1 n2_2 = n**2 @ probs2 var0 = n0_2 - n0_1**2 var1 = n1_2 - n1_1**2 var2 = n2_2 - n2_1**2 assert np.allclose(photon_number_cumulant(mu, cov, [0, 0], hbar=hbar), var0) assert np.allclose(photon_number_cumulant(mu, cov, [1, 1], hbar=hbar), var1) assert np.allclose(photon_number_cumulant(mu, cov, [2, 2], hbar=hbar), var2) n0_3 = n**3 @ probs0 - 3 * n0_2 * n0_1 + 2 * n0_1**3 n1_3 = n**3 @ probs1 - 3 * n1_2 * n1_1 + 2 * n1_1**3 n2_3 = n**3 @ probs2 - 3 * n2_2 * n2_1 + 2 * n2_1**3 assert np.allclose(photon_number_cumulant(mu, cov, [0, 0, 0], hbar=hbar), n0_3) assert np.allclose(photon_number_cumulant(mu, cov, [1, 1, 1], hbar=hbar), n1_3) assert np.allclose(photon_number_cumulant(mu, cov, [2, 2, 2], hbar=hbar), n2_3) # Check two body cumulants probs01 = np.sum(probs, axis=(2)) probs02 = np.sum(probs, axis=(1)) probs12 = np.sum(probs, axis=(0)) n0n1 = n @ probs01 @ n n0n2 = n @ probs02 @ n n1n2 = n @ probs12 @ n covar01 = n0n1 - n0_1 * n1_1 covar02 = n0n2 - n0_1 * n2_1 covar12 = n1n2 - n1_1 * n2_1 assert np.allclose(photon_number_cumulant(mu, cov, [0, 1], hbar=hbar), covar01) assert np.allclose(photon_number_cumulant(mu, cov, [0, 2], hbar=hbar), covar02) assert np.allclose(photon_number_cumulant(mu, cov, [1, 2], hbar=hbar), covar12) kappa001 = n**2 @ probs01 @ n - 2 * n0n1 * n0_1 - n0_2 * n1_1 + 2 * n0_1**2 * n1_1 kappa011 = n @ probs01 @ n**2 - 2 * n0n1 * n1_1 - n1_2 * n0_1 + 2 * n1_1**2 * n0_1 kappa002 = n**2 @ probs02 @ n - 2 * n0n2 * n0_1 - n0_2 * n2_1 + 2 * n0_1**2 * n2_1 kappa022 = n @ probs02 @ n**2 - 2 * n0n2 * n2_1 - n2_2 * n0_1 + 2 * n2_1**2 * n0_1 kappa112 = n**2 @ probs12 @ n - 2 * n1n2 * n1_1 - n1_2 * n2_1 + 2 * n1_1**2 * n2_1 kappa122 = n @ probs12 @ n**2 - 2 * n1n2 * n2_1 - n2_2 * n1_1 + 2 * n2_1**2 * n1_1 assert np.allclose(photon_number_cumulant(mu, cov, [0, 0, 1], hbar=hbar), kappa001) assert np.allclose(photon_number_cumulant(mu, cov, [0, 1, 1], hbar=hbar), kappa011) assert np.allclose(photon_number_cumulant(mu, cov, [0, 0, 2], hbar=hbar), kappa002) assert np.allclose(photon_number_cumulant(mu, cov, [0, 2, 2], hbar=hbar), kappa022) assert np.allclose(photon_number_cumulant(mu, cov, [1, 1, 2], hbar=hbar), kappa112) assert np.allclose(photon_number_cumulant(mu, cov, [1, 2, 2], hbar=hbar), kappa122) # Finally, the three body cumulant n0n1n2 = np.einsum("ijk, i, j, k", probs, n, n, n) kappa012 = n0n1n2 - n0n1 * n2_1 - n0n2 * n1_1 - n1n2 * n0_1 + 2 * n0_1 * n1_1 * n2_1 assert np.allclose(photon_number_cumulant(mu, cov, [0, 1, 2], hbar=hbar), kappa012)