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)
示例#2
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)
示例#5
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)
示例#6
0
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)
示例#8
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)
示例#11
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)
示例#12
0
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)
示例#13
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)
示例#14
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)
示例#15
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
示例#16
0
    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)
示例#17
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)