visualdynamics.core.compliance¶
compliance
¶
How a measured response compares with the specification it was controlled to.
A random vibration test is judged by comparing the response PSD with its specification, and a report needs numbers from that comparison: the power specified, the power measured, and how much of the band fell outside the warning and abort limits.
The two rarely share a frequency axis. A specification is often written at a few breakpoints, or on the controller's lines rather than the analysis lines. A specification written at breakpoints is read log-log, the way it is drawn, so the curve between two breakpoints is the straight line on log axes, a power law. Interpolated linearly instead, a segment one decade wide reads several dB above the specification through the middle of the segment.
The comparison is made cell by cell (cells): on the measurement's lines
against a breakpoint curve, and on the coarser of the two grids when both
are densities on bins. Only frequencies both objects are written over are
judged. Outside the specification's band nothing is specified, and a
response there neither passes nor fails.
At the two ends of that band a line's bin is usually partly inside and partly outside. Counting the whole bin credits the response with power the specification did not ask for, and dropping it loses power that was asked for. So a line at an end is compared over the part of its bin that the specification covers, the same part the plot shades. Both sides of the comparison are then areas over the same range of frequency, which is what an RMS error needs.
Classes:
| Name | Description |
|---|---|
ScaleWarning |
Warned when a comparison scaled the measurement by a level it |
SrsError |
One channel and event of |
SineError |
One tone at one channel in |
Functions:
| Name | Description |
|---|---|
log_interpolate |
A specification's values at |
cross_term_at |
A cross term written at breakpoints, read at |
record_at |
One record of a specification read at |
log_log_areas |
The exact area under the log-log curve through these points, over |
log_log_area |
The exact area under the log-log curve through these points, over |
written_band |
(low, high), the range one written curve covers: its lowest and |
coverage |
The frequency ranges one record of a spectrum is written over, |
comparable |
Why this pair cannot be compared, or None when it can. |
units_check |
(refused, note): whether a quantity held can be compared with |
cells |
The cells one comparison is judged over. |
cell_lines |
The measured lines one cell of |
judge |
Which cells of a comparison fall outside one limit, and where. |
beyond |
Where a measured power is past a limit's: more than an upper |
covered |
Each measured bin cut to the part of it inside the band from |
outside |
Which measured lines fall outside one limit curve, for a limit and |
band_of |
(low, high), the range the specification is written 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: the square root of |
bounds |
(lower, upper) of one pair of limits, interpolated log-log onto |
significant_band |
|
detect_scale_db |
The offset in dB, a multiple of 3 dB, that best lays the |
read_scale_db |
The offset in dB, a multiple of 3 dB, that best lays the |
comparison_scale_db |
The dB every comparison adds to |
matched_records |
[(label, spec record, measured record)] for the channels the two |
exceedances |
(over, under): which measured lines are above the upper and below |
compare |
How one measured record compares with one record of a |
lines_state |
'over' when more than |
rms_state |
'over' when a channel's RMS is more than |
readouts |
The two numbers a channel's comparison figure carries, from its |
verdict |
Whether a run passed, from the rows |
channel_errors |
[(label, dB out, percent of lines outside abort)] for a bar chart. |
channel_refusals |
{index: 'not compared: reason'} for the rows of |
comparison_note |
|
outside_fraction |
The percentage of these values outside the threshold. |
compare_all |
[(label, result)] for every channel the two have in common, each |
resolved_scale_db |
The one scale a whole set of comparisons is made at: |
srs_errors |
[SrsError(dof, event, db, refused)] for a shock response spectrum |
signed_rms_db |
The RMS of the line-by-line deviation in dB, signed by the mean |
sine_errors |
[SineError(dof, tone, db, refused, note)] for extracted sine |
srs_points |
A measured SRS judged point by point against its requirement's |
srs_limit_bands |
(inner, outer): the bands a requirement's own warning and abort |
srs_band_text |
An SRS tolerance band in words: '−1.5/+3 dB', or '−1.5/+3 dB |
srs_tolerance |
One record of a measured SRS judged point by point against one |
srs_tolerance_all |
[(dof, event, result)] for every measured record the |
Classes¶
ScaleWarning
¶
Bases: UserWarning
Warned when a comparison scaled the measurement by a level it
detected from the data rather than one it was given. The charts
built from channel_errors do not show the scale, so the warning
does.
SrsError
¶
Bases: NamedTuple
One channel and event of srs_errors: its signed RMS deviation
in dB, or why it could not be scored.
SineError
¶
Bases: NamedTuple
One tone at one channel in sine_errors: its signed RMS
deviation in dB, or why it could not be scored.
Functions:¶
log_interpolate
¶
log_interpolate(frequencies: ArrayLike, spec_frequencies: ArrayLike, spec_values: ArrayLike) -> ndarray
A specification's values at frequencies, interpolated log-log.
Points that are zero, negative or NaN are dropped first (a controller
writes zero on the lines outside its band), and the result is NaN
below the lowest and above the highest remaining point: extending the
end segments would add a requirement nobody wrote. A dropped point
between two remaining ones is a gap, NaN up to the remaining points
either side of it, as coverage counts it. Fewer than two remaining
points gives NaN everywhere.
Source code in src/visualdynamics/core/compliance.py
cross_term_at
¶
A cross term written at breakpoints, read at lines the way a
specification is drawn: the magnitude log-log between two points
that are both above zero and straight between two that are not (a
coherence of zero, two channels stated to be independent, is a
value and not a gap), the phase linearly. Not a number outside the
points, and at lines at or below zero hertz.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frequencies
|
array_like
|
The breakpoints' frequencies, in Hz, in any order. |
required |
values
|
array_like
|
The complex cross term at each breakpoint. |
required |
lines
|
array_like
|
Where to read it. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Complex, one value per line. |
Source code in src/visualdynamics/core/compliance.py
record_at
¶
One record of a specification read at lines, the way it is
drawn, and not a number where it is not written.
An autospectrum is written where it is above zero, and between
breakpoints ('log_log') is read log-log with its gaps
(log_interpolate). A cross term is written wherever it is a
number, zero included, and is read between breakpoints by
cross_term_at; whether its coherence is defined is for its two
autospectra to say. On lines or bands ('bin') a record takes the
value of the bin the line falls in. The matrix comparison and the
drive prediction both read a specification here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Psd or Specification
|
The specification. |
required |
record
|
int
|
Which record. |
required |
lines
|
array_like
|
Where to read it, in Hz. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Real for an autospectrum, complex for a cross term. |
Source code in src/visualdynamics/core/compliance.py
log_log_areas
¶
log_log_areas(frequencies: ArrayLike, values: ArrayLike, lows: ArrayLike, highs: ArrayLike) -> ndarray
The exact area under the log-log curve through these points, over
each of several bands at once: log_log_area for many bands.
The points are breakpoints of a continuous curve, and between two of them the curve is the straight line they make on log axes, a power law W = C f**n. Its integral has a closed form, so the area does not depend on a grid or on how finely anything else was measured. For a segment from (f1, W1) to (f2, W2), with n the slope in log-log, 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 computed once, and each band's area is the cumulative area at its high edge less that at its low edge, with the partial segments at the two ends taken from the same power law. A thousand bands cost about what one does. Only positive, finite points are used. A band that reaches past them is cut at them, a band wholly outside them is zero, and fewer than two points gives NaN for every band.
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 exact area under the log-log curve through these points, over one band, or over the whole curve when no band is given.
One band of log_log_areas, cut to the written points. Zero when
the band is empty, NaN when fewer than two points are positive and
finite.
Source code in src/visualdynamics/core/compliance.py
written_band
¶
(low, high), the range one written curve covers: its lowest and highest positive, finite points. None when there are fewer than two.
Source code in src/visualdynamics/core/compliance.py
coverage
¶
The frequency ranges one record of a spectrum is written over, merged.
For a density on bins, the bins of the lines the object counts as
written (written). For a specification, that is finite and
positive, so a controller's zero and NaN lines are gaps, not a
requirement of zero. For a curve between breakpoints, the segments
between consecutive written breakpoints. Either way the result is
(low, high) pairs in ascending order, with touching ranges joined. A
comparison is judged only over the part of a cell inside these
ranges.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spectrum
|
Psd or Specification
|
The spectrum. |
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 specification on octave bands compares only with a response on bands of the same fraction, and a response on octave bands only with such a specification: a band's power cannot be assigned 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 with a specification on lines. The reason is a phrase 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
units_check
¶
units_check(wanted: str, held: str, *, wanted_name: str = 'the requirement', held_name: str = 'the measurement') -> tuple[str | None, str | None]
(refused, note): whether a quantity held can be compared with the one required, from the two records' declared quantities.
Values are held in SI once their units are declared and as they
came otherwise, so the rule has three cases. Both declared: they
must be the same quantity, since an acceleration says nothing about
a velocity requirement. One declared: refused, since one side is in
SI and the other is not, and the ratio is off by the unit's scale.
Neither declared: compared, on the reading that whoever compares
two sets of raw numbers knows they share a unit, with RAW_NOTE
returned so the result can say so. Every comparison of a
measurement against what it had to meet uses this one rule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
wanted
|
str
|
The two quantities, as |
required |
held
|
str
|
The two quantities, as |
required |
wanted_name
|
str
|
How the reason names each side. |
'the requirement'
|
held_name
|
str
|
How the reason names each side. |
'the requirement'
|
Returns:
| Type | Description |
|---|---|
tuple of (str or None, str or None)
|
Why the pair is refused, or None; and |
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.
Against a curve at breakpoints, the cells are the measurement's own
lines. When both are densities on bins, the cells are the bins of the
coarser grid, by median bin width: a requirement written per octave
band is a requirement on the band's power, not on every line under
it. A banded measurement compares only with a specification on bands
of the same fraction, never with a breakpoint curve (comparable),
and a pair that cannot be compared has no cells.
Each cell is cut to what both objects are written over: the
specification's ranges (coverage, where a controller's zero lines
are gaps) and the measurement's. A cell with nothing written in it is
not judged, and a cell written over part of its width is judged over
that part, both sides integrated over the same range. The same rule
applies at the ends of the band and in the middle.
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
cell_lines
¶
The measured lines one cell of cells reaches, as a slice of
the bins whose edges are left and right (sorted), or None when
it reaches none.
Found by where the cell's ends fall among the bins: a bin that ends
where the cell begins shares an edge and no width, and is not in
it. judge marks the lines of an out cell by this, and a reading
that spreads a cell's result over its lines uses the same rule.
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 fall outside one limit, and where.
For each cell, the measurement's power (its area, integrated the way
it is drawn, times the comparison's scale) is compared with the
limit's power over the same range. The limit is integrated the way
its specification is: the exact area under the power law between
breakpoints, or a density per line or per band. A cell is out when
it holds more than an upper limit (over True) or less than a lower
one (over False). 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
|
True when holding more than the curve is out, False when holding less is. |
True
|
scale_db
|
float
|
The comparison's scale in dB, added to the measurement;
resolved through |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
Per cell: 'cells' (as |
Source code in src/visualdynamics/core/compliance.py
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 606 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 | |
beyond
¶
Where a measured power is past a limit's: more than an upper
limit (over True), less than a lower one. A power exactly on the
limit is inside. The one rule judge reads every cell by, and
core.chance its groups of cells.
Source code in src/visualdynamics/core/compliance.py
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 inside the band from low
to high.
A line stands for its whole bin, and at the ends of a specification
a bin extends past the edge. Returns (start, stop, width, cut): where
each bin lies inside the band, how wide that part is, and which bins
the band's edge cuts. A bin wholly outside has zero width and is not
cut. With widths given, the bins are proportional bands with edges
from octave.bin_bounds; without, the bins end at the midpoints
between neighbors. Fewer than two lines gives arrays of zeros.
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 fall outside one limit curve, for a limit and
a measurement held as plain arrays: the array form of judge.
The limit is read as reading: 'log_log' between its points, or
'bin' as a density per bin of spec_widths. The measurement is a
density per bin of widths (bins ending at the midpoints between
neighbors when None). The two go through the same cell rule as every
other comparison, against the limit itself with no scale. Returns a
bool per line, all False when there are fewer than two lines or
limit points, or when only one of the two widths is given (a banded
object does not compare with an unbanded one).
Source code in src/visualdynamics/core/compliance.py
band_of
¶
band_of(specification: Specification, record: int = 0) -> tuple[float, float] | None
(low, high), the range the specification is written over: the
outer ends of its coverage. None when nothing is written.
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.
It is not computed on a measurement's frequency grid. The level a specification asks for belongs to the specification, and it should not change in the fourth decimal place because a frame length changed.
The integral is the specification's own area, taken the way the
object is drawn: the exact power-law area for a specification
written at breakpoints, the sum over bins for one computed from a
record (the PSD of a transient target is a Specification of
thousands of density lines). low and high, in Hz, limit the band.
Source code in src/visualdynamics/core/compliance.py
rms
¶
The RMS a PSD carries over the lines given: the square root 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 hold, exactly as Parseval's theorem gives it, where the trapezoid rule halves the two end bins and reads a little low.
The widths come from the whole axis before any line is dropped, so a NaN or negative line contributes nothing and its bin is not added to its neighbors'; bridging a gap would assume what is in it. NaN when there are fewer than two lines or no usable one.
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, interpolated log-log onto
frequencies.
pair is 'warning' or 'abort'. None for a limit the specification
does not carry: nothing can be exceeded where nothing was specified.
Source code in src/visualdynamics/core/compliance.py
significant_band
¶
good, narrowed to the lines where the specification has real
content.
Lines within DETECTION_RANGE_DB of this channel's own peak in
want. Relative to the peak rather than a fixed level, because a
specification's units and size are its own; per channel, because
each channel is specified at its own level.
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, controls: Sequence[str] | None = None) -> int
The offset in dB, a multiple of 3 dB, that best lays the
measurement on the specification, or zero when the data does not
show one clearly: read_scale_db, with its None read as zero, the
scale that changes nothing.
Source code in src/visualdynamics/core/compliance.py
read_scale_db
¶
read_scale_db(specification: Specification, measured: DataArray, spec_records: Sequence[int] | None = None, measured_records: Sequence[int] | None = None, controls: Sequence[str] | None = None) -> int | None
The offset in dB, a multiple of 3 dB, that best lays the measurement on the specification, or None when the data does not show one clearly. Zero is a reading here: the bands agree that the run was at full level.
A run captured at -6 dB needs +6 dB added before it is compared with
the 0 dB specification. Both objects are banded to sixth octaves
(octave.PER_OCTAVE); a pair already banded is used as it is. Every
band of every compared channel gives the dB it would need added,
rounded to the nearest multiple of SCALE_STEP_DB, and the most
common value (the mode) is the candidate. It is taken as the test's
level only if all of these hold:
- it lies within
DETECTION_MIN_DBtoDETECTION_MAX_DB, a level a controller would command (a run 50 dB hot is a failure, not a level); - at least
DETECTION_AGREEMENTof the bands lie within half a step of it, and their median is withinDETECTION_RUNG_DBof it, so the bands agree on one level and that level is on a step; - the total power agrees with it within
ERROR_DB. A change of level moves the band median and the total power together. A run that is low between its resonances and hot at them has most bands agreeing on a low level while its power is at the peaks; the agreement test does not see the peaks, which are a minority of the bands, and the total power does.
Otherwise the result is None, and the comparison is shown as
measured (detect_scale_db). Scaling on doubtful evidence is worse than showing the
mismatch, because a wrong scale can turn a failed test into a passed
one. The Scaling field is where the true level is typed.
The mode is used because a controller scales every channel together, so the level is one number for the whole test. The control channels fall on the same step, while monitor channels, each its own distance under its envelope, spread over many; the mode finds the controls, where a median of all the bands finds wherever the monitors happen to fall. Bands are used because in narrowband each line is a noisy estimate and many lines are at anti-resonances no controller holds: runs at full level read +14, +9 and +4 dB on lines and 0 dB in sixth-octave bands.
A full-level test the controller held 3 dB low everywhere looks the
same in the data as a test commanded at -3 dB, and is read as one.
The commanded level is not in a Rattlesnake file, so a detected level
is always reported where it is used: the report's test-level box,
the captions and ScaleWarning.
controls names the control channels (by DOF, or by the label
matched_records gives) when the caller knows them, and only those
are counted. Without it, a specification that also bounds many
monitor channels spreads its bands and reads zero. Only the bands
where the specification has real content are used
(significant_band, per channel).
Source code in src/visualdynamics/core/compliance.py
888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 | |
comparison_scale_db
¶
comparison_scale_db(specification: Specification, measured: DataArray, spec_records: Sequence[int] | None = None, measured_records: Sequence[int] | None = None, controls: Sequence[str] | None = None) -> int
The dB every comparison adds to measured: the value set on the
object (scale_db, zero included), or the detected one
(detect_scale_db) when none is set. controls, when known, names
the channels the detection uses. Every comparison gets its scale
here, 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.
Records are paired by response and reference DOF and by quantity, in
the specification's order. A record of unknown quantity pairs with
any quantity, and an exact match is preferred. A DOF measured only
in another quantity is still paired, so that compare refuses it
with the reason rather than the channel vanishing. The label is the DOF,
or 'response/reference' for a cross term. compare_all and the
scale detection both pair channels here, so the detection uses
exactly the channels the comparison compares.
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 measured lines are above the upper and below the lower limit of one pair.
pair is 'warning' or 'abort'. These are the lines the plot marks,
so a line out of tolerance can be found by looking at the plot. The
lines judged are those of the scaled measurement, the same curve
the comparison draws; scale_db is resolved through
comparison_scale_db unless given. A limit the specification does
not carry gives all False.
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 compares with one record of a specification.
Returns a dict: 'lines' (how many cells were judged), 'band' (low and high, Hz), 'scale_db', 'specification_rms' and 'measured_rms', 'difference_percent' and 'difference_db' (the measured RMS against the specification's; 20 log10 for the dB), and for each pair of limits the specification carries, 'warning_lines' and 'warning_percent' or 'abort_lines' and 'abort_percent': how many judged cells fell outside the pair and their share of the comparison. A pair the specification does not carry is absent rather than zero. The share counts cells, each weighted by the fraction of its bin that was judged, so a band at the top of the range counts the same as one at the bottom.
A pair of objects that cannot be compared (comparable), a pair
of records of different quantities or with only one side's units
declared (units_check), or a specification record that is a cross
term (CROSS_TERM_REFUSAL), returns {'lines': 0, 'refused':
reason}, and one with nothing to compare returns {'lines': 0}. With
neither side's units declared the pair is compared and the result
carries 'note' (RAW_NOTE).
Every number is of the scaled measurement: scale_db is resolved
through comparison_scale_db unless given, and is returned so a
table can say what was compared.
Source code in src/visualdynamics/core/compliance.py
1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 | |
lines_state
¶
'over' when more than LINES_PERCENT of a channel's band is
outside its abort limits, else 'in'. No amount of staying inside is
a fault, so there is no 'under'.
Source code in src/visualdynamics/core/compliance.py
rms_state
¶
'over' when a channel's RMS is more than ERROR_DB above the
specification's, 'under' when more than that below, else 'in'
(and for a NaN, which nothing judges).
Source code in src/visualdynamics/core/compliance.py
readouts
¶
The two numbers a channel's comparison figure carries, from its
compare result: the share of its band outside abort and its RMS
error, each with its state against the tolerance (lines_state,
rms_state), the same reading verdict and the bar charts give.
Each is a dict of 'name' ('lines' or 'rms'), 'label', 'text' (the number as it is shown) and 'state' ('in', 'over' or 'under'). The share is left out when the specification carries no abort limits, and the error when it could not be read; a channel with nothing compared has none.
Source code in src/visualdynamics/core/compliance.py
verdict
¶
Whether a run passed, from the rows compare_all returned.
Two shares of the channels judged: those with more than
LINES_PERCENT of their band outside the abort limits, and those
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 one at or over its threshold
fails it. Channels with nothing to compare are not counted, and with
no channels at all there 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.
One row per channel, in the specification's order, from the rows
compare_all returned. Channels with nothing to compare are left
out, since a bar at zero would read as a channel that matched. A
refused channel keeps its row with both values NaN, and
channel_refusals says why, by the same index.
Source code in src/visualdynamics/core/compliance.py
channel_refusals
¶
{index: 'not compared: reason'} for the rows of channel_errors
that were refused, indexed as channel_errors orders them.
Source code in src/visualdynamics/core/compliance.py
comparison_note
¶
RAW_NOTE when any of these comparisons was made with neither
side's units declared, else None. Takes the result dicts of
compare, compare_all's (label, result) rows, or SrsError
rows.
Source code in src/visualdynamics/core/compliance.py
outside_fraction
¶
The percentage of these values outside the threshold.
With high None the test is one-sided, values at or above low:
the lines-out chart has a ceiling and no floor, because staying
inside the abort limits is never a fault. Otherwise a value is
outside when it is above high or below low. Values that are not
finite are ignored, and none left gives zero.
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, each
result from compare.
A specification bounds the control channels, and a measurement
usually holds those and many more. Channels are paired by DOF and
quantity (matched_records). Only the specification's autospectra
are compared here: a cross term is not a level, and its coherence
and phase are judged by core.matrix_comparison.compare_matrix,
which compares the autospectra through this same function.
Rows are in the specification's order, the order a control room reads its channels in. The scale is resolved once for the whole set, so one measurement gets one scale, never a different value per channel. 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 cannot round to different values.
A nonzero scale detected here (none passed and none set on the
measurement) is warned as a ScaleWarning: every row carries it as
'scale_db', but channel_errors and the charts built from it do
not. Pass scale_db=0 to compare as measured.
Source code in src/visualdynamics/core/compliance.py
resolved_scale_db
¶
resolved_scale_db(specification: Specification, measured: DataArray, spec_records: Sequence[int] | None = None, measured_records: Sequence[int] | None = None, scale_db: float | None = None, stacklevel: int = 2) -> float
The one scale a whole set of comparisons is made at: scale_db
when given, else comparison_scale_db's, with a ScaleWarning
when that scale was detected from the data rather than set on the
measurement. compare_all and the matrix comparison resolve their
scale here, so both warn alike. stacklevel points the warning at
the caller's caller.
Source code in src/visualdynamics/core/compliance.py
srs_errors
¶
[SrsError(dof, event, db, refused)] for a shock response spectrum against the one it was required to meet.
The deviation is in dB, RMS across the band, and signed
(signed_rms_db on each channel's lines). An SRS spans decades and
is read on log axes. A relative norm, ||m - s|| / ||s|| as a time
waveform gets, is dominated by the loudest bands and does not see a
large ratio error where the target is small. In dB every band counts
the same, which is how a shock is judged, and the threshold beside it
(SRS_ERROR_DB) is the same 3 dB 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 4 dB out everywhere and a curve with a single 4 dB peak are different results. The worst band is still visible on the plot.
The sign is the sign of the mean dB deviation, the side of the requirement the spectrum is mostly on, so an under-test reads negative. A curve on both sides of the requirement (a large RMS with a mean near zero) takes the side its mean is on; the magnitude is what fails it, and either bound is the same 3 dB away.
The specification is interpolated log-log onto the measurement's own frequencies, and only where it is written. Channels are paired by response DOF, and the event is the measurement's block label for the record, None when it has none.
A pair at another Q, or one units_check refuses (another quantity,
or only one side's units declared), keeps its row with db NaN and
refused saying why, so the channel shows as not scored instead of
disappearing or reading 20 dB out. With neither side declared the
pair is scored and note says it was compared as raw numbers. The
rule is the one srs_tolerance refuses by.
Source code in src/visualdynamics/core/compliance.py
1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 | |
signed_rms_db
¶
The RMS of the line-by-line deviation in dB, signed by the mean deviation: positive when over, negative when under.
The SRS and the sine comparisons both use this one definition, so the two cannot come to differ.
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
¶
sine_errors(specification, levels) -> list[SineError]
[SineError(dof, tone, db, refused, note)] for extracted sine levels against the tones they were controlled to.
The same value srs_errors gives a shock spectrum, for the same
reasons: in dB so every part of the sweep counts alike, RMS so one
noisy line does not fail a channel, and signed by the mean so an
under-test reads negative. The target is the tone's own breakpoint
interpolation (SineTone.target: linear in frequency on linear
segments, linear in log frequency on log ones), read only at the
frequencies the level covers. Lines where the tone was under the
noise floor (SineLevel.resolved False) are not scored, and neither
are zero readings. levels is a SineLevelSet or a sequence of
SineLevel; a tone or channel the specification does not have is
skipped.
A level and its tone are compared by units_check: a level of
another quantity, or with only one side's units declared, keeps its
row with db NaN and refused saying why, and with neither
declared the row is scored and carries note. A specification
whose ordinate_unit is None has its units undeclared, whatever
its ordinate_dim names.
Source code in src/visualdynamics/core/compliance.py
1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 | |
srs_points
¶
srs_points(frequencies: ArrayLike, values: ArrayLike, spec_frequencies: ArrayLike, spec_values: ArrayLike, *, inner: tuple[float, float] = SRS_INNER_DB, outer: tuple[float, float] | None = None, rule: str = 'share', percent: float = SRS_POINTS_PERCENT) -> dict[str, Any]
A measured SRS judged point by point against its requirement's
tolerance band, for a spectrum and a requirement held as plain
arrays: the array form of srs_tolerance.
Each point of the measurement is read against the requirement
interpolated log-log onto its natural frequency, as a distance in
dB, twenty log ten of the ratio (an SRS is an amplitude). A point
is judged where the requirement is written (inside its first and
last breakpoints, log_interpolate) and both values are finite and
positive; the measurement's own points are the ones counted,
because a tolerance on an SRS is applied at the points the
spectrum's spacing defines. A point on a band's edge is inside it.
Two rules are offered, each an example of the kind of rule test
documents ask for rather than any one document's requirement.
'share' passes when at least percent of the judged points lie
inside the inner band; a rule that asks for a share of the points
above the requirement is this one with a band from 0 dB up,
inner=(0.0, numpy.inf). 'above_lower' passes when every judged
point is at or above the inner band's lower edge; the band has no
upper edge under it, so every point at or above the lower edge is
inside, and 'inner' in the result is (lower edge, inf). Under
either rule, when outer is given every judged point must also lie
inside it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frequencies
|
array - like
|
The measured spectrum: natural frequencies in Hz and the peak response at each. |
required |
values
|
array - like
|
The measured spectrum: natural frequencies in Hz and the peak response at each. |
required |
spec_frequencies
|
array - like
|
The requirement, at its own breakpoints or points, in the same
units as |
required |
spec_values
|
array - like
|
The requirement, at its own breakpoints or points, in the same
units as |
required |
inner
|
(float, float)
|
The inner band, dB below and above the requirement. |
`SRS_INNER_DB`
|
outer
|
(float, float)
|
The outer band every point must lie inside, dB below and above. |
None
|
rule
|
str
|
One of |
'share'
|
percent
|
float
|
For 'share', the percentage of points that must lie inside the inner band. |
`SRS_POINTS_PERCENT`
|
Returns:
| Type | Description |
|---|---|
dict
|
One entry per measured point, False or NaN where a point was not judged: 'judged', 'db' (its distance from the requirement), 'inside' (inside the inner band), 'over' and 'under' (above and below it) and 'beyond_outer' (outside the outer band). Then the counts over the judged points: 'points', 'inside_count' and 'inside_percent', 'over_count', 'under_count' and 'outer_count'; 'passed' (None when no point was judged); the worst point, 'worst_frequency', 'worst_db' and 'worst_margin_db' (how far outside the band the rule applies it is, negative when inside), NaN when no point was judged; and the settings it was judged under, 'inner', 'outer', 'rule' and 'percent'. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a rule that is not one of |
Source code in src/visualdynamics/core/compliance.py
1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 | |
srs_limit_bands
¶
srs_limit_bands(specification: DataArray, record: int = 0) -> tuple[tuple[Any, Any] | None, tuple[Any, Any] | None]
(inner, outer): the bands a requirement's own warning and abort
limits set, read at its points as srs_tolerance reads them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
specification
|
ShockSpecification
|
The requirement. |
required |
record
|
int
|
Which record. |
0
|
Returns:
| Type | Description |
|---|---|
tuple
|
Each band as (dB below, dB above), an edge an array of dB at the requirement's points where its limit curve is not a fixed number of dB from it; None for a band the requirement has no limits for. |
Source code in src/visualdynamics/core/compliance.py
srs_band_text
¶
An SRS tolerance band in words: '−1.5/+3 dB', or '−1.5/+3 dB within −3/+6 dB' with an outer band, an inner band with no ceiling '−1.5/up dB'. A band read from limit curves that vary along it is named for them, 'the warning limits' and 'the abort limits'. The plot's title and legend and the report's caption both say it so.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inner
|
(float, float)
|
The inner band, dB below and above the requirement, or an array of dB at each point for an edge that varies. |
required |
outer
|
(float, float) or None
|
The outer band, the same way; None for none. |
required |
Returns:
| Type | Description |
|---|---|
str
|
|
Source code in src/visualdynamics/core/compliance.py
srs_tolerance
¶
srs_tolerance(specification: DataArray, measured: DataArray, spec_record: int = 0, measured_record: int = 0, *, inner: tuple[float, float] | None = None, outer: tuple[float, float] | None = None, rule: str = 'share', percent: float = SRS_POINTS_PERCENT, limits: bool = True) -> dict[str, Any]
One record of a measured SRS judged point by point against one record of the SRS it was required to meet.
result = srs_tolerance(spec, srs, outer=(-3.0, 6.0))
result['inside_count'], result['points'], result['passed']
A shock test is accepted on a tolerance band and a rule about the
share of points inside it, and this is that check (srs_points
gives the rule). It is a separate reading from srs_errors, which
gives one signed RMS figure in dB: a spectrum with a narrow, deep
dip can be well inside 3 dB in RMS while every point in the dip is
outside the band, and only a count of the points says so.
A band not given is read from the requirement's own limit curves
when it has them, so the judgement is against the curves the
comparison draws: the warning limits are the inner band and the
abort limits the outer one, each read log-log at every measured
point. An inner band neither given nor written is SRS_INNER_DB;
an outer band neither given nor written is not applied. With
limits False only the bands given are used.
The spectrum is judged as it is, in whichever polarity it holds
(Srs.kind): to judge a positive and a negative spectrum against
one maximax requirement, judge each. Both curves must be at the
same Q and of the same declared quantity, since a spectrum at
another Q or of another quantity is a different curve and no band
can make up the difference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
specification
|
ShockSpecification or Srs
|
The requirement. |
required |
measured
|
Srs
|
The measured spectra. |
required |
spec_record
|
int
|
Which record of each. |
0
|
measured_record
|
int
|
Which record of each. |
0
|
inner
|
(float, float)
|
The bands in dB below and above the requirement, as for
|
None
|
outer
|
(float, float)
|
The bands in dB below and above the requirement, as for
|
None
|
rule
|
str
|
The rule, as for |
'share'
|
percent
|
str
|
The rule, as for |
'share'
|
limits
|
bool
|
Read a band not given from the requirement's limit curves. |
True
|
Returns:
| Type | Description |
|---|---|
dict
|
What |
Raises:
| Type | Description |
|---|---|
ValueError
|
As |
Source code in src/visualdynamics/core/compliance.py
2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 | |
srs_tolerance_all
¶
srs_tolerance_all(specification: DataArray, measured: DataArray, *, inner: tuple[float, float] | None = None, outer: tuple[float, float] | None = None, rule: str = 'share', percent: float = SRS_POINTS_PERCENT, limits: bool = True) -> list[tuple[str, str | None, dict[str, Any]]]
[(dof, event, result)] for every measured record the
specification names, each result from srs_tolerance.
Records are paired the way srs_errors pairs them, by response
DOF, one row per channel per event, so the two readings of a run
cover the same curves. A record with no point to judge is left out,
as it is there.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
specification
|
ShockSpecification or Srs
|
The requirement. |
required |
measured
|
Srs
|
The measured spectra, one record per channel per event. |
required |
inner
|
tuple[float, float] | None
|
The bands and the rule, as for |
None
|
outer
|
tuple[float, float] | None
|
The bands and the rule, as for |
None
|
rule
|
tuple[float, float] | None
|
The bands and the rule, as for |
None
|
percent
|
tuple[float, float] | None
|
The bands and the rule, as for |
None
|
limits
|
tuple[float, float] | None
|
The bands and the rule, as for |
None
|
Returns:
| Type | Description |
|---|---|
list of (str, str or None, dict)
|
One row per judged record, in the measurement's order. |