Single precision#
solprop computes in IEEE binary64 (double precision) by default. A run can instead hold
its propagated field and its nonlinear evaluation in binary32 (single precision) with
precision="single" (SEP 33).
Single precision halves the memory of every field-sized buffer and is what graphics
processors without fast binary64 arithmetic compute in; whether a given problem keeps
its accuracy in binary32 is a property of the problem, so this page records which do.
Note
Single precision runs on the mlx backend, on the GPU of an Apple-silicon Mac
(choosing a backend), for capillary and fibre runs
and radial and Cartesian free space. Its Metal kernels run in the macOS lane of
continuous integration. mlx is experimental in this release: its coverage is not
measured, the survival record below was measured on the reference implementation
(on mlx for two cases only), and the keyword interfaces size its grids for MLX’s
transforms (grid sizes on mlx). numpy_ref runs
every route below as the reference implementation, which defines the arithmetic and
measures it but is not fast; away from Apple silicon it is the supported way to run single
precision, by name, because native computes in binary64 only
(#342).
Asking for it#
from solprop.driver import prop_capillary
output = prop_capillary(
125e-6,
0.1,
"He",
0.8,
wavelength=800e-9,
fwhm=10e-15,
energy=100e-6,
wavelength_limits=(200e-9, 3e-6),
trange=200e-15,
precision="single",
backend="numpy_ref",
)
precision is a keyword of prop_gnlse, prop_capillary and prop_waveguide, and a
field of SimulationSettings for the free-space
interfaces. Under single precision:
The tolerance floor. An explicit adaptive
rtolbelow1e-6(SINGLE_RTOL_FLOOR) is refused: a local error target below binary32’s rounding per step buys steps without accuracy. A fixed schedule reads no tolerance.The default tolerance. Each interface takes the larger of its binary64 default and the floor, and no more than
1e-4(SINGLE_RTOL_CAP), the loosest value the survival record below measured:1e-6onprop_gnlse(whose binary64 default is1e-7) andprop_capillary,1e-5onprop_waveguide,prop_radialandprop_planar, and1e-4in free space (its record). A spatial interface applies it where itsSimulationSettingsgive no controller; an explicitrtolor controller is the caller’s.The step’s exponent. Binary32 represents
exp(x)only up to \(x \approx 88.7\) (binary64 to 709.8). The Runge–Kutta interaction picture multiplies by the linear propagator and its inverse, so underRungeKutta()a replayed step (or an adaptive run’s floordz_min) over which a strong collar or loss reaches that exponent is refused at translation, with the shortest step that fits, and one within a tenth of it is warned of; a 1 mm capillary’s default collar on 0.25 mm steps is one such case. Onmlxan adaptiveRungeKutta()trial past that range is combined in the lab frame instead, by forward flows only, so it is not rejected and shrunk for the inverse’s sake: a radial Kerr beam whose first trial reaches 459 takes its millimetre in one step, as binary64 does. The low-storage march and the exponential Adams method apply forward flows only: a damped bin’s flow underflows harmlessly towards zero, so they are limited by growth alone, which no passive medium has. The Kerr beams’ 0.5 µm steps, whose spectral collar reaches 150 to 180, run in binary32 withLowStorageRungeKutta()for this reason.The integrator. Each interface takes its binary64 default (the defaults):
Multistep()in its exponential formulation onprop_capillary,prop_radialandprop_planar(the Adams record),RungeKutta()(DOPRI5) elsewhere; the low-storage march is admitted too. A run changes arithmetic, not method, when it changes precision.Noise. A default
noiseresolves to none, and the run records why; an explicit noise model is refused (below).The backend.
backend="auto"resolves to"mlx"when MLX and a Metal GPU are present, and is refused otherwise; it never resolves to a binary64 backend, nor to the reference implementation, which is named explicitly,backend="numpy_ref".Outputs are unchanged: saved fields, statistics and processing are binary64, and files are the same size. The precision is part of the run’s configuration, so a run is never resumed in the other precision.
What it covers#
|
|
|
|---|---|---|
capillaries, solid-core and GNLSE fibres, mode-averaged and multimode, field and envelope grids |
yes |
yes |
Kerr, the envelope grid’s third-harmonic term, scalar and tensor Raman, gas Raman mixtures, the silica bundle |
yes |
yes |
field-grid plasma |
yes |
yes |
envelope-grid plasma |
yes |
yes, each sample’s ellipse and cycle moments in one GPU kernel |
radial and Cartesian free space, including a line, with their transverse collars |
yes |
yes |
gradients, tapers, loss, both temporal absorber schemes, the polarisation taper |
yes |
yes, where the operator is device-ready (not a z table) |
Runge–Kutta, the low-storage march |
yes |
yes |
fixed-schedule batches through |
yes |
refused |
the Adams integrator |
the exponential formulation |
the exponential formulation, its history on the GPU; on the Metal device only |
noise models |
refused (a known limitation, below) |
refused |
the general modal route, the hybrid planar kind, coupled flows, TDSE responses, the dispersive electronic response, the far-field statistics |
refused |
refused |
Grid sizes on mlx#
A grid’s default sample count, fft_size="auto", is the cheapest size with prime
factors 2, 3 and 5 by a transform-cost model (grids).
For FFTW on a CPU that is often a smooth size such as 10240. MLX’s GPU transform runs
any such size up to 4096 directly, but above 4096 only powers of two: every other
length takes Bluestein’s algorithm, six to twelve times slower than the next power of
two. On an Apple M5 Max an rfft and irfft pair took:
samples |
µs |
next power of two |
µs |
|---|---|---|---|
5120 |
502 |
8192 |
78 |
10240 |
715 |
16384 |
63 |
So "auto" minimises the cost of the transform library the run will use
(solprop.grid.fft_cost()). The keyword interfaces (prop_capillary,
prop_gnlse, prop_waveguide) take MLX’s costs when the run is single precision on
mlx (or backend="auto", which resolves to it), and so build a power of two past
4096 samples; with backend="numpy_ref", or in binary64, they keep FFTW’s. A grid of
your own, as the free-space interfaces take, chooses with transforms=:
from solprop.grid import FieldGrid
grid = FieldGrid.from_limits(0.1, 800e-9, (200e-9, 3e-6), 200e-15, transforms="mlx")
fft_size="pow2" gives the same sizes past 4096. A run on mlx with a smooth grid
past 4096 samples, given explicitly, runs correctly but slowly.
How the arithmetic is kept accurate#
Binary32 carries about seven significant digits and represents magnitudes between about \(10^{-38}\) and \(3\times10^{38}\). solprop keeps both from limiting a run:
Range. Every table a run multiplies by is narrowed with its own power-of-two exponent, and the field is held at an exact power-of-two scale chosen from the launch. A nitrogen Raman kernel whose spectrum is near \(10^{-50}\) — zero in binary32 as it stands — keeps every bin; a Cartesian energy sum that would reach \(10^{44}\) is scaled before squaring.
The linear propagator \(e^{\Delta\Phi}\) is formed to binary64 accuracy and rounded once per step. Its rounding still adds coherently from step to step, about \(c\,M\,u\) after \(M\) steps with \(u = 2^{-24}\); the record below states \(c\).
Populations. The ionised fraction and the neutral survival are formed in forms that keep relative accuracy at tiny and saturated exposures, and running integrals over time are summed in a balanced order rather than one long sequential sum.
The controller, landing and every scalar stay in binary64.
The survival record#
Each case below was run with RungeKutta() in both precisions: in single precision at
rtol = 1e-6 and 1e-5, compared at the end of its window with a binary64 run on the
same grid at rtol = 1e-10, against accuracy envelopes fixed before any comparison: the
field to \(10^{-3}\), the spectral energy in each of 32 bands above −40 dB to 1 %, the
total energy to \(10^{-4}\), the peak power or intensity, the final ionised fraction and
the beam width to \(10^{-3}\), all relative. The table gives the largest of those relative
errors at rtol = 1e-6, the field’s error, and binary64’s own field error at the same
tolerance for comparison.
case |
single: field error |
largest other metric |
binary64 at |
verdict |
|---|---|---|---|---|
self-phase modulation, 10π, no dispersion (GNLSE) |
7.9e-6 |
9.6e-5 (bands) |
1.9e-6 |
survives |
fundamental soliton, ten periods (GNLSE) |
2.7e-5 |
6.5e-6 (peak) |
1.1e-6 |
survives |
dispersive-wave emission, He capillary gradient, two modes, 1.5 m |
2.6e-6 |
4.9e-5 (bands) |
1.3e-6 |
survives |
comb-to-continuum, H₂ capillary at 40 bar, 0.1 m, noise-free |
2.6e-3 |
3.4e-4 (peak) |
3.0e-7 |
survives over 0.05 m |
plasma blue shift, Ar capillary, field grid, 0.4 m |
1.8e-4 |
2.1e-4 (bands) |
1.6e-4 |
survives |
plasma blue shift, Ar capillary, envelope grid, 0.4 m |
1.3e-5 |
2.1e-5 (ionised) |
1.3e-5 |
survives |
molecular Raman, N₂ capillary at 2 bar, 0.3 m |
1.1e-6 |
2.7e-5 (bands) |
1.6e-7 |
survives |
radial Kerr beam, 0.3 m |
8.5e-6 |
1.3e-4 (bands) |
8.6e-6 |
survives |
radial filament in argon with plasma, 3 cm |
2.3e-6 |
1.2e-5 (peak) |
2.2e-6 |
survives |
Cartesian Gaussian diffraction, 64 × 64 |
2.4e-7 |
2.4e-7 (bands) |
0 |
survives |
Cartesian Kerr beam, 64 × 64 |
2.7e-6 |
2.0e-5 (bands) |
4.2e-6 |
survives |
At rtol = 1e-5 every case but the comb-to-continuum stays inside its envelopes too.
The table is the reference implementation’s, and its survival limits are the
reference’s; mlx’s are unmeasured but for the two cases below. Its rows were measured
on 24 and 25 October 2026, before the single-precision fixes of 27 October 2026 made the
reference’s transforms sum in binary32 (the comb-to-continuum case, re-measured on them,
is below) and before the default grids and boundaries changed
(SEP 21); they have not been
repeated since. On mlx, whose envelope-grid plasma is its own discretisation of the
cycle moments, the envelope-grid blue shift over its full 0.4 m has field error 4.8e-5
(largest other 5.3e-5, the ionised fraction) and the field-grid one 2.2e-4 (2.8e-4,
bands): both survive.
The comb-to-continuum limit. Noise-free stimulated Raman scattering grows from whatever seed the Stokes band holds; rounding seeds it at the level of the unit roundoff times the field. In binary64, adding a seed of \(2^{-24}\) of the launch’s peak spectral amplitude moves the result at 0.1 m by 5 %: the sensitivity is the physics, and binary32’s own rounding is such a seed. Over 0.05 m the run is inside every envelope. The reference’s transforms now sum in binary32, as the backends’ do (NumPy’s float32 transforms are binary64 rounded once, which this record first used); on them the field error is 2.5×10⁻⁴ at 0.05 m and 1.6×10⁻³ at 0.1 m, so the limit stands (SEP 36).
The tolerance floor stays at
1e-6: tightening from1e-5to1e-6still buys accuracy (by 92 % in the self-phase-modulation case’s field).The propagator’s step limit. With no nonlinear source the rounding of \(e^{\Delta\Phi}\) accumulates as \(c\,M\,u\) after \(M\) steps, with \(c \approx 0.5\) measured on a capillary operator over \(10^2\)–\(10^4\) steps (0.1–0.3 on a GNLSE one). The field envelope of \(10^{-3}\) is reached after about \(3\times10^4\) steps; a run that needs more takes binary64.
Free space at its binary64 tolerance#
Free space’s binary64 default, rtol = 1e-4, was qualified in binary32 on 5 November
2026 (#85); until then a single-precision
free-space run took 1e-5. Each case ran with RungeKutta(), noise-free, on a
power-of-two grid, in binary32 on mlx and binary64 on native, against a binary64 run
at rtol = 1e-6 (the field’s relative difference):
case |
binary64 at |
binary32 at |
binary64 at |
binary32 at |
|---|---|---|---|---|
radial filament in argon with plasma, 3 cm |
9.8e-5 |
2.0e-5 |
1.2e-5 |
1.0e-5 |
radial Kerr beam with its transverse collar, 8 cm |
2.5e-5 |
2.1e-5 |
1.7e-6 |
2.0e-6 |
Cartesian argon filament, 128 × 128, 1.5 cm |
6.9e-5 |
2.4e-5 |
6.2e-6 |
5.2e-6 |
Binary32 at 1e-4 is as close to the reference as binary64 at 1e-4, or closer: the
tolerance, not the precision, sets the error. The Cartesian filament takes 48 steps at
1e-4 against 61 at 1e-5.
The Adams integrator in binary32#
Multistep() was admitted on 5 November 2026 by the rule
#342 proposed: binary32 Adams judged
against binary64 Adams at the same tolerance, rather than against Runge–Kutta, whose
difference from Adams is the methods’ truncation. Each case ran its whole length,
noise-free, on a power-of-two grid, at each interface’s default tolerance, on mlx
(binary32) and native (binary64). The table gives the relative difference of the
final fields, and binary64 Runge–Kutta’s from binary64 Adams as the scale of the
methods’ own truncation:
case |
binary32 Adams against binary64 Adams |
binary64 RK against binary64 Adams |
binary32 RK against binary64 Adams |
|---|---|---|---|
He capillary gradient, mode-averaged, 1.5 m (RDW) |
2.3e-6 |
3.6e-6 |
4.3e-5 |
the same, four modes |
4.3e-6 |
1.7e-6 |
1.3e-4 |
Ar capillary, field-grid plasma, 0.2 m |
1.3e-5 |
2.3e-5 |
4.9e-5 |
Ar capillary, envelope-grid plasma, 0.2 m |
8.2e-5 |
1.1e-4 |
1.2e-4 |
H₂ capillary, Raman, mode-averaged, 0.4 m |
8.0e-5 |
2.9e-5 |
1.0e-4 |
GNLSE, Raman and shock, 0.15 m |
2.2e-4 |
3.1e-5 |
9.4e-5 |
radial filament in argon with plasma, 3 cm |
6.8e-6 |
1.4e-5 |
1.1e-5 |
radial Kerr beam with its transverse collar, 8 cm |
5.0e-6 |
6.7e-6 |
7.7e-6 |
Every case is far inside the field envelope of \(10^{-3}\), and all but the GNLSE within a
small multiple of the methods’ own difference; the GNLSE runs at 1e-6 in binary32
against binary64’s 1e-7 (the floor), which accounts for its row. On the capillaries
binary32 Adams is closer to the binary64 reference than binary32 Runge–Kutta is.
Known limitations#
Noise. A one-photon-per-mode seed is of order \(10^{-5}\) of a microjoule pulse’s peak spectral amplitude and smaller at higher energies, while binary32’s accumulated rounding can reach \(10^{-5}\) after \(10^4\) steps. Noise-seeded runs are refused under single precision until that boundary is qualified.
Amplifying dynamics. Rounding at the level of \(10^{-7}\) per step is amplified like any perturbation by soliton fission, modulation instability or noise-free stimulated Raman scattering; such runs reach their accuracy over a shorter length, as the record states.
Performance. On the reference implementation a single-precision run costs about as much as a double-precision one. On
mlxthe gain overnativegrows with the problem, and is largest for the envelope-grid plasma, whose cycle moments the GPU evaluates one sample per thread. On an otherwise idle MacBook Pro M5 Max (18 CPU cores, 40-core GPU), in the same process, on power-of-two grids and before the default grids and boundaries changed (SEP 21), the median time of a fixed step onnative(binary64) over that onmlx(binary32) was:case
native/mlxGNLSE with Raman, one line, 2¹⁴ / 2¹⁶ / 2¹⁸ samples
1.7 / 3.4 / 5.3
Ar capillary, field-grid plasma, 1 / 4 modes (300 fs) and 8 modes (1 ps)
0.51 / 0.69 / 1.12
the same with the envelope-grid plasma
8.1 / 8.1 / 3.5
radial Kerr beam, 64 / 256 nodes
1.05 / 2.9
Cartesian Kerr beam, 64 × 64 / 128 × 128
1.9 / 1.9
radial argon filament, field grid, 64 / 128 / 256 nodes
1.8 / 2.5 / 3.2
Small field-plasma capillaries are slower on the GPU, bound by its per-operation latency. These are same-process comparisons on one machine, not the standard benchmark suite, and they have not been repeated since: they indicate what a power-of-two grid gains, and do not hold on the default grids, where
mlxcan be slower thannative(grid sizes onmlx).mlxis experimental. Its custom Metal kernels run in continuous integration’s macOS default lane, which requires a usable Metal device. That lane does not measure their coverage or run the runtime type checks, and no CI pass selects MLX’s CPU device (the remaining CI gaps). Its survival limits are unmeasured beyond the two plasma cases above: the comb-to-continuum’s 0.05 m is the reference implementation’s, whose binary32 transforms (SciPy’s) are two to four times more accurate than MLX’s GPU transform (on an Apple M5 Max, against a binary64 transform: 2.0 to 2.6 times at 2¹⁴ to 2¹⁸ samples, 3.1 to 4.0 times at 12288 and 98304). A less accurate transform shortens the limit: the binary32 FFTW transforms of the withdrawn SEP 36 kept the case inside its envelopes to 0.025 m only. Power-of-two grids by default under single precision, a macOSmlxjob with coverage and type checks, and a re-timing on current grids come before it is fully supported.
The worked example runs a
capillary with the envelope-grid plasma and a radial filament in both precisions and
compares them. The precision setting, its binary32 contract, the survival study and the
mlx backend were proposed and carried out by
SEP 33.