visualdynamics.core.replication¶
replication
¶
How closely a transient replicated the waveform it was controlled to.
A random test is judged against a specification that is already a statistic: the PSD is an average, and comparing it to another average is comparing like with like. A transient specification is not a statistic. It is one waveform, and the controller played it over and over, so the comparison is a different shape — the record has to be cut back into the repeats first, and then each repeat is compared against the same target.
Where the frames are is not guesswork and not a convention. Each repeat plays exactly the control signal, so the frame is the specification's length, the repeats are back to back, and the window is rectangular because any taper distorts the very thing being replicated. The one number that has to be measured rather than derived is how many repeats the record holds, and that is division.
None of this can be read out of the file. Rattlesnake's transient
environment saves the control signal, the channel indices, the ramp
time and the control-law wiring, and nothing about repeats at all —
repeat is a runtime instruction that never reaches the netCDF. What
the file does carry is a trap: a shelf of sysid_* attributes giving
a Hann window, half overlap and a 2048-sample frame. Those describe the
random excitation Rattlesnake runs beforehand to measure the FRF it
inverts. Read as the averaging parameters they are wrong three ways.
Three metrics, and they answer different questions:
waveform is ||m - s|| / ||s||, and it is the honest one. Amplitude,
phase and shape all move it, it is what the controller is minimising,
and it has no way to look good by accident.
srs asks whether the shock is equivalent, which is the question the
shock community actually asks. Two transients with the same SRS damage
the same hardware however little they resemble each other.
level is the scale on its own, in dB, with shape and phase divided
out. By Parseval it is the ratio of RMS values whether it is worked out
from the PSD or from the samples; it is stated as a PSD level because
that is how a level is read. Paired with waveform it separates "too
small" from "wrong", which one number cannot do.
TRAC and a complex-spectrum FRAC are deliberately absent. They are the same number: both are normalised inner products and the DFT is unitary, so Parseval makes a complex FRAC equal to TRAC to within the half-bin the real FFT keeps at DC and Nyquist. Reporting both would present one measurement as two pieces of evidence.
Functions:
| Name | Description |
|---|---|
averaging_for |
The averaging a transient run implies, or None if it cannot. |
playings |
Where each whole playing of the waveform sits. |
leftover |
What the record holds past the last whole playing. |
lag_of |
One integer sample shift aligning the record to the target. |
compare |
One row per control channel per repeat: how well each was |
event_slice |
One repeat of the control channels, on the target's own clock. |
Classes¶
Functions:¶
averaging_for
¶
averaging_for(
measured: TimeHistory, specification: DataArray
) -> Averaging | None
The averaging a transient run implies, or None if it cannot.
Everything but the count follows from the specification: the frame is its length, the repeats are back to back, and the window is rectangular. The count is how many whole ones the record holds.
Whole ones only. A record that stops part way through a repeat —
which is what a profile whose stop lands near a frame boundary
produces — carries that fraction, and a fraction of a frame is not
an average. Averaging will not hold it either, by design.
Source code in src/visualdynamics/core/replication.py
playings
¶
playings(
measured: TimeHistory,
specification: DataArray,
averaging: Averaging | None = None,
lag: int | None = None,
) -> list[dict[str, int]]
Where each whole playing of the waveform sits.
One list that everything else reads, so the plot, the grid, the
numbers and the shading on the time history cannot disagree about
how many playings there are or where they start. They did: an
earlier version counted a part-played one as a seventh event while
the shading, which comes from Averaging.frame_bounds, knew only
about whole frames. Two views of one record, giving two answers.
Whole ones only. A record rarely stops on a boundary — Rattlesnake repeats until it is told to stop, and streaming often ends before the environment does — so there is usually a fragment on the end. It is not analysed: a waveform error over part of a window is taken against a different stretch of the target and is not the same measurement as the ones beside it, which makes a column of them an invitation to compare things that do not compare.
Not analysed is not the same as not mentioned, which is where this
started. leftover says what was recorded past the last whole
playing so it can be reported rather than quietly dropped.
Source code in src/visualdynamics/core/replication.py
leftover
¶
leftover(
measured: TimeHistory,
specification: DataArray,
averaging: Averaging | None = None,
lag: int | None = None,
) -> tuple[int, float] | None
What the record holds past the last whole playing.
(samples, share of a playing), or None when it ends on a
boundary. Nothing is scored from it — see playings — but a run
that recorded two and a half seconds of a further event should say
so rather than have it vanish between a frame count and a file
size.
Source code in src/visualdynamics/core/replication.py
lag_of
¶
lag_of(
measured: TimeHistory,
specification: DataArray,
averaging: Averaging | None = None,
) -> int
One integer sample shift aligning the record to the target.
One, for every channel and every repeat together. The delay is a property of the acquisition, not of a channel, and letting each channel choose its own shift would let a poorly replicated one hunt for its most flattering alignment and report the error it found there. Estimated on the first repeat, over every control channel at once, by the shift that minimises total squared difference.
Source code in src/visualdynamics/core/replication.py
compare
¶
compare(
measured: TimeHistory,
specification: DataArray,
averaging: Averaging | None = None,
lag: int | None = None,
q: float | None = None,
frame: int | None = None,
metrics: Sequence[str] = METRICS,
) -> list[dict[str, Any]]
One row per control channel per repeat: how well each was replicated, and which playing of the waveform it was.
Every repeat is reported and none is singled out. An earlier
version reduced these to the worst repeat per channel and labelled
it so, which is a judgement rather than a measurement — what counts
as the bad event depends on which reading you care about and on
what the article is for, and the numbers are all here for the
reader to decide with. frame narrows the answer to one repeat
when the caller already knows which one it is asking about.
metrics narrows which readings are taken, and a row carries
only the ones asked for. This is not a micro-optimisation: the SRS
costs a ramp-invariant filter per band per cell, and a screen
showing a grid of waveform errors was spending two seconds an
update on sixty shock spectra nobody had asked to see.
Whole playings only — see playings. A fragment on the end of the
record is reported by leftover and scored by nobody.
A channel whose target is nothing — see ZERO_TARGET_DB — gets
NaN rather than a number, because every one of these metrics is a
ratio against that target.
Source code in src/visualdynamics/core/replication.py
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 | |
event_slice
¶
event_slice(
measured: TimeHistory,
specification: DataArray,
frame: int,
averaging: Averaging | None = None,
lag: int | None = None,
) -> TimeHistory | None
One repeat of the control channels, on the target's own clock.
Rebased onto the specification's abscissa rather than kept at its place in the record, because the comparison is with the target and the target starts at zero. The alignment is applied here, so the two are drawn where they are compared rather than a sample apart.
Only the control channels, in the specification's order, so the two line up record for record. None if that repeat is not in the record.