The time seam¶
longeron.widgets.time makes time a shared, linkable state across
every time-aware view. One Clock holds the playhead. link_time
subscribes the replay player, the mission globe, and the scrubber to
it. A scrub in one view then scrubs them all, with no echo. The
time-seam design states the full contract.
Clock, Timebase, and link_time need no extras. The scrubber
widget needs the replay extra (pip install "longeron[replay]").
The pieces¶
Clockholds the shared state:t,playing,rate, andspan. It owns no timer. Views animate; the clock fans state out.Timebasealigns one recording with its optional mission track, so every view plays the same execution.link_timewires views to the clock and returns anunlink()disposer, exactly likelink_selection.time_scrubberbuilds the transport bar: play/pause, rate, the time axis with event ticks and phase bands, and a telemetry readout.
Loss tolerance¶
Comm messages can be dropped under load, and the protocol has no
retransmit. The seam heals: kernel pushes carry generation stamps,
front-end reports acknowledge them (stale machine reports are rejected
and answered with a full re-push; user actions outrank a raced push),
and a trailing-edge verify re-states kernel truth after each traffic
burst. The kernel clock is the source of truth; a dropped report heals
by visible reversion, never a silent split. The
time-seam design
states the protocol; longeron.widgets._seam implements it.
Step-only traces¶
A pure event cascade records in step mode, and steps are not seconds.
The seam refuses the globe binding for such a trace by default.
seconds_per_step opts in: a scalar, or a per-step sequence/mapping
when steps take unequal durations. Durations the caller states count
as first-class. The scrubber labels only the synthesized gaps, with a
striped band and an explicit (x10 s) readout tag.
The time seam: one clock, many views (the temporal selection seam).
The diagram replay player, the Cesium mission replay, and any future time-aware view each expose a playhead as a widget trait. This module makes “when are we” one shared state with many subscribers, exactly as the selection seam made “what is selected” one state (see The time seam: one clock across the views (design), the adopted contract). Three toolkit pieces live here:
Clock– the shared playhead. A small kernel-side object with no front-end, no timer, and no dependencies: views animate, the clock holds state and fans it out. Seeks clamp into the span and coalesce within one JSON quantum (1e-3, the roundinglongeron.replay.Timeline.to_json()established), so every write settles at its first fixpoint – the selection seam’s no-echo discipline, restated for floats.Timebase– one recording, many views. It aligns aTimelinewith its optionalMissionTrackbinding, so every linked view plays the SAME recording. The axis is the timeline’s own: sim seconds, or the step index in step mode. Step-only traces have no time axis, so a step-mode timebase REFUSES a track unlessseconds_per_stepstates one – a scalar, or a per-step sequence/mapping when steps take unequal durations. Durations the caller states (for example from the model’s own time triggers or occurrence durations) count as first-class; only the synthesized gaps are labeled synthetic (Timebase.synthetic_intervals()).link_time()– the temporallink_selection. It attaches each view through a small adapter that knows the view’stimetrait and its axis mapping, wiresplaying/ratetraits where the view has them, and returns anunlink()disposer. A view holds ONE time link: linking it again unbinds the previous adapter first (thelongeron.analysis.link.bind_config_view()handle pattern).
time_scrubber() is the fourth piece and the first new subscriber:
a standalone transport bar (play/pause, rate, the time axis with the
recording’s event ticks and mission phase bands, a telemetry readout)
that subscribes to the clock like any other view. It renders alone
under a dashboard, beside views that have no transport of their own.
Non-fighting rule (the Cesium bridge, phase 2 of the design): while
playing, every animating view integrates t locally at the
shared rate and reconciles against the clock at ~4 Hz; a follower
snaps only when its local time drifts past a bounded tolerance
(0.25 axis units, scaled by the rate), and on pause every view
converges exactly. The scrubber’s front-end and the mission viewer’s
Cesium bridge (longeron.widgets.mission3d) both implement it.
The seam is LOSS-TOLERANT (longeron.widgets._seam): comm
messages get dropped under load and in-flight reports race kernel
seeks, so every kernel push carries a generation stamp, front-end
reports acknowledge the last stamp they saw, and the link REJECTS a
stale report (answering with an idempotent full-state re-push) instead
of letting it re-seek the clock. The kernel clock is the source of
truth; front-ends reconcile to it.
Everything but the scrubber’s front-end is pure kernel code: headless
tests drive clock.seek / clock.play and assert trait fan-out,
mirroring how the selection seam is tested without a browser. The
playhead deliberately does NOT join the selection seam (decision Q7 of
the design), and the clock owns no wall-clock timer: a headless
play() moves t only when a front-end or a test advances it.
Requires the replay extra (anywidget) for the scrubber widget only;
Clock, Timebase, and link_time need nothing.
- class longeron.widgets.time.Clock(span=(0.0, 0.0), *, step_mode=False, rate=1.0, t=None)[source]¶
Bases:
objectThe shared playhead for one linked group of views.
tis the playhead in axis units (sim seconds, or the step index whenstep_mode),playingsays someone is animating,rateis axis units per wall second (1.0 = real time; negative plays backwards, as Cesium’s shuttle ring does), andspanis the(t0, t1)window seeks clamp into. The clock owns no wall-clock timer: views animate, the clock holds state and fans it out through plain callbacks, so the core package stays dependency-free.The no-echo discipline is the selection seam’s, restated for floats: a
seek()within1e-3of the currenttdoes not fan out,playing/ratecoalesce on equality, and every subscriber applies the same rule before writing back, so each write settles at its first fixpoint. Linking is explicit and scoped (link_time()); two dashboards in one notebook keep two clocks.
- class longeron.widgets.time.Timebase(timeline, track=None, seconds_per_step=None)[source]¶
Bases:
objectOne recording, many views: a trace plus its optional mission binding, aligned on one axis.
timelineis the recorded truth (longeron.replay);trackis the optional globe binding, built FROM that timeline (longeron.analysis.mission3d.track_from_timeline()), so the two views replay one execution. The shared axis is the timeline’s own: sim seconds for a timed trace (track seconds are then the same numbers, the 1:1 mapping the design verified), or the step index in step mode.Step-only traces have no time axis, so a step-mode timebase refuses a
trackunlessseconds_per_stepstates one (a scalar, or a per-step sequence/mapping – seestep_seconds()); the same value must then have built the track. Stated durations count as first-class; only the synthesized gaps show up insynthetic_intervals(), which is what the scrubber labels.- property span: tuple[float, float]¶
The shared axis window:
(t_start, t_end), or(0, n_steps - 1)in step mode.
- env_at(t)[source]¶
The telemetry row at
t: the last scalar-env snapshot at or before it (step semantics, like the tracks);{}before the first.
- longeron.widgets.time.link_time(clock, *views, seconds_per_step=None)[source]¶
Wire time-aware views to one clock (the temporal
link_selection).Each
viewis any widget with atimetrait on the clock’s axis: the replay player, the mission viewer, the scrubber, or a future subscriber. The adapter observes the trait intoClock.seek()and fans clock changes back, both sides under the1e-3coalescing tolerance, so scrubbing one view scrubs them all and no write echoes. Views that also carryplayingandratetraits (the scrubber; the mission viewer’s Cesium bridge) get those wired the same way, and the clock’s current state fans out to every view at link time.The one non-identity mapping is the globe under a step-mode clock: steps are not seconds, so the binding is REFUSED unless
seconds_per_stepopts in (a scalar, or a per-step sequence/mapping matching the track’s own build – seestep_seconds()); the adapter then maps step positions through the stated durations, scalesrateto track seconds per wall second, and sizes the viewer’s drift tolerance to match.A view holds ONE time link; linking it again replaces the previous adapter. Returns an idempotent
unlink()that detaches every adapter, mirroringlink_selection.
- longeron.widgets.time.step_seconds(n_steps, seconds_per_step)[source]¶
Map a step-mode axis onto seconds.
A recording with
n_stepssteps hasn_steps - 1intervals (intervaliruns from stepito stepi + 1).seconds_per_stepstates their durations:a scalar – every interval lasts that many seconds, and every interval counts as SYNTHETIC (the tool fabricated the number);
a sequence – interval
ilastsseconds_per_step[i]; all stated, none synthetic (at leastn_steps - 1entries; extras are ignored);a mapping
{interval: seconds}– stated where present; gaps synthesize the 10-second default and count as synthetic.
Returns
(seconds, stated):seconds[k]is the second at stepk(seconds[0] == 0.0, one entry per step), andstated[i]tells whether intervali’s duration was stated by the caller. Every duration must be positive; a mapping key outside the interval range is refused loudly.
- longeron.widgets.time.time_scrubber(timebase, *, width_px=760)[source]¶
The standalone transport bar for a recording.
A play/pause button, a rate select, a slim slider over the timebase’s span with tick marks at the recorded transition instants (a density band above ~100 events), the mission phase bands where a track binding exists, a readout clock, and the scalar-telemetry line that follows the playhead. Step-mode recordings read
step k / N; where a seconds axis was stated per step the stated seconds show plainly and the synthesized segments carry an explicit(xN s)tag plus a striped band – a fabricated second is always displayed as fabricated.The scrubber is one subscriber among equals: pass it to
link_time()beside the replay player and the mission viewer. While playing it animates locally at the shared rate and syncs itstimetrait at ~4 Hz, exactly like its peers.Needs the
replayextra (anywidget).- Return type:
AnyWidget