//  [python]
def test_dssvd_symmetric_exp(
    rng_seeds,  # noqa: F811
    torch_devices,  # noqa: F811
    atol_exp,
    dims_ranks_outer_inner_posteriori,
    steep_exp_decay,
    recons_frob_tol,
    svectors_frob_tol,
    svectors_check_n,
):
    """Test distributed SSVD on symmetric exponential matrices.

    Sample matrices, and compute their ``torch.svd`` as well as the sketched
    SVD. Test that:
    * Recovered singular values are correct
    * Recovered matrix is correct and symmetric
    * The first bulk singular vectors are correct
    * A-posteriori error estimation is low
    """
    for seed in rng_seeds:
        for dtype, atol in atol_exp.items():
            for d, r, (outer, inner), post in dims_ranks_outer_inner_posteriori:
                for dec in steep_exp_decay:
                    # we create matrix and full SVD on cpu, it is faster
                    mat = SynthMat.exp_decay(
                        shape=(d, d),
                        rank=r,
                        decay=dec,
                        symmetric=True,
                        seed=seed,
                        dtype=dtype,
                        device="cpu",
                    )
                    U, S, Vt = torch.linalg.svd(mat)

                    # then we test decomposition on both devices
                    for device in torch_devices:
                        mat = mat.to(device)
                        U = U.to(device)
                        S = S.to(device)
                        Vt = Vt.to(device)
                        #
                        tmpdir = tempfile.TemporaryDirectory()
                        (ro_Q, core_U, core_S, core_Vt, lo_Qt) = mock_dssvd(
                            tmpdir.name, mat, device, dtype, outer, inner, seed
                        )
                        # check that singular values are correct
                        assert torch.allclose(
                            S[: 2 * r], core_S[: 2 * r], atol=atol
                        ), "Bad recovery of singular values!"
                        # check that recovered matrix is the same
                        core_U, core_S, core_Vt = truncate_core(
                            2 * r, core_U, core_S, core_Vt
                        )
                        mat_recons = (
                            ro_Q @ core_U @ torch.diag(core_S) @ core_Vt @ lo_Qt
                        )
                        recons_err = torch.dist(mat, mat_recons) / mat.norm()
                        assert recons_err <= abs(
                            recons_frob_tol[dtype]
                        ), "Bad SSVD reconstruction!"
                        # check that a few singular vectors are correct: avoid
                        # the ones that have the same singular values, since
                        # they aren't unique. Also compare outer prods, since
                        # multiplying left and right by -1 yields same result.
                        left = ro_Q @ core_U[:, r:]
                        right = core_Vt[r:, :] @ lo_Qt

                        for idx in range(svectors_check_n):
                            recons_err = torch.dist(
                                torch.outer(left[:, idx], right[idx]),
                                torch.outer(U[:, idx + r], Vt[idx + r]),
                            ) / (U[:, idx + r].norm() * Vt[idx + r].norm())
                            assert recons_err <= abs(
                                svectors_frob_tol[dtype]
                            ), "Bad recovery of singular vectors!"
                        # check a-posteriori error. With 30 measurements, we
                        # know that the probability of the actual error being
                        # twice as big is less than 1%, and keeps shrinking
                        # exponentially: a_posteriori_error_bounds(30, 1)
                        # Therefore, a very small estimate almost certainly
                        # guarantees that the approximation is good, and we
                        # have enough rank in our truncated SVD.
                        for adjoint in [True, False]:
                            _, _, err_estimate = a_posteriori_error(
                                mat,
                                mat_recons,
                                post,
                                seed=seed,
                                dtype=dtype,
                                device=device,
                                adjoint=adjoint,
                            )[0]
                            assert (
                                err_estimate / mat.numel()
                            ) <= atol, "Bad a-posteriori estimate!"
                        # cleanup HDF5 tmpdir
                        tmpdir.cleanup()