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_areas |
The area under the power law through these points over each of |
log_log_area |
The area under the power law through these points, exactly — |
written_band |
(low, high) one written curve actually says something over. |
coverage |
The stretches one record of a spectrum speaks for, merged. |
comparable |
Why this pair cannot be compared, or None when it can. |
cells |
The cells one comparison is judged over. |
judge |
Which cells of a comparison fell outside one limit, and where. |
covered |
Each measured bin cut to the part of it a band covers. |
outside |
Which measured lines fell outside one written limit curve — the |
band_of |
(low, high) the specification actually says something over: the |
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 offset, on the 3 dB ladder from zero, that best lays 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. |
verdict |
Whether a run passed, read off its octave-band comparison. |
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_areas
¶
log_log_areas(frequencies: ArrayLike, values: ArrayLike, lows: ArrayLike, highs: ArrayLike) -> ndarray
The area under the power law through these points over each of
several bands at once, exactly — log_log_area, vectorized.
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
The cumulative area at every breakpoint is taken once, and each band's answer is the cumulative area at its high edge less that at its low edge, the partial segment at either end taken from the same power law — so a thousand cells cost what one did (the comparison is judged cell by cell, 2026-09-19). A band reaching past the written points is cut at them; one wholly outside is zero; too few points to make a curve is NaN.
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 —
one band of log_log_areas, the whole curve when no band is
given. Zero when the band is empty, NaN when fewer than two points
are written.
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
coverage
¶
The stretches one record of a spectrum speaks for, merged.
A density's written bins — a controller's zero and NaN lines are holes, not a requirement of nothing — or the segments between a curve's consecutive written breakpoints; either way as (low, high) pairs in order, neighbors joined. What a comparison is judged over: the part of a cell inside these, and nothing else.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spectrum
|
Psd or Specification
|
The object. |
required |
record
|
int
|
Which record. |
0
|
Returns:
| Type | Description |
|---|---|
list of (float, float)
|
Empty when nothing is written above zero hertz. |
Source code in src/visualdynamics/core/compliance.py
comparable
¶
comparable(specification: Specification, measured: DataArray) -> str | None
Why this pair cannot be compared, or None when it can.
A requirement on octave bands compares only with a response on the same bands, same fraction, and a response on octave bands only with such a requirement (Brandon, 2026-09-19): a band's power cannot be attributed to part of its width, and a requirement on bands says nothing about the lines under them. A narrowband response compares with a curve at breakpoints or a requirement on lines. Anything else is not judged, and this says so in words the table and the status bar can show.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
specification
|
(Specification, DataArray)
|
The pair. |
required |
measured
|
(Specification, DataArray)
|
The pair. |
required |
Returns:
| Type | Description |
|---|---|
str or None
|
The reason, or None. |
Source code in src/visualdynamics/core/compliance.py
cells
¶
cells(specification: Specification, measured: DataArray, spec_record: int = 0, measured_record: int = 0) -> list[dict[str, Any]]
The cells one comparison is judged over.
A comparison happens on the coarser of the two grids — a
requirement written per octave band is a requirement on the
band's power, not on every line under it, and a banded
measurement against a breakpoint curve is judged band by band —
and each cell is cut to what both objects speak for: the
specification's written stretches (coverage; a controller's
zeros are holes) and the measurement's. A cell nothing is written
in is not judged at all, and one written over part of its width
is judged over that part, both sides integrated over the same
stretch. That is the one rule at the ends and in the middle
alike (Brandon, 2026-09-19; PLAN.md "A comparison is an area
against an area"). A pair that cannot be compared (comparable)
has no cells at all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
specification
|
(Specification, DataArray)
|
The pair. |
required |
measured
|
(Specification, DataArray)
|
The pair. |
required |
spec_record
|
int
|
Which record of each. |
0
|
measured_record
|
int
|
Which record of each. |
0
|
Returns:
| Type | Description |
|---|---|
list of dict
|
Each with 'low' and 'high' (the cell's outer reach), 'pieces' (the stretches judged, inside it), 'width' (their sum) and 'span' (the whole bin it came from, judged or not). |
Source code in src/visualdynamics/core/compliance.py
judge
¶
judge(specification: Specification, measured: DataArray, spec_record: int = 0, measured_record: int = 0, limit: str | None = 'abort_upper', over: bool = True, scale_db: float | None = None) -> dict[str, Any]
Which cells of a comparison fell outside one limit, and where.
Over each cell the measurement's power — its area, read as it is
drawn, times the comparison's scale — against the power the limit
asks for over the very same stretch, the limit read the way its
specification is: the exact area under a power law between
breakpoints, a density per line or per band otherwise. A cell is
out when it holds more than the upper limit asks (over) or less
than the lower one (not over). limit None judges against the
target itself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
specification
|
(Specification, DataArray)
|
The pair. |
required |
measured
|
(Specification, DataArray)
|
The pair. |
required |
spec_record
|
int
|
Which record of each. |
0
|
measured_record
|
int
|
Which record of each. |
0
|
limit
|
str or None
|
The limit curve, one of |
'abort_upper'
|
over
|
bool
|
Whether holding more than the curve asks is what is out. |
True
|
scale_db
|
float
|
The comparison's scale; resolved through |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
'cells' (as |
Source code in src/visualdynamics/core/compliance.py
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 413 414 415 416 417 418 419 420 421 422 423 | |
covered
¶
covered(lines: ArrayLike, low: float, high: float, widths: ArrayLike | None = None) -> tuple[ndarray, ndarray, ndarray, ndarray]
Each measured bin cut to the part of it a band covers.
A line stands for its whole bin, 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. With widths given, a bin is its own geometric edges; without, the midpoints between neighbors.
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', spec_widths: ArrayLike | None = None, widths: ArrayLike | None = None) -> ndarray
Which measured lines fell outside one written limit curve — the
array form of judge, for a limit and a measurement held as
arrays: the limit is read as reading ('log_log' between its
points, 'bin' as a density per bin of spec_widths), the
measurement as a density per bin of widths (midpoints between
neighbors when None), and the two go through the one cell rule.
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: the
outer ends of its coverage.
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 neighbors. 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 offset, on the 3 dB ladder from zero, 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 — snapped to the nearest multiple of SCALE_STEP_DB, since
that is the ladder runs are commanded on; a level error of a
decibel or two reads as its nearest step, and the Scaling field is
there to type the truth. (Whole decibels until 2026-09-19.)
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
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 | |
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. A pair
that cannot be compared (comparable) answers with no lines and
'refused', the reason in words.
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
781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 | |
verdict
¶
Whether a run passed, read off its octave-band comparison.
Two readings across the control channels compare_all judged:
the share of them with more than LINES_PERCENT of their band
outside the abort limits, and the share more than ERROR_DB off
in RMS. The run passes when the first is under
LINES_CHANNELS_FAIL_PERCENT and the second under
RMS_CHANNELS_FAIL_PERCENT; either at or past its number fails
it (Brandon, 2026-09-20). Channels with nothing to compare are
not counted. No channels at all is no verdict: 'passed' None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rows
|
sequence of (str, dict)
|
What |
required |
Returns:
| Type | Description |
|---|---|
dict
|
'passed' (True, False or None), 'channels' (how many were judged), 'lines_percent' and 'rms_percent' (the two shares), and the four thresholds they were read against. |
Source code in src/visualdynamics/core/compliance.py
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.