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 rtol below 1e-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-6 on prop_gnlse (whose binary64 default is 1e-7) and prop_capillary, 1e-5 on prop_waveguide, prop_radial and prop_planar, and 1e-4 in free space (its record). A spatial interface applies it where its SimulationSettings give no controller; an explicit rtol or 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 under RungeKutta() a replayed step (or an adaptive run’s floor dz_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. On mlx an adaptive RungeKutta() 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 with LowStorageRungeKutta() for this reason.

  • The integrator. Each interface takes its binary64 default (the defaults): Multistep() in its exponential formulation on prop_capillary, prop_radial and prop_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 noise resolves 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#

numpy_ref

mlx

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 Batch

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 1e-6: field

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 from 1e-5 to 1e-6 still 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 1e-4

binary32 at 1e-4

binary64 at 1e-5

binary32 at 1e-5

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 mlx the gain over native grows 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 on native (binary64) over that on mlx (binary32) was:

    case

    native / mlx

    GNLSE 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 mlx can be slower than native (grid sizes on mlx).

  • mlx is 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 macOS mlx job 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.