def graph_embed(A,
                mean_photon_per_mode=1.0,
                make_traceless=False,
                rtol=1e-05,
                atol=1e-08):
    r"""Embed a graph into a Gaussian state.

    Given a graph in terms of a symmetric adjacency matrix
    (in general with arbitrary complex entries),
    returns the squeezing parameters and interferometer necessary for
    creating the Gaussian state whose off-diagonal parts are proportional to that matrix.

    Uses :func:`~.takagi`.

    Args:
        A (array[complex]): square, symmetric (weighted) adjacency matrix of the graph
        mean_photon_per_mode (float): guarantees that the mean photon number in the pure Gaussian state
            representing the graph satisfies  :math:`\frac{1}{N}\sum_{i=1}^N sinh(r_{i})^2 ==` :code:``mean_photon``
        make_traceless (bool): Removes the trace of the input matrix, by performing the transformation
            :math:`\tilde{A} = A-\mathrm{tr}(A) \I/n`. This may reduce the amount of squeezing needed to encode
            the graph but will lead to different photon number statistics for events with more than
            one photon in any mode.
        rtol (float): relative tolerance used when checking if the input matrix is symmetric
        atol (float): absolute tolerance used when checking if the input matrix is symmetric

    Returns:
        tuple[array, array]: squeezing parameters of the input
        state to the interferometer, and the unitary matrix representing the interferometer
    """
    (m, n) = A.shape

    if m != n:
        raise ValueError("The matrix is not square.")

    if not np.allclose(A, np.transpose(A), rtol=rtol, atol=atol):
        raise ValueError("The matrix is not symmetric.")

    if make_traceless:
        A = A - np.trace(A) * np.identity(n) / n

    scale = adj_scaling(A, n * mean_photon_per_mode)
    A = scale * A
    s, U = takagi(A, tol=atol)
    vals = -np.arctanh(s)
    return vals, U
def bipartite_graph_embed(A, mean_photon_per_mode=1.0, rtol=1e-05, atol=1e-08):
    r"""Embed a bipartite graph into a Gaussian state.

    Given a bipartite graph in terms of an adjacency matrix
    (in general with arbitrary complex entries),
    returns the two-mode squeezing parameters and interferometers necessary for
    creating the Gaussian state that encodes such adjacency matrix

    Uses :func:`~.takagi`.

    Args:
        A (array[complex]): square, (weighted) adjacency matrix of the bipartite graph
        mean_photon_per_mode (float): guarantees that the mean photon number in the pure Gaussian state
            representing the graph satisfies  :math:`\frac{1}{N}\sum_{i=1}^N sinh(r_{i})^2 ==` :code:``mean_photon``
        rtol (float): relative tolerance used when checking if the input matrix is symmetric
        atol (float): absolute tolerance used when checking if the input matrix is symmetric

    Returns:
        tuple[array, array, array]: squeezing parameters of the input
        state to the interferometer, and the unitaries matrix representing the interferometer
    """
    (m, n) = A.shape

    if m != n:
        raise ValueError("The matrix is not square.")

    B = np.block([[0 * A, A], [A.T, 0 * A]])
    scale = adj_scaling(B, 2 * n * mean_photon_per_mode)
    A = scale * A

    if np.allclose(A, A.T, rtol=rtol, atol=atol):
        s, u = takagi(A, tol=atol)
        v = u
    else:
        u, s, v = np.linalg.svd(A)
        v = v.T

    vals = -np.arctanh(s)
    return vals, u, v