Skip to main content
Sandbox Physics
Searching for axions with cosmic magnetic fields

Recompute a cluster sightline from saved inputs

Sandbox Physics · · L3 · Method

By the end of this guide you will have rerun three saved photon–axion sightlines with a short Python script that shares no code with the Lab. You will check that probability is conserved, that the uniform case matches the closed formula, and that a Monte Carlo mean over random field directions matches the ensemble rule. You will also see four ways the calculation goes wrong. Everything here is an offline simulation of model inputs. None of it is an observation.

Download three files: the saved inputs, the script and the saved outputs. The script needs Python 3 and its standard library only. The outputs were produced with Python 3.9.6. The physics is the two-state mixing of Photon and axion as two mixing states and the random-cell model of Random domains, polarization and stellar cooling.

Step 1: read the inputs and their units

Open the inputs file first. The units block fixes every unit before any number appears: photon energy in keV, coupling in inverse GeV, field in microgauss, electron density per cubic centimetre, axion mass in eV, lengths in kpc and angles in radians. Seven energies from 0.6 to 9 keV are listed once and shared by all cases.

There are three cases. The uniform case holds one field direction split into 12 cells of 8 kpc. The tangled case is the Lab's default sightline, 24 cells with fixed but irregular angles. The resonance case sets the axion mass equal to the plasma frequency of the gas. Each case stores the cell angles explicitly. The Lab draws its angles from a seeded generator. Saving the angles lets anyone rerun the case with any random-number library.

Step 2: run the script

python3 -I recompute_sightline.py sightline-inputs.json my-outputs.json

The -I flag keeps Python from importing modules from the current folder. Compare your file with the saved outputs. They should agree to the last few digits. The Lab's own transfer matrices, computed in a different language and a different way, agree with these outputs to better than 10−1010^{-10} in survival probability. That agreement is checked by a repository test.

Step 3: what one cell does

Each cell rotates the state into the field frame, evolves the parallel photon and the axion in the eigenbasis of their two-by-two matrix, gives the perpendicular photon its plasma phase, and rotates back. This is the central function:

def evolve_cell(state, plasma, axion, mixing, length, angle):
    ax, ay, a = state
    c, s = math.cos(angle), math.sin(angle)
    parallel = c * ax + s * ay
    perpendicular = (-s * ax + c * ay) * cmath.exp(1j * plasma * length)
    theta = 0.5 * math.atan2(2 * mixing, plasma - axion)
    mean = 0.5 * (plasma + axion)
    half_split = 0.5 * math.hypot(plasma - axion, 2 * mixing)
    ct, st = math.cos(theta), math.sin(theta)
    upper = (ct * parallel + st * a) * cmath.exp(1j * (mean + half_split) * length)
    lower = (-st * parallel + ct * a) * cmath.exp(1j * (mean - half_split) * length)
    parallel, a = ct * upper - st * lower, st * upper + ct * lower
    return [c * parallel - s * perpendicular, s * parallel + c * perpendicular, a]

The three arguments plasma, axion and mixing are Δpl\Delta_{\rm pl}, Δa\Delta_a and Δaγ\Delta_{a\gamma} in inverse kiloparsecs. The script derives them from CODATA constants. One kiloparsec is 1.5637×1026 eV−11.5637\times10^{26}\,\mathrm{eV^{-1}}, one tesla is 195.35 eV2195.35\,\mathrm{eV^2} and the plasma frequency is 3.713×10−113.713\times10^{-11} eV at one electron per cubic centimetre. These three values are written into the outputs under derivedConstants, so a reader can see which constants produced a given file.

Unpolarized light is handled by running the two pure polarizations separately and averaging their density matrices with weight one half each. The linear polarization degree is

ΠL=(ρxx−ρyy)2+4(Re ρxy)2ρxx+ρyy.\Pi_L=\frac{\sqrt{(\rho_{xx}-\rho_{yy})^2+4(\mathrm{Re}\,\rho_{xy})^2}}{\rho_{xx}+\rho_{yy}}.

Step 4: the checks that must pass

Probability. Photon survival plus axion fraction must equal one. The largest deviation over all 21 saved rows is 3.6×10−153.6\times10^{-15}, which is rounding error.

Uniform case against the formula. For one uniform field the survival of unpolarized light is 1−Pγ→a/21-P_{\gamma\to a}/2 with the two-state probability. Every uniform row agrees with this closed form to about 10−1510^{-15}. At 6.6 keV both give 0.5572. At 3 keV both give 0.9729, which sits near a node of the oscillation.

Resonance flatness. With the axion mass equal to the plasma frequency, survival is 0.7761 at all seven energies.

Monte Carlo mean. The tangled case's saved angles are one realization. To test the ensemble rule, the script draws 2000 new sets of 24 uniform angles with Python's own generator, seed 79, and averages the axion fraction at 3 keV:

Table 5.1 Monte Carlo mean against the random-angle rule, at 3 keV.
QuantityValue
Mean axion fraction from 2000 realizations0.24340.2434
Standard error of that mean0.00270.0027
Product rule over 24 cells0.23990.2399
Difference in standard errors1.31.3

A difference of 1.3 standard errors is consistent with chance. A pull above about three would point to a bug in either the propagation or the formula.

Step 5: four failure cases

The outputs also store four deliberate mistakes, each with a number that shows the damage.

Table 5.2 Saved failure cases and what they show.
MistakeMeasured effectLesson
Plasma term with the wrong sign at resonanceSurvival spread over energy rises from 2×10−152\times10^{-15} to 0.0130.013The resonance needs Δpl\Delta_{\rm pl} and Δa\Delta_a to have the same sign
Twelve energies instead of 400Interpolation misses by up to 0.280.28One realization has narrow ripples
Sixteen forward-Euler steps in one strong cellTotal probability 1.0551.055Euler steps are not unitary
Treating one sightline as the meanLargest gap 0.220.22 over 0.5 to 10 keVCompare realizations with realizations

The Euler case is the most common in practice. Any integrator that does not preserve the norm will slowly create or destroy photons, and the error grows with path length. The exact cell transfer avoids this. A fourth-order Runge–Kutta integrator with a 2 pc step, used as an independent check in the repository tests, agrees with it to 10−610^{-6}.

Step 6: recompute a sightline exported from the Lab

The Lab's export button writes a JSON record with format sandboxphysics.axion-photon-conversion.v1. Its input block stores logarithms for coupling, density and mass, with keys such as logCouplingPerGev. Convert each with ten to that power and copy the domains list unchanged. Then add the result as a new case in your inputs file. The export also stores spectrum.simulatedSurvival and, separately, spectrum.idealReference. Compare your recomputed rows with the simulated curve. The ideal reference is a different object: the closed formula in uniform mode, or the ensemble mean in tangled mode.

Step 7: read the frozen limits with their assumptions

The Lab's stellar ruler shows four published limits. They are copied as printed and not recomputed, so each carries its authors' assumptions.

Table 5.3 Published limits shown in the Lab and the assumptions they rest on.
LimitValueHolds forRests on
Chandra spectrum of NGC 12757.9×10−13 GeV−17.9\times10^{-13}\,\mathrm{GeV^{-1}}, 99.7%ma<10−12m_a<10^{-12} eVField model B of the Perseus cluster
Fermi-LAT spectrum of NGC 12755×10−12 GeV−15\times10^{-12}\,\mathrm{GeV^{-1}}, 95%0.5 to 5 neVSix years of LAT data and an assumed Perseus field model
CAST helioscope5.8×10−11 GeV−15.8\times10^{-11}\,\mathrm{GeV^{-1}}, 95%ma<0.02m_a<0.02 eVThe solar axion flux from the standard solar model
Horizontal-branch star counts6.6×10−11 GeV−16.6\times10^{-11}\,\mathrm{GeV^{-1}}, 95%up to a few keVStellar models and the helium abundance

The X-ray value is eq. (5) of Reynolds et al., 2020. The gamma-ray value is from the abstract of Ajello et al., 2016, which we did not read in full. The CAST and stellar values are eqs. (89.20) and (89.16) of the PDG review of axions, 2025. The stellar mass edge in the Lab is set conservatively at 1 keV, because Ayala et al., 2014 discuss particles below a few keV but print no sharp cutoff.

The confidence levels differ, 95 percent against 99.7 percent, so the numbers are not directly interchangeable. Reynolds and colleagues give 6.3×10−13 GeV−16.3\times10^{-13}\,\mathrm{GeV^{-1}} for their field model A. Changing the field model alone moves the X-ray limit by tens of percent.

A checklist for the next sightline

  1. Fix units for every input and record them in the file.
  2. Save field angles explicitly, not only a seed.
  3. Check normalization on every row.
  4. Check a uniform case against the closed formula and a resonance case for flatness.
  5. Compare a mean over many realizations with the ensemble rule, and report the pull.
  6. Sample energies finely enough to resolve one realization's ripples.
  7. Label every curve as a simulation, an ideal reference or an observation, and keep published limits with their confidence level, mass range and field model.

Try it in the Lab: Export a sightline from the Lab and compare it with the saved outputs.

Sources

  1. Reynolds et al., 2020
  2. Ajello et al., 2016
  3. PDG review of axions, 2025
  4. Ayala et al., 2014

Keep reading

What this page links to directly

Searching for axions with cosmic magnetic fields · Reading path

From two mixing states to a published limit

  1. ArticleHow could an invisible particle change a distant spectrum?
  2. ArticlePhoton and axion as two mixing states
  3. ArticleMixing in a cluster magnetic field and plasma
  4. ArticleRandom domains, polarization and stellar cooling
  5. You are hereRecompute a cluster sightline from saved inputs
  6. ArticleReading the Chandra NGC 1275 axion limit
Something broken, confusing, or missing on this page?Tell us
What kind of feedback?

Sent with this page’s address, its Lab ID, the site language and your device type. Please leave out personal details. Feedback is kept for up to 12 months.Privacy policy