Execution resources#

Budget memory before a large propagation or scan. The state size grows with time samples, spatial nodes, modes and polarisations; saved planes can add a substantial output cost. Choose an adequate model and resolution first, then select a backend and thread settings suitable for the machine.

Backend choice and scan workers#

backend="auto" resolves to the native backend when installed and to Numba otherwise. It does not silently retry another backend if the chosen one refuses a representation. Use an explicit name for a scan spanning machines with different installations. The resolved backend and runtime settings are recorded with the output.

Thread and runtime settings must be in place before the relevant libraries load. Avoid giving every process all CPU threads: budget workers and threads together. See the scan executor guidance and tuning recipes.

Memory#

The general modal route holds dense frequency-dependent maps and the guide’s stored E/H reference for the whole run: \(16\,Q\,C\,M\,n_\omega\) bytes per map, for \(Q\) nodes, \(C\) components, \(M\) channels and \(n_\omega\) frequency bins, up to fourteen such maps on a constant guide. That grows quickly: the graded-index worked example (412 nodes, four channels, 2049 bins) holds 2.3 GB. A coherent wall adds the flow’s per-section matrices.

Before a backend allocates anything, the driver estimates the run’s persistent bytes from the same formulas the backends’ ledgers use (estimate_memory(), equal to each ledger to the byte) and admits the run against a budget (admit_memory()). Undeclared, the budget is four fifths of the memory available — the smallest of the physical memory, a Linux cgroup limit and a Slurm allocation — and a run over it is warned about; a declared budget refuses a run over it, unless it is prefixed warn:. One case is refused even undeclared: a run that would fit but for the Adams method’s history is refused before it starts, naming RungeKutta(), which keeps none (choosing a stepper); warn: or "none" lets it try anyway. A budget is a byte count or a number with a unit ("16 GiB", "12e9", "64 GB"), or "none" for no check. Declare it for one run with Simulation.run(memory_budget=...), or for every run in a process, including the prop_* functions and scan workers, through the environment:

from solprop.driver import Simulation

simulation = Simulation.from_capillary(
    125e-6,
    1e-3,
    "He",
    1.0,
    lambda0=800e-9,
    wavelength_limits=(200e-9, 2e-6),
    trange=100e-15,
    fwhm=10e-15,
    energy=1e-9,
    n_save=2,
    backend="numba_cpu",
)
budgeted = simulation.run(memory_budget="warn:8 GiB", progress="none")
print(budgeted.metadata.backend.settings.get("memory"))
SOLPROP_MEMORY_BUDGET="64 GiB" uv run python my_run.py   # or "warn:64 GiB", or "none"

The prop_* functions have no memory_budget= keyword, so the environment variable is their only way to declare a budget; likewise field retention (fields=, field_planes=) is set on SimulationSettings or through run_scan(), not as a prop_* keyword.

The estimate is recorded in the file’s backend record under memory, with its groups (base, general_maps, general_reference, general_chunk, coupled_flow, adams_history, …), so you can see which one to shrink: fewer channels or nodes for the maps and the reference, a narrower frequency window for all of them. Nothing is ever approximated to fit. Not counted, and named on every estimate: FFTW’s planning heap and per-stage transients.

The output. A run kept in memory (output=None or a MemoryOutput) holds every plane it saves, allocated at the start for the entrance and every save plane, so the admission adds it as the group output: the field, the noise phase and the statistics of each plane (plane_bytes()). 201 saved planes of a large grid is not small; fewer planes (n_save) or a file (output="run.h5"), which writes each plane out and is charged nothing, shrink it. The step history, which grows with the steps taken, is not counted.

The propagator table. propagator_mode on SimulationSettings decides whether the stage propagators are kept for the whole step ("tabulate") or recomputed in each stage ("recompute"), which changes the memory held in the base group, not the method. Its default, "auto", leaves the choice to the backend and records what was resolved in the backend record: numpy_ref tabulates, numba_cpu and native recompute, and a free-space or planar run always recomputes and refuses "tabulate". When base is what does not fit, propagator_mode="recompute" is one of the remedies the refusal names.

Operators tabulated along z. A linear operator or nonlinear prefactor that varies along z but cannot be expanded in the mode set’s scalar parameter paths is tabulated along z instead (varying operators). On a large multimode set that table can run to gigabytes, and, like the problem’s other tables, it is not in the estimate. numpy_ref and numba_cpu run it; native refuses it (backends).

A general modal guide is admitted twice. Its route — the dense reference and the two execution maps, nearly all of a large guide’s memory — is built when the simulation is constructed, before run can see a translated problem. So from_waveguide() admits the guide first (admit_guide_memory()), under the same budget and the same refuse-or-warn rule: general_reference, general_maps and general_chunk are estimated from the guide’s declared shape alone — its channels, its node rule’s nodes, the grid’s bins and time samples, a moving guide’s sections and degree (estimate_guide_memory()) — and equal to the byte what the translated estimate later finds for those groups. A guide over a budget declared to refuse is refused before any of it is allocated; from_waveguide(memory_budget=...) declares that budget, and otherwise SOLPROP_MEMORY_BUDGET or the machine’s default applies. The whole run is admitted again by run.

The estimate’s groups, the backends’ memory ledgers and the formulas behind them are in the backend design.