visualdynamics.core.replication¶
replication
¶
How closely a transient test reproduced 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 it is compared with another average. A transient specification is one waveform, which the controller played over and over, so the record is first cut back into its repeats and each repeat is compared with the same target.
The frames follow from the specification. Each repeat plays exactly the control signal, so a frame is the specification's length, the repeats are back to back, and the window is rectangular, because any taper would change the waveform being compared. The one number that is measured rather than derived is how many repeats the record holds, the record's length divided by the frame's.
None of this can be read from 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: repeat is
a runtime instruction that never reaches the netCDF file. The file does
carry a set of sysid_* attributes giving a Hann window, half overlap
and a 2048-sample frame. Those describe the random excitation run
beforehand to measure the FRF the controller inverts, and as averaging
parameters for the transient all three are wrong.
Three metrics are reported, and they answer different questions:
waveformis ||m - s|| / ||s||, in percent. Amplitude, phase and shape all change it, it is what the controller minimizes, and it cannot look good by accident.srsis the largest deviation of the measured SRS from the target's, in dB, signed. It asks whether the shock is equivalent: two transients with the same SRS damage the same hardware however little they resemble each other.levelis the scale alone, in dB, with shape and phase left out. It is the ratio of the mean squares, which by Parseval is the ratio of the two PSDs' totals, so it reads as a PSD level. Paired withwaveformit tells "too small" from "wrong", which one number cannot do.
TRAC and a complex-spectrum FRAC are left out on purpose. They are the same number: both are normalized inner products and the DFT is unitary, so by Parseval a complex FRAC equals TRAC to within the half-bin the real FFT keeps at DC and Nyquist. Reporting both would present one measurement as two.
Functions:
| Name | Description |
|---|---|
averaging_for |
The averaging a transient run implies, or None if there is none. |
playings |
Where each whole playing of the waveform sits in the record. |
leftover |
What the record holds past the last whole playing, as |
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 there is none.
Everything but the count follows from the specification: the frame is its length, the repeats are back to back with no overlap, and the window is rectangular. The count is how many whole repeats the record holds. A record that stops partway through a repeat (a profile whose stop lands near a frame boundary does this) leaves that fraction out, since a fraction of a frame is not an average.
When the record already carries an averaging with the specification's frame length, such as one set in the averaging panel, that averaging is used instead, so narrowing the analysis there narrows the comparison too. None when the specification has fewer than two samples or the record is shorter than one repeat.
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 in the record.
Returns one dict per playing: frame (its index), start (its
first sample, with the alignment lag applied) and samples (its
length). The plot, the grid, the numbers and the shading on the
time history all read this list, so they agree on how many playings
there are and where they start. averaging defaults to
averaging_for, and lag to lag_of.
Only whole playings are listed. A record rarely stops on a
boundary, since Rattlesnake repeats until it is told to stop and
streaming often ends before the environment does, so there is
usually a fragment at the end. The fragment is not analyzed: a
waveform error over part of a frame is taken against a different
stretch of the target, and does not compare with the ones beside
it. leftover reports what was recorded past the last whole
playing.
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, as
(samples, fraction 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. The
fragment is measured against how many whole playings the record
could hold, not how many are being analyzed. averaging and lag
default as in playings.
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, used for every channel and every repeat.
The delay belongs to the acquisition, not to a channel, and letting
each channel choose its own shift would let a poorly replicated one
find its most favorable alignment and report the error there. The
shift is the one that minimizes the total squared difference over
the first frame of the analysis, across every control channel at
once. It is searched up to LAG_FRACTION of a frame (2 %) either
side of where the analysis starts, and never before the record's
first sample, so an analysis that starts at 0 is searched forward
only. 0 when there is nothing to align.
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.
Each row is a dict with label (the channel's DOF), frame (the
playing's index), zero_target, and one entry per metric asked
for: waveform in percent, srs and level in dB. Every repeat is
reported and none is singled out, because which event counts as the
bad one depends on which reading matters and on what the article is
for. frame narrows the answer to one repeat.
metrics picks which readings are taken (any of METRICS), and a
row carries only those; an unknown name raises ValueError. The SRS
costs a ramp-invariant filter per band per cell, about 70 ms against
under 1 ms for the other two, so a caller that does not need it
should leave it out. q is the SRS quality factor (srs.DEFAULT_Q
when None), and averaging and lag default as in playings.
Only whole playings are scored (see playings); a fragment at the
end of the record is reported by leftover. A channel whose target
is more than ZERO_TARGET_DB (120 dB) below the loudest control
channel's gets NaN for every metric, with zero_target True,
because every metric is a ratio against the target. A control
channel with no record at its DOF is left out.
A record and its target are compared by compliance.units_check.
A control channel recorded only as another quantity, or with only
one side's units declared, gets a row per playing with every metric
NaN and refused saying why, so it shows as not scored instead of
vanishing. With neither side's units declared the rows are scored
and carry note (compliance.RAW_NOTE).
Source code in src/visualdynamics/core/replication.py
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 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 | |
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, as a TimeHistory.
The repeat is moved onto the specification's abscissa, which starts
at zero, instead of keeping its place in the record, and the
alignment (lag) is applied, so the two are drawn where they are
compared. Only the control channels are kept, in the
specification's order, so the two line up record for record. None
when that repeat is not in the record, or none of the control
channels is.