Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/radau-axis-node-uncarried-row.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The on-axis plasma statistics on a Gauss-Radau field-scale rule read the whole axis row on a one-component transform ([#663](https://github.com/LupoLab/solprop/issues/663)): when the modes have an axis field in a component the transform does not carry, as a rotated mode on `components=("x",)` does, the rule's axis node no longer stands in for the axis row, so the statistic keeps that component (its `hypot` with y on a field grid, the polarisation ellipse on an envelope grid) at the cost of one row scan.
1 change: 1 addition & 0 deletions changelog.d/snapshot-short-interval.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A snapshot interval shorter than 60 s is honoured ([#600](https://github.com/LupoLab/solprop/issues/600)): the 60 s retry back-off now follows only a failed or deferred snapshot attempt, where every successful publication used to impose it too.
3 changes: 2 additions & 1 deletion docs/dev/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,8 @@ interval. The four on-axis plasma statistics are the plan's too: the axis row th
synthesise on the axis (`populations.axis_row_matrix`, the taper and the pad scale in it) is formed
from the modal state on the fine grid and scanned natively in the first node's buffer by the node
scan's own pass, its tables, preionisation and running-integral rule, or a Radau rule's axis node is
read when the evaluation scans the nodes anyway; the species reduction is finished natively. A
read when the evaluation scans the nodes anyway (`populations.axis_node`; never on a transform that
drops a component the modes have on the axis, whose node scan lacks that row); the species reduction is finished natively. A
one-component scan of a field grid reads both rows' magnitude, `hypot(E_x, E_y)`; an envelope's scan
reads the polarisation ellipse, so on a one-component transform whose modes have a y field on the
axis the two rows go to a row of their own, which the envelope scan reads as a node of two components
Expand Down
5 changes: 4 additions & 1 deletion src/solprop/backends/populations.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ def axis_node(

A Gauss-Radau field-scale rule (SEP 19) has one: its synthesis row is the mode
profile at ``r = 0``, the on-axis synthesis itself, so its own node scan supplies
the axis statistics without a further one.
the axis statistics without a further one. A transform that does not carry every
component the modes have on the axis has none: its node scan lacks that row.

Parameters
----------
Expand All @@ -251,6 +252,8 @@ def axis_node(
if transform.kind != "modal" or transform.modal_tables is not None:
return None
n_pol = transform.n_pol
if np.any(axis[n_pol:]):
return None
rows = transform.synthesis.reshape(transform.n_nodes, n_pol, -1)
scaled = axis[:n_pol] * problem.grids.pad_scale
for index, row in enumerate(rows):
Expand Down
1 change: 1 addition & 0 deletions src/solprop/output/snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ def _service(
else:
assert publication.path is not None
self._last_success = time.monotonic()
self._retry_at = 0.0 # Back off only after a failed or deferred try.
self._pending = self._requested != requested
logger.info(
"snapshot %s: %d bytes, %.6f s copying, %.6f s paused",
Expand Down
32 changes: 31 additions & 1 deletion tests/backends/test_on_axis_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ def argon(m=1, phi=0.0):
return MarcatiliMode(125e-6, "Ar", 1.0, m=m, cladding=1.45, loss=False, phi=phi)


def problem_on(grid, table, modes, rule, start=None, save_planes=(0.02,)):
def problem_on(
grid, table, modes, rule, start=None, save_planes=(0.02,), components=None
):
"""An argon capillary with Kerr and plasma on a given rule; nothing propagated."""
if start is None:
built = GaussianPulse(wavelength=800e-9, fwhm=10e-15, energy=ENERGY)
Expand All @@ -87,6 +89,7 @@ def problem_on(grid, table, modes, rule, start=None, save_planes=(0.02,)):
dz_max=0.002,
dz_init=1e-4,
quadrature=rule,
components=components,
)


Expand Down Expand Up @@ -193,6 +196,33 @@ def test_a_radau_axis_node_supplies_the_row_when_the_nodes_were_scanned(
assert counted == scans


@pytest.mark.parametrize("name", ["numpy_ref", "native"])
def test_a_radau_axis_node_is_declined_when_the_transform_drops_the_axis_y_row(
grid, table, name
):
"""A rotated mode on an x-only transform: its node scan lacks the axis y field.

The axis row carries both Cartesian components, so the node scan, which has only
the x row, must not stand in for it: the value is the row scan's, as on a Gauss
rule that has no axis node, and the Radau rule pays one scan for it.
"""
mode = argon(phi=math.pi / 3)
names = ("species_ionisation", *ON_AXIS_PLASMA_STATISTICS)
spec = StatsSpec(names=names)
radau = transverse_quadrature([mode], n_r=5, radial_rule="field_scale_radau")
problem, run_request = problem_on(grid, table, [mode], radau, components=("x",))
got, counted = backend_stats(name, grid, problem, run_request, [mode], spec)
gauss = transverse_quadrature([mode], n_r=5, radial_rule="field_scale")
problem, run_request = problem_on(grid, table, [mode], gauss, components=("x",))
want, _ = entrance_stats(grid, problem, run_request, [mode], spec)
assert counted == 1
assert float(want["on_axis_electron_density"]) > 0.0
for statistic in ON_AXIS_PLASMA_STATISTICS:
# The same row scan; the native plasma column against the reference's
# multipass scan keeps the node scans' parity tolerance.
np.testing.assert_allclose(got[statistic], want[statistic], rtol=1e-10)


@pytest.mark.parametrize("radial_rule", ["field_scale", "field_scale_radau"])
def test_an_axis_statistic_leaves_the_node_statistics_the_plan_s(
grid, table, radial_rule
Expand Down
17 changes: 17 additions & 0 deletions tests/output/test_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,23 @@ def test_elapsed_requests_and_explicit_retry_are_coalesced(working, monkeypatch)
assert latest is not None and latest.generation == generation


def test_interval_below_the_retry_backoff_is_honoured(working, monkeypatch):
output, controller = working
now = controller._last_success
monkeypatch.setattr(module.time, "monotonic", lambda: now)
controller._interval = 10
generations = []
for _ in range(2):
now += 11
controller._service(
output, None, next_save_index=0, require_history=False, persist=lambda: None
)
latest = controller.last_result
assert latest is not None and latest.disposition == "published"
generations.append(latest.generation)
assert generations[0] != generations[1]


def test_request_during_copy_remains_pending(working, monkeypatch):
_, controller = working
original = module._copy_generation
Expand Down
Loading