feat: replay live PPS telemetry in fixed-point model
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user