visualdynamics.core.compliance¶
compliance
¶
How a measured response compares with what it was controlled to.
A specification and the PSD that answered it are the comparison a random vibration test exists to make, and looking at the two curves says only so much. What a report needs is numbers: how much energy was asked for, how much arrived, and how much of the band strayed outside the bands the controller was told to warn and abort on.
The two rarely share a frequency axis. A specification is written at a handful of breakpoints, or on the controller's lines rather than the analysis's, so it is interpolated onto the measurement's lines — in log-log, because a specification is drawn that way and read that way, and a straight line between two breakpoints on log axes is a power law. Interpolated linearly instead, a decade-wide segment runs several dB above the specification through the middle of its own span.
Only the measurement's own lines are counted, and only those inside the specification's band. Outside it the specification says nothing, and a response there is neither passing nor failing.
At the two ends of that band a line is usually half in and half out, and neither answer is right: counted whole it credits the response with power the specification never asked for, dropped it throws away power that was asked for. So a line at an end is compared over the part of its own bin that the specification covers — the same half-bin the plot shades — which is also what makes an RMS error the comparison of two areas over exactly the same stretch of frequency.
Functions:
| Name | Description |
|---|---|
log_interpolate |
A specification's values at |
log_log_area |
The area under the power law through these points, exactly. |
written_band |
(low, high) one written curve actually says something over. |
band_of |
(low, high) the specification actually says something over. |
covered |
Each measured bin cut to the part of it a band covers. |
outside |
Which measured lines fell outside one written limit curve. |
specification_rms |
A specification's RMS, from its own points and nothing else. |
rms |
The RMS a PSD carries over the lines given: sqrt of its total. |
bounds |
(lower, upper) of one pair of limits, on |
significant_band |
|
detect_scale_db |
The whole-dB offset that best lays the measurement on the |
comparison_scale_db |
The decibels every comparison adds to |
matched_records |
[(label, spec record, measured record)] for the channels the two |
exceedances |
(over, under): which lines went outside a pair of limits, and how. |
compare |
How one measured record answers one record of a specification. |
channel_errors |
[(label, dB out, percent of lines outside abort)] for a bar chart. |
outside_fraction |
What share of these fell outside the threshold, as a percentage. |
compare_all |
[(label, result)] for every channel the two have in common. |
srs_errors |
[(label, event, RMS dB deviation)] for a shock spectrum against |
signed_rms_db |
The RMS of the line-by-line deviation in decibels, signed by |
sine_errors |
[(dof, tone, RMS dB deviation)] for extracted sine levels |
Classes¶
Functions:¶
log_interpolate
¶
log_interpolate(
frequencies: ArrayLike,
spec_frequencies: ArrayLike,
spec_values: ArrayLike,
) -> ndarray
A specification's values at frequencies, interpolated log-log.
NaN wherever the specification does not reach. Outside its band it says nothing, and extending the end segments would invent a requirement nobody wrote; a line written at zero — which is how a controller writes one outside its band — says nothing either.
Source code in src/visualdynamics/core/compliance.py
log_log_area
¶
log_log_area(
frequencies: ArrayLike,
values: ArrayLike,
low: float | None = None,
high: float | None = None,
) -> float
The area under the power law through these points, exactly.
A specification's points are breakpoints of a continuous curve, and the curve between two of them is the straight line they make on log axes — which is a power law, W = C f**n. Its integral has a closed form, so there is nothing to approximate: no grid, no rule, and no dependence on how finely anything else happened to be measured.
For a segment from (f1, W1) to (f2, W2), n is the slope in log-log and the area is
W1 f1 ln(f2/f1) when n is -1
W1 / f1**n * (f2**(n+1) - f1**(n+1)) / (n + 1) otherwise
low and high clip the band. A segment straddling an edge is cut
there and its value at the cut taken from the same power law, so a
comparison over part of a specification integrates exactly that
part.
Source code in src/visualdynamics/core/compliance.py
written_band
¶
(low, high) one written curve actually says something over.
Source code in src/visualdynamics/core/compliance.py
band_of
¶
band_of(
specification: Specification, record: int = 0
) -> tuple[float, float] | None
(low, high) the specification actually says something over.
covered
¶
Each measured bin cut to the part of it a band covers.
A line stands for its whole bin, half a width either side, and at the ends of a specification a bin hangs over the edge. Returns (start, stop, width, cut): where each bin lies inside the band, how wide that is, and which bins the edge went through.
A bin wholly outside has zero width and is neither cut nor covered.
Source code in src/visualdynamics/core/compliance.py
outside
¶
outside(
lines: ArrayLike,
values: ArrayLike,
spec_frequencies: ArrayLike,
limit_values: ArrayLike,
over: bool = True,
reading: str = "log_log",
) -> ndarray
Which measured lines fell outside one written limit curve.
In the middle of the band this is the plain comparison: the density measured on a line against the density the limit asks for there.
At the two ends it cannot be. A bin straddling the edge is covered
by the specification for part of its width and by nothing for the
rest, and reading the limit at the bin's centre either invents a
requirement past the edge or drops a bin that is mostly inside. So
a cut bin is judged on the part that is covered: the power the
measurement holds over that stretch, G times the covered width,
against the power the limit asks for over the same stretch, which
is the exact log-log area under it. Both sides then span one
identical piece of frequency, which is the only way the comparison
means anything at an edge.
Source code in src/visualdynamics/core/compliance.py
specification_rms
¶
specification_rms(
specification: Specification,
record: int = 0,
low: float | None = None,
high: float | None = None,
) -> float
A specification's RMS, from its own points and nothing else.
Not from whatever grid a measurement happened to be computed on: the level a specification asks for is a property of the specification, and it should not move in the fourth decimal because somebody changed a frame length.
The integral is the object's, so it is taken the way the object is
drawn. This used to reach straight for log_log_area, which is
right for a specification written at breakpoints and wrong for one
computed from a record — and the second kind exists: the PSD of a
transient target is a Specification of four thousand density
lines.
Source code in src/visualdynamics/core/compliance.py
rms
¶
The RMS a PSD carries over the lines given: sqrt of its total.
Each line times the width of its own bin, summed. A discrete spectrum is a density per bin, so this is the total the lines actually hold — Parseval's, exactly — where a trapezoid halves the two end bins and reads a little under.
The widths come from the whole axis before any line is dropped, so a line that says nothing contributes nothing rather than having its bin quietly widened onto its neighbours. Bridging a gap would be assuming what is in it.
Source code in src/visualdynamics/core/compliance.py
bounds
¶
bounds(
specification: Specification,
record: int,
pair: str,
frequencies: ArrayLike,
) -> tuple[ndarray | None, ndarray | None]
(lower, upper) of one pair of limits, on frequencies.
None for a limit the specification does not carry — nothing was exceeded there because nothing was asked.
Source code in src/visualdynamics/core/compliance.py
significant_band
¶
good, narrowed to where the specification has real content.
Lines within DETECTION_RANGE_DB of this channel's own peak. The
peak rather than a fixed level, because a specification's units
and size are its own; per channel rather than across them, because
channels are bounded at their own levels.
Source code in src/visualdynamics/core/compliance.py
detect_scale_db
¶
detect_scale_db(
specification: Specification,
measured: DataArray,
spec_records: Sequence[int] | None = None,
measured_records: Sequence[int] | None = None,
) -> int
The whole-dB offset that best lays the measurement on the specification — what a run captured at -6 dB needs added to be compared against the 0 dB requirement.
Each common channel answers with the median dB difference across the lines it shares with the specification — the median because a resonance or a notch is exactly the kind of departure the comparison exists to show, and a mean would let it vote on the level. Whole decibels because runs are commanded in them, and rounding is what keeps a genuine half-dB level error visible as an error instead of silently absorbed.
Across channels the answer is the smallest offset at least two channels agree on, not the median of all of them. A specification usually bounds monitors as well as controls, each monitor sitting its own distance under its envelope, and the median of that spread lands wherever the monitors happen to pile up — measured on a real 36-channel run it said +8 dB for a section commanded at -6. The controls are the channels on their specification, every one at exactly the commanded offset, which makes the commanded level the smallest value with corroboration; requiring two keeps one broken channel from answering alone. No agreement anywhere, the median of the channel answers; no channels, zero.
And one physical veto over whichever answer wins: a commanded level is a floor. In a genuine run-up every channel sits at or above the commanded offset — controls exactly on it, monitors above it — so a channel whose band-median lands well below the candidate falsifies the scaled-run premise outright — provided that channel is itself behaving (a channel sitting over its own envelope is a fault, and a fault vetoes nothing). The plate's demonstration run is the case that demanded it: two shakers cannot hold eight channels to the specification, the drive point ended dead on spec and the rest scattered 3-14 dB low, and two of them happening to agree at +4 read as a well-controlled -4 dB run. When the floor contradicts the candidate the answer is zero: scaling a comparison silently on doubtful evidence is worse than showing the mismatch, and the Scaling field is right there to be typed in.
Source code in src/visualdynamics/core/compliance.py
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 | |
comparison_scale_db
¶
comparison_scale_db(
specification: Specification,
measured: DataArray,
spec_records: Sequence[int] | None = None,
measured_records: Sequence[int] | None = None,
) -> int
The decibels every comparison adds to measured: the value the
user holds on the object (scale_db, 0 included), or the detected
one when nothing is held. The one resolver, so the drawn curves,
the error metrics and the report cannot disagree.
Source code in src/visualdynamics/core/compliance.py
matched_records
¶
matched_records(
specification: Specification,
measured: DataArray,
spec_records: Sequence[int] | None = None,
measured_records: Sequence[int] | None = None,
) -> list[tuple[str, int, int]]
[(label, spec record, measured record)] for the channels the two
have in common, paired by DOF in the specification's own order —
the pairing compare_all has always used, named so the scale
detection walks exactly the channels the comparison will.
Source code in src/visualdynamics/core/compliance.py
exceedances
¶
exceedances(
specification: Specification,
measured: DataArray,
spec_record: int = 0,
measured_record: int = 0,
pair: str = "abort",
scale_db: float | None = None,
) -> tuple[ndarray, ndarray]
(over, under): which lines went outside a pair of limits, and how.
What the plot marks, so a line out of tolerance is found by looking
rather than by reading a percentage and hunting for it. The lines
judged are the scaled measurement — the same curve the comparison
draws — resolved through comparison_scale_db unless the caller
already did.
Source code in src/visualdynamics/core/compliance.py
compare
¶
compare(
specification: Specification,
measured: DataArray,
spec_record: int = 0,
measured_record: int = 0,
scale_db: float | None = None,
) -> dict[str, Any]
How one measured record answers one record of a specification.
The two RMS levels and the difference between them as a percentage, how many lines were compared and over what band, and for each pair of limits how many of those lines fell outside it. A limit the specification does not carry is absent rather than zero.
Every number is of the scaled measurement — scale_db resolved
through comparison_scale_db unless the caller already did — and
scale_db is echoed in the result so a table can say what was
compared.
Source code in src/visualdynamics/core/compliance.py
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 | |
channel_errors
¶
[(label, dB out, percent of lines outside abort)] for a bar chart.
A row per control channel, in the specification's own order, from
whatever compare_all returned. Channels with nothing to compare
are left out rather than drawn at zero, which would read as a
channel that matched.
Source code in src/visualdynamics/core/compliance.py
outside_fraction
¶
What share of these fell outside the threshold, as a percentage.
One-sided when high is None — the lines-out chart has a ceiling
and no floor, because no amount of staying inside the abort
limits is a fault.
Source code in src/visualdynamics/core/compliance.py
compare_all
¶
compare_all(
specification: Specification,
measured: DataArray,
spec_records: Sequence[int] | None = None,
measured_records: Sequence[int] | None = None,
scale_db: float | None = None,
) -> list[tuple[str, dict[str, Any]]]
[(label, result)] for every channel the two have in common.
A specification bounds control channels, and a measurement holds those and usually many more. Pairing is by DOF — the response, and the reference where there is one — so a cross spectrum is compared against the cross term of the specification if it carries one, and against nothing if it does not.
In the specification's own order, which is the order a control room reads its channels in. The scale is resolved once for the whole set — one measurement gets one scaling, never a different number per channel — and a caller comparing a derived form of the data (the report's own octave banding) passes the scale it resolved on the original, so the two gridings cannot round to different decibels.
Source code in src/visualdynamics/core/compliance.py
srs_errors
¶
[(label, event, RMS dB deviation)] for a shock spectrum against the one it was required to meet.
In decibels, and root-mean-square across the band. An SRS spans
decades and is read on log axes, so the obvious distance — the one
a time waveform gets, ||m - s|| / ||s|| — is dominated by whatever
bands happen to be loudest and goes blind to a large ratio error
anywhere the target is small. Measured on the airplane run that
reading passed every channel at 20% while one of them sat 25 dB
over: 99.5% of its norm came from the top two decades. In dB every
band counts the same, which is how a shock is judged and why the
threshold beside it is the same three decibels a level error gets.
RMS rather than the worst band, because the worst band is one point and one point can be noise; a curve that is 4 dB out everywhere and a curve with a single 4 dB spike are not the same result and should not read as one. The worst is still visible on the plot.
Signed: the magnitude is the RMS, the sign is which side of the requirement the spectrum predominantly sits — the sign of the mean dB deviation. An unsigned RMS read an under-test as an over-test: a spectrum 5 dB low everywhere reported +5, the bar chart shaded it past the ceiling, and the reader concluded the shock was too hard when the machine had under-hit (Brandon, on the drone stress set, 2026-08-20). A curve genuinely astride the requirement — big RMS, mean near zero — takes whichever side its mean leans, which is honest: the magnitude is what convicts it, and either bound is the same three decibels away.
The specification is interpolated onto the measurement's own frequencies, log-log, the way a written specification is — and only where it reaches. Outside its band it says nothing.
Source code in src/visualdynamics/core/compliance.py
signed_rms_db
¶
The RMS of the line-by-line deviation in decibels, signed by which way the mean falls: over is positive, under is negative. The one metric the SRS and the sine readings share — a definition written once, so the two cannot drift apart.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
got
|
ndarray
|
Measured and target, positive and finite, line for line. |
required |
wanted
|
ndarray
|
Measured and target, positive and finite, line for line. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The signed RMS deviation in dB. |
Source code in src/visualdynamics/core/compliance.py
sine_errors
¶
[(dof, tone, RMS dB deviation)] for extracted sine levels against the tones they were controlled to.
The same reading srs_errors gives a shock spectrum, for the same
reasons: decibels so every part of the sweep counts alike, RMS so
one noisy line does not convict a channel, signed by the mean
so an under-test reads under. The target is the tone's own
breakpoint interpolation (SineTone.target — linear in f on
linear segments, log-f on log ones), evaluated only where the
level actually swept: coverage is the comparison's, not assumed.