Source code for solprop.stepper.low_storage
# SPDX-FileCopyrightText: 2026 John C. Travers and contributors
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""The low-storage interaction-picture Runge-Kutta march as a run's integrator (SEP 24).
A third integrator beside :class:`solprop.stepper.multistep.Multistep` and
:class:`solprop.stepper.runge_kutta.RungeKutta`: the classical fourth-order Runge-Kutta
method in the interaction picture anchored at the step's midpoint, Lawson's
transformation [1]_ in Hult's arrangement [2]_ (RK4IP). It replays a prescribed
:class:`~solprop.stepper.schedule.FixedSchedule` and holds three field-sized registers
beside the source pipeline's scratch: the state, overwritten in place by its
interaction-picture image and then by the accepted step, one accumulator and one stage
register. It has no embedded error estimate; its accuracy is calibrated from outside,
on the measured observable, by :func:`~solprop.stepper.schedule.calibrate`
(``docs/guide/numerics/fixed-schedules.md``).
This module holds the specification a run carries. The march and its register passes
are in :mod:`solprop.backends.low_storage`.
References
----------
.. [1] J. D. Lawson, "Generalized Runge-Kutta processes for stable systems with large
Lipschitz constants", SIAM Journal on Numerical Analysis 4, 372 (1967).
doi:10.1137/0704033
.. [2] J. Hult, "A fourth-order Runge-Kutta in the interaction picture method for
simulating supercontinuum generation in optical fibers", J. Lightwave Technol.
25, 3770 (2007). doi:10.1109/JLT.2007.909373
"""
from dataclasses import dataclass
from typing import Literal
from solprop.exceptions import StepperError
__all__ = [
"LOW_STORAGE_REAL_INTERVAL",
"LowStorageRungeKutta",
"LowStorageScheme",
]
LowStorageScheme = Literal["rk4ip"]
"""The register schemes :class:`LowStorageRungeKutta` steps with.
``"rk4ip"`` is classical RK4 in the midpoint interaction picture, four source
evaluations a step and no estimate (SEP 24 section 4.2).
"""
LOW_STORAGE_REAL_INTERVAL = 2.785
"""Negative-real-axis stability interval of classical RK4, rounded down.
RK4's stability function is ``R(x) = 1 + x + x**2/2 + x**3/6 + x**4/24``; on the
negative real axis ``|R(x)| <= 1`` for ``x >= -2.7853`` (the root of ``|R| = 1``), so a
damping rate integrated explicitly -- the in-stage collars, plasma loss -- must keep
``h alpha_max / 2`` at most 2.785, against 3.3 for DOPRI5
(:data:`solprop.boundaries.STABILITY_RADIUS`). Checked against the stability function in
``tests/stepper/test_low_storage.py``.
"""
[docs]
@dataclass(frozen=True)
class LowStorageRungeKutta:
"""RK4 in the midpoint interaction picture, replaying a schedule in three registers.
The integrator for a fixed-step run whose state is too large for the Runge-Kutta
march's stage rows or the Adams history: a Cartesian free-space grid of gigabytes,
an ensemble or a scan on one schedule. Each step makes four source evaluations and
accepts its result in place, so the march needs a schedule
(``FixedSchedule.uniform`` for plain fixed steps) and records no error estimate:
the step history's norm column is 0.0, meaning "not estimated". Calibrate the
schedule on the observable that matters with
:func:`~solprop.stepper.schedule.calibrate` at ``order=4``
(``docs/guide/numerics/choosing-a-stepper.md``).
Attributes
----------
scheme : {'rk4ip'}
The register scheme; ``"rk4ip"``, the only one, is Hult's RK4IP.
Raises
------
StepperError
If the scheme is not one of :data:`LowStorageScheme`.
Examples
--------
>>> from solprop.stepper.low_storage import LowStorageRungeKutta
>>> LowStorageRungeKutta()
LowStorageRungeKutta(scheme='rk4ip')
>>> LowStorageRungeKutta().name, LowStorageRungeKutta().order
('low-storage-rk4ip', 4)
"""
scheme: LowStorageScheme = "rk4ip"
def __post_init__(self) -> None:
"""Check the scheme.
Raises
------
StepperError
Naming the scheme asked for.
"""
# The Literal annotation refuses an unknown scheme first wherever the
# annotations are checked at run time, as in the test suite; this check holds
# where they are not.
if self.scheme != "rk4ip":
msg = f"scheme must be 'rk4ip', got {self.scheme!r}"
raise StepperError(msg)
[docs]
@property
def name(self) -> str:
"""Return the method's record name.
Returns
-------
str
``low-storage-<scheme>``.
"""
return f"low-storage-{self.scheme}"
[docs]
@property
def order(self) -> int:
"""Return the scheme's classical order, the ``order`` to calibrate at.
Returns
-------
int
Four.
"""
return 4
[docs]
@property
def evaluations(self) -> int:
"""Return the source evaluations a step makes.
Returns
-------
int
Four: no slope carries over, because the fourth is taken at a predicted
state, not at the accepted one.
"""
return 4
[docs]
@property
def real_stability_interval(self) -> float:
"""Return the negative-real stability interval for a damping term.
Returns
-------
float
:data:`LOW_STORAGE_REAL_INTERVAL`: the largest ``h |lambda|`` for a damping
rate ``lambda`` integrated explicitly.
"""
return LOW_STORAGE_REAL_INTERVAL