Skip to content
GSPICE

Concepts

Numerics

GSPICE computes Gaussian conditional predictions from the precision matrix P=C−1P = C^{-1}, avoiding full-spectrum covariance factorizations on each pixel mask. The sections below state the linear algebra behind Prediction and Detection, the operation counts, the numerical condition checks, and measured runtimes.

The conditional distribution of The model needs the inverse of COOC_{OO}, the covariance of the conditioning pixels. That set changes with every prediction and is nearly all of the pixels, so each factorization would cost about npix³/3 flops. GSPICE uses the precision matrix P=C−1P = C^{-1} instead. The density of xx is proportional to exp⁡(−12xTPx)\exp(-\tfrac12 x^\mathsf{T} P x). As a function of xGx_G, it is proportional to

exp⁡ ⁣(−12xGTPGG xG−xGTPGO xO),\exp\!\left(-\tfrac12 x_G^\mathsf{T} P_{GG}\, x_G - x_G^\mathsf{T} P_{GO}\, x_O\right) ,

so

xG∣xO∼N ⁣(−PGG−1PGO xO,  PGG−1).x_G \mid x_O \sim \mathcal{N}\!\left(-P_{GG}^{-1} P_{GO}\, x_O,\; P_{GG}^{-1}\right) .

The two forms are equal in exact arithmetic. The precision form reads only the rows GG of PP: it factors PGGP_{GG} and multiplies PGOP_{GO} by xOx_O. Each prediction has its own GG:

  • PGG, factored
  • PGO, multiplied by xO
  • not read
  • xG, not read
  • xO

The three panels compare pixelwise, infill, and block-scan predictions on 24 pixels. Each row and column of P is one pixel. A prediction reads the rows G of P and the values of x outside G.

The precision form has three advantages:

  • Cost. The model computes PP once for each S/N bin, in npix³ flops, and keeps it. Each pixel set then needs a Cholesky factor of PGGP_{GG}, in ∣G∣3/3|G|^3/3 flops. GG is usually small, a guard window or a few bad pixels.
  • No values from G. In gspice.infill(), the mean −PGG−1(PGOxO)-P_{GG}^{-1} (P_{GO} x_O) reads no value in GG, so an outlier in GG cannot change it.
  • Accuracy. A Cholesky solve with a matrix of condition number κ\kappa has a relative error of order κϵ\kappa \epsilon, with ϵ=2.2×10−16\epsilon = 2.2 \times 10^{-16} (Higham, 2002). The 2022 code instead updates a precomputed inverse (paper Appendix A). Where the guard window is coupled to the conditioning pixels, that update subtracts nearly equal terms, and its error has no such bound. The legacy interface keeps the 2022 arithmetic.

The tests compare each fast path with a direct solve, at condition numbers up to 10⁸.

A call pays for PP once per model and bin, then a cost per pixel set and a cost per spectrum. Spectra of one S/N bin with equal pixel sets GG, MM and TT form a pattern. The spectra of a pattern in one chunk of max(1, 2**24 // npix) rows share one factorization. An infill is therefore fast where many spectra share their masks. With ∣g∣=min⁡(2 nguard+1,npix)|g| = \min(2\,\mathrm{nguard} + 1, \mathrm{npix}) the width of a guard window, the main costs in flops are:

WorkOnce per model and binPer pixel setPer spectrum
PP of a dense covariancenpix³
pixelwisenpix (∣g∣3/3\lvert g\rvert^3/3 + 2 npix ∣g∣\lvert g\rvert) for the guard weights2 npix²
infill∣G∣3/3+∣M∣3/3\lvert G\rvert^3/3 + \lvert M\rvert^3/3 per pattern and chunk2 ∣G∣\lvert G\rvert (npix + ∣G∣\lvert G\rvert)
block scan of fit_blocks and repair∣G∣3/3\lvert G\rvert^3/3 per block2 npix² + ∑A∣G∣2\sum_A \lvert G\rvert^2
fit, per clipping passnpix³, and the guard weightsnpix² for the covariance, 2 npix² for z

The model keeps PP and the guard weights of the latest nguard of each bin, so the “once” column is paid at the first use. pixelwise predicts every pixel by one fixed linear function of xx. It therefore applies the weights to all the spectra of a bin as one matrix product. The docstring of each function states its full cost, and Memory lists the large arrays.

A noise-aware model has its own covariance CsC_s for each spectrum. Its pixelwise therefore pays the “once” column for every spectrum: about npix³ flops with a dense covariance, or npix² KK with rank=K. Its infill forms no PsP_s (Other routes), but pays a factorization per spectrum.

A covariance can be positive definite and still too close to singular for a reliable PP. Every covariance therefore passes a check before its first use, and numpy.linalg.LinAlgError reports a failure:

  • A dense covariance fails where its Cholesky factorization fails, or where its reciprocal condition number is below 10⁻¹³ after scaling the covariance to unit diagonal. LAPACK estimates that number in the 1-norm from the factor.
  • A gspice.LowRank covariance fails where the bound κW\kappa_W of its docstring exceeds 10¹³. κW\kappa_W bounds the condition number of the covariance scaled to unit diagonal.

A default model near the minimum of training spectra, Ns−1N_s - 1 close to npix, can fail the check. Troubleshooting gives the remedies.

Four computations do not factor PGGP_{GG} of a dense PP:

  • A low-rank model, C=VΛVT+DC = V \Lambda V^\mathsf{T} + D, computes PP by the Woodbury identity (Woodbury, 1950), in npix² KK flops, without forming CC:

    P=D−1−D−1V(Λ−1+VTD−1V)−1VTD−1.P = D^{-1} - D^{-1} V \left(\Lambda^{-1} + V^\mathsf{T} D^{-1} V\right)^{-1} V^\mathsf{T} D^{-1} .
  • A dense noise-aware model has its own covariance CsC_s for each spectrum. infill solves with the Cholesky factor of CsC_s and forms no PsP_s.

  • A low-rank noise-aware model forms no PP either. Its infill solves a system of size KK for each spectrum.

  • In a dense model without noise, some infills compute the conditional mean alone: those inside gspice.repair() and gspice.fit_blocks(). They factor COOC_{OO} where GG holds more than half the pixels. That factor is then the smaller one.

The times below come from one run of benchmarks/run.py, which reports the minimum and the median of 5 timed repeats after a warm-up run. The table gives the minimum. The covariance of every case is a squared-exponential kernel of length 15 pixels, plus a rank-30 term, plus the identity, at npix 2400, with nguard=20.

CaseSpectraMinimumPer spectrum, computed
PP of the covariance0.314 s
Guard weights0.075 s
pixelwise, after PP and the weights100,00036.3 s36.3 s / 100,000 = 0.36 ms
infill, 30 pixels, each spectrum its own pattern10000.218 s0.218 s / 1000 = 0.22 ms
infill, 30 pixels, one pattern69900.465 s0.465 s / 6990 = 0.067 ms
infill, 1200 pixels, each spectrum its own pattern201.18 s1.18 s / 20 = 59 ms
Block scan, 31 blocks40001.27 s1.27 s / 4000 = 0.32 ms
fit_blocks40001.87 s1.87 s / 4000 = 0.47 ms
repair, Gaussian spectra20001.93 s1.93 s / 2000 = 0.96 ms
repair, gspice.simulate with artifacts=0.45002.86 s2.86 s / 500 = 5.7 ms
fit, three clipping passes20,00026.9 s
Noise-aware pixelwise, dense83.56 s3.56 s / 8 = 0.45 s
Noise-aware infill, dense, 30 pixels81.50 s1.50 s / 8 = 0.19 s
Noise-aware pixelwise, rank=5080.484 s0.484 s / 8 = 61 ms
Noise-aware infill, rank=50, 30 pixels80.0151 s0.0151 s / 8 = 1.9 ms

Two cases compare with the 2022 arithmetic:

  • The 2022 gp_interp took 18.5 s for 4000 spectra. pixelwise took 1.31 s for the same work, PP and the guard weights included.
  • The 2022 update of a precomputed inverse took 1.45 s for 20 pixel sets of 190 pixels, one spectrum each, with the covariance. The precision form took 0.039 s.

The setup of the run:

  • Commit 10350fa of main, with no local changes to src/gspice or benchmarks/run.py.
  • Intel Xeon Processor @ 2.10 GHz, 4 cores, 16 GB, in a cloud machine. The load average over one minute was 0.49 before the run and 1.00 after it, as benchmarks/run.py records.
  • OPENBLAS_NUM_THREADS=1.
  • Python 3.13.17, numpy 2.5.4 on OpenBLAS 0.3.34, scipy 1.18.1 on OpenBLAS 0.3.31.dev, gspice 2.0.0.

Times from another machine or another thread setting are not comparable with these.