Skip to content

ssa: count the run's clock from a far t_start, and read held rates inside their interval (#1059) - #1381

Open
Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/1059-ssa-elapsed-clock
Open

Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/1059-ssa-elapsed-clock

Conversation

@Montana

@Montana Montana commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Closes #1059 (the SSA/PSA half; see "Not in this PR").

What was wrong

#1075 fixed the two discrete SSA loops, which draw a waiting time from a constant total propensity and now carry what t + tau rounds away. The continuous loop never got that fix. That is the loop for a model whose rate reads the clock, or that has rate rules. It integrates the propensity over error-controlled panels in absolute time. Near t = 1e12 the doubles are 1.2e-4 apart, and the panel step floor there, 64·eps·|t|, is 1.4e-2. A firing placed at tp + th·hh loses what is below that spacing.

A decay of 1e5 copies, k*A*(1 + 1e-30*time()) at k = 0.5 (it reads the clock but does not move with it). The exact mean at t0 + 2 is 36788. Ten seeds, on main c0877fa:

t_start mean z
0 36809 +0.4
1e9 36809 +0.4
1e12 22829 −289

A second failure on main, found while testing this, has the same cause. A rate held constant between breakpoints, k*A*piecewise(1, time() > t0 + 1, 0), is switched on at the breakpoint. The refresh reads it at the interval's midpoint, correctly. But after each firing the propensities are updated at the firing's time, and from 1e12 a firing just past t0 + 1 rounds back onto t0 + 1. There the condition is still false, so the rate went back to 0 after its first firing: 99999 of 100000 left, where e⁻¹ of them should be.

The fix (src/ssa_simulator.cpp)

The run keeps its own clock as time elapsed since t_start. It hands the model origin + elapsed wherever the model reads the clock, so panels, step floors and waiting times are as fine at 1e12 as at 0. Every change is inside run_internal (the SSA and PSA body):

  • The origin. ssa_clock_origin(times) is t_start where |t_start| >= 2^20, where the doubles are 1.2e-10 apart, and 0 below. The run's TimeSpec (t_start, t_end, sample_times) and its breakpoints and time-table knots are shifted onto the run's clock.

  • The clock reads. All 19 model.evaluate_functions(x) and 9 model.set_current_time(x) calls in the body read origin + x. At origin 0, 0.0 + x == x for every finite x, so every run below 2^20 is the run it was, to the bit. No call site needs judging individually.

  • Rows are given the exact times asked for afterwards, through a new Result::set_time, not origin + elapsed.

  • What goes to the next run:

    • the model's time and the event carry's leg end are the exact t_end;
    • pending executions are shifted back to absolute times;
    • the carry a run continues from is looked up, and its pending executions shifted, the same way.

    origin + (t_end − origin) is t_end whenever t_end <= 2·t_start (Sterbenz), but not for a longer run, which a test covers.

  • Error messages that print a time print the absolute one.

  • A held rate is read where pc_refresh_at read it, inside its interval, where the origin has moved. That is the second failure above.

Tests (test_ssa_elapsed_clock.py, 7)

test on main
test_a_decay_from_a_far_start_keeps_its_rate[continuous loop (reads the clock)]: t0 = 1e12, 10 seeds, |z| < 5 fail (z = −289)
…[discrete loop (#1075)]: the loop #1075 fixed, still right pass
test_a_rate_that_switches_at_an_absolute_time_switches_there: no decay before t0 + 1, then e⁻¹ (breakpoints on the run's clock; the held rate) fail (99999)
test_an_event_at_an_absolute_time_fires_there: the trigger reads the absolute clock pass
test_the_rows_are_the_times_asked_for: result.time == sample_times exactly at 1e12 pass
test_a_run_continues_from_where_the_last_one_ended: a delayed event pending across two runs applies at t0 + 3 in the second pass
test_a_long_run_hands_on_its_exact_end: t_end > 2·t_start, where t0 + (te − t0) != te; the next run still finds the pending execution pass

Four pass on main because they check what must not regress under the new clock.

Mutations (C++ rebuilt for each)

mutant fails
M1: never move the origin the continuous-loop decay and the switching rate
M2: breakpoints not shifted the switching rate
M3: a pending execution not shifted back at write-out the continuation
M4: the carry's leg end as origin + elapsed the long-run continuation. It first survived: for t_end ≤ 2·t_start it is exact, which is why the long-run test exists
M5: rows at elapsed times the rows test and the continuation
M6: the held rate read at the firing's time the switching rate

Measured

  • PSA goes through the same body. 10 seeds give a mean of 38579.4 from t0 = 0 and from 1e12 alike.
  • Full suite (macOS arm64, main c0877fa + this branch): 16,423 passed, 0 failed, 137 skipped.
  • Below the threshold, against main, bit for bit: 5 models (a discrete decay, a clock-reading rate, a held switch, a rate rule, an event) × t_start 0, 5, 1e5 and 1048575 (= 2^20 − 1) × SSA and PSA, at one seed: 40 of 40 identical to main's core, times and states.
  • pre-commit run --all-files (new files staged; clang-format included) and sphinx -W with docs/requirements-ci.txt are clean.

Not in this PR

…side their interval (lanl#1059)

lanl#1075 fixed the discrete loops; the continuous loop (a rate that reads
the clock, or rate rules) still placed its firings on panels in absolute
time, and from t_start = 1e12 a decay came back at z = -289. A rate held
between breakpoints also went back to 0 after its first firing, the
firing's clock rounding onto the breakpoint. Above |t_start| = 2^20 the
run now counts its clock from t_start and hands the model origin +
elapsed where it reads the clock; rows get the times asked for
(Result::set_time), and the state and event carry the next run reads
keep their exact absolute times. At origin 0 every run is bit-identical.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSA, and rm on a model that reads the clock, lose waiting times to rounding at a large absolute t_start (z = -287 at 1e12, silent)

1 participant