Skip to content

NaN lensing potential on Windows after matplotlib lays out text in a process that imported scipy.stats #210

Description

@salihyesil59

On Windows, get_cmb_power_spectra returns an all-NaN lens_potential (and hence NaN lensed_scalar/total) for an ordinary Planck-like LCDM, with no exception raised. Everything else in the same call is correct.

I appreciate this looks like it cannot be CAMB's fault, and it may well not be — the trigger involves two unrelated libraries. I'm reporting it here because CAMB is where the wrong number is produced, and because the failure is confined to the non-linear lensing branch, which narrows it a lot. Happy to move it to scipy or matplotlib if you think it belongs there.

Reproduction

Three ingredients. All three are required; any two are harmless.

import scipy.stats                                  # 1

from matplotlib import font_manager
from matplotlib.ft2font import FT2Font
font = FT2Font(font_manager.findfont("DejaVu Sans"))
font.set_size(10, 100)
font.set_text("Redshift z")                         # 2
font.get_width_height()

import numpy as np, camb                            # 3
pars = camb.CAMBparams()
pars.set_cosmology(H0=70.0, ombh2=0.02237, omch2=0.1177, tau=0.0544, mnu=0.06)
pars.InitPower.set_params(As=np.exp(3.044) / 1e10, ns=0.9649)
pars.set_for_lmax(2508, lens_potential_accuracy=1)
pars.set_matter_power(redshifts=[0.0], kmax=2.0)

results = camb.get_results(pars)
powers = results.get_cmb_power_spectra(pars, CMB_unit="muK", raw_cl=True)

print("sigma8         ", np.isfinite(results.get_sigma8_0()))
print("unlensed_scalar", np.all(np.isfinite(powers["unlensed_scalar"])))
print("lens_potential ", np.all(np.isfinite(powers["lens_potential"])))

Output on a failing run:

sigma8          True
unlensed_scalar True
lens_potential  False

Each run needs a fresh process. The outcome is decided once per process — repeating the call inside one process returns NaN again every time, so this is not a transient glitch. Rate is 8/8 with default threading, 7/10 with OMP_NUM_THREADS=1.

A self-contained script that prints versions and can measure the rate itself is at
https://github.com/salihyesil59/CosmoFit/blob/dev/tools/camb_nan_repro.py

What still works in the failing call

hubble_parameter, comoving_radial_distance finite
matter transfer data, P(k) finite
get_sigma8_0() finite
unlensed_scalar, unlensed_total, tensor finite
lens_potential NaN
lensed_scalar, total NaN (inherited)

pars.NonLinear = camb.model.NonLinear_none makes the same call finite. Every configuration with the non-linear correction on fails, including lens_potential_accuracy=0 with NonLinear_lens still set.

Things I checked, so you don't have to

Everything below was measured over at least 8 fresh processes per configuration; single runs are meaningless here.

  • Not the floating-point mode. MXCSR and the CRT control word are bit-identical (0x1fa0, round-to-nearest, all exceptions masked) before the layout and in 8 of 8 failing runs.
  • Not memory pressure. Substituting allocation churn for the text layout — 20k small numpy arrays, 40 MB-scale numpy arrays, or 400k Python objects — is 0/8 in all three cases.
  • Not scipy's bundled OpenBLAS. scipy.linalg is what loads it, and scipy.linalg + layout is 0/8. Of linalg, special, integrate, interpolate, optimize, sparse, stats, only stats reproduces (4/8 alone; 7/8 with all seven imported).
  • Not HMcode's derivation. At set_feedback_level(3) the HMcode-2020 output (Dv, dc, eta, k*, A, fdamp, alpha at each redshift) is byte-identical, 240 lines, between a failing run and a clean one. With NonLinear_both the returned non-linear P(k) is finite while lens_potential is NaN in the same call.
  • Not the parameters. Pickling every CAMBparams from a failing session and replaying them in a fresh process is completely clean.
  • Order-dependent. Importing scipy.stats after the glyph layout instead of before it is clean, 0/8. So it is not simply that both are loaded; which one initialises first decides it.

The last point plus the per-process determinism is what makes me suspect module layout or DLL binding rather than arithmetic.

Environment

Windows 11 (10.0.26200), AMD64, Intel 16 cores
Python 3.13.15 (MSC v.1944 64 bit)
camb 2.0.4          (pip wheel, cambdll.dll 2,722,426 bytes)
numpy 2.5.2
scipy 1.18.1
matplotlib 3.11.1
halofit_version = mead2020 (the default via set_for_lmax)

Verified on this one machine only. Everything above is measured
rather than inferred, but all of it comes from a single Windows
installation, so I cannot say whether the trigger generalises or
depends on something local to it. That is the first thing worth
checking, and the script prints the versions it ran under to make
comparison easy.

Why it matters

The NaN is returned rather than raised, so a likelihood consuming these spectra turns it into a non-finite chi2, which a sampler reads as a rejection. In an MCMC that is a quietly dropped point rather than a visible error. I guard against it downstream now, but a caller who does not check would never see it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions