3. Time Synchronization and Lag Correction Between Test and CAE
Why Time Lag Matters in Test–CAE Correlation
In a correlation study, the test and CAE signals are supposed to describe the same physical event on the same time base. In practice, they rarely do. A test channel may be delayed by sensor group delay, anti-alias filter latency, or a trigger offset in the data acquisition system. A CAE response may be shifted because the solver output was written at a different reference instant, because an initial transient was trimmed inconsistently, or because the simulation event start does not coincide with the physical impact or excitation. Even a lag of a few milliseconds can destroy the alignment of peaks and valleys, and CORA metrics are extremely sensitive to that misalignment: a well-matched amplitude response with a 5 ms shift can score far worse than a slightly mismatched response that is perfectly aligned.
This case builds a small, self-contained workflow that (1) estimates the lag between a test and a CAE time history using cross-correlation, (2) applies the correction, and (3) quantifies how residual synchronization error propagates into a CORA-style score. The example uses synthetic signals so every number is reproducible and verifiable, but the same procedure applies directly to real channels.
Reference Convention and Example Signals
All signals in this case are defined on a common time vector t in seconds, sampled at fs = 1000 Hz. Positive lag means the test signal is late relative to the CAE signal; to correct it we shift the test signal earlier by that amount. We define a synthetic "truth" response as a damped oscillation, then create the test channel by adding a known delay and noise, and the CAE channel by adding a small amplitude error and a different delay. This gives us ground truth to check the estimator against.
import numpy as np
fs = 1000.0
t = np.arange(0, 2.0, 1.0 / fs)
# Ground-truth physical response: damped oscillation
f0, zeta = 25.0, 0.08
omega = 2 * np.pi * f0
truth = np.exp(-zeta * omega * t) * np.sin(omega * t)
# Test channel: delayed by +8 ms, plus noise
test_delay_s = 0.008
test = np.interp(t - test_delay_s, t, truth, left=0.0, right=0.0)
rng = np.random.default_rng(7)
test = test + 0.02 * rng.standard_normal(t.size)
# CAE channel: delayed by -3 ms, 10% amplitude error
cae_delay_s = -0.003
cae = 1.10 * np.interp(t - cae_delay_s, t, truth, left=0.0, right=0.0)
The true relative lag between test and CAE is test_delay_s - cae_delay_s = 11 ms. The estimator should recover approximately this value; the residual after correction should be small compared with the sampling interval.
Estimating Lag with Cross-Correlation
Cross-correlation finds the shift that maximizes the similarity between two signals. Because the raw correlation peak is biased by signal energy and by any DC offset, we first remove the mean and normalize. The lag in samples is the index of the maximum of the cross-correlation, converted to seconds by dividing by fs.
def estimate_lag(test, cae, fs):
a = test - test.mean()
b = cae - cae.mean()
# Full cross-correlation; lags run from -(N-1) to +(N-1)
corr = np.correlate(a, b, mode="full")
lags = np.arange(-len(a) + 1, len(a))
k = np.argmax(corr)
lag_samples = lags[k]
return lag_samples / fs, corr, lags
lag_s, corr, lags = estimate_lag(test, cae, fs)
print(f"Estimated lag: {lag_s*1000:.2f} ms")
With the example data the estimator returns a value close to 11 ms (the exact figure depends on the noise realization). Two practical refinements matter:
- Parabolic interpolation around the peak gives sub-sample resolution, which is important when the true lag is not an integer number of samples.
- Restricting the search window to a physically plausible range (e.g. ±50 ms) prevents the estimator from locking onto a spurious secondary peak caused by periodic content.
def refine_peak(corr, lags, k):
if 0 < k < len(corr) - 1:
y0, y1, y2 = corr[k - 1], corr[k], corr[k + 1]
denom = (y0 - 2 * y1 + y2)
if denom != 0:
delta = 0.5 * (y0 - y2) / denom
return lags[k] + delta
return lags[k]
k = np.argmax(corr)
lag_samples_refined = refine_peak(corr, lags, k)
lag_s_refined = lag_samples_refined / fs
print(f"Refined lag: {lag_s_refined*1000:.2f} ms")
Applying the Correction and Checking Residual Error
Once the lag is known, shift the test signal by the negative of the estimated lag so that it aligns with the CAE signal. Use interpolation rather than integer-sample shifting to preserve sub-sample accuracy. After correction, re-run the estimator; the residual lag should be near zero and, more importantly, the peak alignment should visibly improve.
def shift_signal(sig, t, lag_s):
return np.interp(t - lag_s, t, sig, left=0.0, right=0.0)
test_aligned = shift_signal(test, t, lag_s_refined)
residual_s, _, _ = estimate_lag(test_aligned, cae, fs)
print(f"Residual lag after correction: {residual_s*1000:.2f} ms")
A residual of a fraction of a millisecond is expected and acceptable. A residual that remains large (say, more than one or two sampling intervals) usually indicates one of the following:
- The lag is not constant — for example, a drifting clock or a frequency offset between test and CAE. Cross-correlation assumes a single rigid shift; a drift requires time-warping or segment-wise alignment.
- The signals contain strong periodic content that produces multiple correlation peaks of similar height. Restrict the search window or pre-filter to the dominant frequency band.
- The signals are dominated by noise or by a DC offset that was not removed before correlation.
How Residual Lag Propagates into the CORA Score
CORA evaluates multiple sub-criteria (progression, amplitude, phase, etc.) over defined intervals. Phase and progression are the most sensitive to residual lag. To make the effect concrete, we compute a simplified phase-style metric: the correlation coefficient between the aligned signals, and a peak-timing error, for several residual lags.
def phase_metric(a, b):
a = a - a.mean(); b = b - b.mean()
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
for extra_ms in [0, 1, 2, 5, 10]:
shifted = shift_signal(test_aligned, t, extra_ms / 1000.0)
r = phase_metric(shifted, cae)
print(f"Residual {extra_ms:>2} ms -> correlation {r:.4f}")
The correlation coefficient degrades monotonically as residual lag grows, and the drop is steepest for high-frequency content. In a full CORA scorecard, this translates into a lower phase sub-score and, depending on the weighting, a lower overall score. The practical lesson: correct the lag before tuning any other parameter, because amplitude and offset corrections cannot compensate for a timing error, and a scorecard computed on misaligned signals will mislead you about the true quality of the model.
Practical Checklist for This Step
| Check | Acceptance criterion | If it fails |
|---|---|---|
| Mean removed before correlation | Both signals zero-mean | DC offset biases the peak |
| Search window restricted | Within physically plausible lag range | Spurious periodic peak selected |
| Sub-sample refinement applied | Lag reported to < 1 sample | Residual of up to half a sample remains |
| Residual lag after correction | < 1 sampling interval | Check for drift or frequency offset |
| Correction documented | Lag value, method, and window recorded | Scorecard not reproducible |
Record the estimated lag, the refinement method, and the search window alongside the corrected signals. When the CORA scorecard is later reviewed, that record is what allows a reviewer to distinguish a genuine model deficiency from a synchronization artifact — and it is the difference between a score you can defend and one you cannot.