feat: replay live PPS telemetry in fixed-point model

This commit is contained in:
2026-07-29 00:16:43 +03:00
parent a697284bff
commit 9b0828dbaa
10 changed files with 3019 additions and 12 deletions
+82
View File
@@ -404,6 +404,88 @@ class FixedPointAdaptivePps:
def _phase_q32_to_seconds(value: int) -> float:
return value / (TRACKING_HZ * Q32)
def seed_tracking_state(
self,
*,
time_s: float,
profile: Profile,
phase_error_s: float,
rate_command_ppm: float,
integrator_ppm: float,
software_error_s: float,
software_bias_m: float,
software_clock_drift_mps: float,
software_previous_measurement_m: float,
reference_id: int,
) -> None:
"""Start a replay from a captured, already-locked board snapshot.
Normal simulations deliberately start unaligned and exercise coarse
acquisition. Hardware logs, however, are commonly captured after the
board has been tracking for minutes. Replaying such a log from reset
introduces an artificial acquisition transient and clears the PI
integrator that was present on the board. This method imports only
state that the production diagnostic registers and firmware status
expose; subsequent updates still use the normal bit-accurate path.
The method must be called before any observation has been recorded.
It is a model facility and has no firmware/RTL side effect.
"""
finite_values = (
time_s,
phase_error_s,
rate_command_ppm,
integrator_ppm,
software_error_s,
software_bias_m,
software_clock_drift_mps,
software_previous_measurement_m,
)
if self.trace.time_s:
raise ValueError("tracking state must be seeded before replay")
if not all(math.isfinite(value) for value in finite_values):
raise ValueError("tracking state values must be finite")
if time_s < 0.0:
raise ValueError("tracking state time must be non-negative")
if profile == Profile.HOLDOVER:
raise ValueError("a locked snapshot cannot use holdover profile")
self.current_time_s = time_s
self.raw_tick_remainder = 0.0
self.phase_q32 = self._seconds_to_phase_q32(phase_error_s)
self.profile = profile
self.selector.profile = profile
self.selector.candidate = profile
self.selector.candidate_updates = 0
self.aligned = True
self.locked = True
self.lock_good_updates = 0
self.reference_watchdog_holdover = False
self._select_loop_parameters()
self.integrator_q32 = self._clamp(
round(integrator_ppm * 1.0e-6 * Q32), self.max_slew_q32
)
self.rate_word_q32 = self._clamp(
round(rate_command_ppm * 1.0e-6 * Q32), self.max_slew_q32
)
self.software_error_s = software_error_s
self.software_bias_m = software_bias_m
self.software_clock_drift_mps = software_clock_drift_mps
self.software_previous_measurement_m = (
software_previous_measurement_m
)
self.software_drift_anchor_time_s = time_s
self.software_drift_anchor_measurement_m = (
software_previous_measurement_m
)
self.software_drift_initialized = True
self.software_updates = max(
1, self.parameters.software_settle_updates
)
self.software_reference_id = reference_id
self.last_observation_time_s = time_s
def _apply_profile(self, profile: Profile) -> None:
self.profile = profile
self._select_loop_parameters()