|
| 1 | +import jax.numpy as jnp |
1 | 2 | import numpy as np |
2 | 3 | import pytest |
| 4 | +from jax.test_util import check_grads |
3 | 5 |
|
4 | 6 | import s2fft |
5 | 7 | import s2fft.sampling as smp |
@@ -143,3 +145,93 @@ def test_generate_flmn(rng, L, N, L_lower, reality): |
143 | 145 | assert np.allclose(f_complex.imag, 0) |
144 | 146 | f_real = s2fft.wigner.inverse(flmn, L, N, reality=True, L_lower=L_lower) |
145 | 147 | assert np.allclose(f_complex.real, f_real) |
| 148 | + |
| 149 | + |
| 150 | +def gaussian_covariance(spectra): |
| 151 | + """Gaussian covariance for a stack of spectra. |
| 152 | +
|
| 153 | + If the shape of *spectra* is *(K, K, L)*, the shape of the |
| 154 | + covariance is *(L, C, C)*, where ``C = K * (K + 1) // 2`` |
| 155 | + is the number of independent spectra. |
| 156 | +
|
| 157 | + """ |
| 158 | + _, K, L = spectra.shape |
| 159 | + row, col = np.tril_indices(K) |
| 160 | + cov = np.zeros((L, row.size, col.size)) |
| 161 | + ell = np.arange(L) |
| 162 | + for i, j in np.ndindex(row.size, col.size): |
| 163 | + cov[:, i, j] = ( |
| 164 | + spectra[row[i], row[j]] * spectra[col[i], col[j]] |
| 165 | + + spectra[row[i], col[j]] * spectra[col[i], row[j]] |
| 166 | + ) / (2 * ell + 1) |
| 167 | + return cov |
| 168 | + |
| 169 | + |
| 170 | +@pytest.mark.flaky |
| 171 | +@pytest.mark.parametrize("L", L_values_to_test) |
| 172 | +@pytest.mark.parametrize("xp", [np, jnp]) |
| 173 | +def test_generate_flm_from_spectra(rng, L, xp): |
| 174 | + # number of fields to generate |
| 175 | + K = 4 |
| 176 | + |
| 177 | + # correlation matrix for fields, applied to all ell |
| 178 | + corr = xp.asarray( |
| 179 | + [ |
| 180 | + [1.0, 0.1, -0.1, 0.1], |
| 181 | + [0.1, 1.0, 0.1, -0.1], |
| 182 | + [-0.1, 0.1, 1.0, 0.1], |
| 183 | + [0.1, -0.1, 0.1, 1.0], |
| 184 | + ], |
| 185 | + ) |
| 186 | + |
| 187 | + ell = xp.arange(L) |
| 188 | + |
| 189 | + # auto-spectra are power laws |
| 190 | + powers = xp.arange(1, K + 1) |
| 191 | + auto = 1 / (2 * ell + 1) ** powers[:, None] |
| 192 | + |
| 193 | + # compute the spectra from auto and corr |
| 194 | + spectra = xp.sqrt(auto[:, None, :] * auto[None, :, :]) * corr[:, :, None] |
| 195 | + assert spectra.shape == (K, K, L) |
| 196 | + |
| 197 | + # generate random flm from spectra |
| 198 | + flm = s2fft.utils.signal_generator.generate_flm_from_spectra(rng, spectra) |
| 199 | + assert flm.shape == (K, L, 2 * L - 1) |
| 200 | + |
| 201 | + # compute the realised spectra |
| 202 | + re, im = flm.real, flm.imag |
| 203 | + result = ( |
| 204 | + re[None, :, :, :] * re[:, None, :, :] + im[None, :, :, :] * im[:, None, :, :] |
| 205 | + ) |
| 206 | + result = result.sum(axis=-1) / (2 * ell + 1) |
| 207 | + |
| 208 | + # compute covariance of sampled spectra |
| 209 | + cov = gaussian_covariance(spectra) |
| 210 | + |
| 211 | + # data vector, remove duplicate entries, and put L dim first |
| 212 | + x = result - spectra |
| 213 | + x = x[np.tril_indices(K)] |
| 214 | + x = x.T |
| 215 | + |
| 216 | + # compute chi2/n of realised spectra |
| 217 | + y = xp.linalg.solve(cov, x[..., None])[..., 0] |
| 218 | + n = x.size |
| 219 | + chi2_n = (x * y).sum() / n |
| 220 | + |
| 221 | + # make sure chi2/n is as expected |
| 222 | + sigma = np.sqrt(2 / n) |
| 223 | + assert np.fabs(chi2_n - 1.0) < 3 * sigma |
| 224 | + |
| 225 | + |
| 226 | +@pytest.mark.parametrize("L", L_values_to_test) |
| 227 | +def test_generate_flm_from_spectra_grads(L): |
| 228 | + # fixed set of power spectra |
| 229 | + ell = jnp.arange(L) |
| 230 | + cl = 1 / (2 * ell + 1) |
| 231 | + spectra = cl.reshape(1, 1, L) |
| 232 | + |
| 233 | + def func(x): |
| 234 | + rng = np.random.default_rng(42) |
| 235 | + return s2fft.utils.signal_generator.generate_flm_from_spectra(rng, x) |
| 236 | + |
| 237 | + check_grads(func, (spectra,), 1) |
0 commit comments