visualdynamics.core.detect¶
detect
¶
Finding the stretch of a record worth averaging.
A run holds more than the test: the shaker coming up, a reduced-level check, the full level, and whatever was still recording after the environment stopped. Averaging the lot mixes them into one PSD that describes none of them. This works out which stretch to use.
The record is compressed to one number per hop before anything else is decided — a level, in dB — and every judgement after that is made on that short series. That is what keeps it quick on the sets where it matters: three hundred channels of a ten-minute run cost one pass, and the rest is arithmetic on a few thousand points.
Two things make the compression trustworthy rather than merely small:
- the consensus is the median across channels, not the mean, so a channel that drops out or spikes or sits forty dB above its neighbours is one vote and not the answer;
- the mean is removed within each hop, so a DC offset or a slow drift is not read as level.
Nothing normalises the levels against each other or centres the series. Both were tried: the median across channels already does that work, and the threshold sweep walks every bin edge from the top, so scaling a whole record by a million moves the bins together and lands on the same hops. Neither changed an answer, so neither is here.
The level is in dB rather than in power because a level change is multiplicative. In power, scatter grows with the level, so the flattest stretch of a record is reliably its quietest — which is the opposite of what is wanted. In dB a quiet stretch and a loud one scatter alike, and flat means flat.
Functions:
| Name | Description |
|---|---|
frame_length_for |
The nearest power of two to one second of record. |
scatter_floor |
The least a stretch can scatter, in dB, for estimator noise alone. |
observed_scatter |
How much this record actually scatters hop to hop, in dB. |
hop_levels |
The record as one level per hop, in dB. |
plateau |
The highest level the record holds for |
trim |
Take the settled middle of a stretch. |
compress |
(levels in dB, hops per frame, hop in samples, scatter floor). |
analysis_window |
(first sample, last sample) worth averaging in this history. |
suggest |
The averaging to use for a record nobody has set parameters on. |
Classes¶
Functions:¶
frame_length_for
¶
The nearest power of two to one second of record.
A power of two for the FFT, and about a second because that is the resolution a random vibration test is usually specified at. On a rate that is itself a power of two — which most are — the two come out the same number.
Source code in src/visualdynamics/core/detect.py
scatter_floor
¶
The least a stretch can scatter, in dB, for estimator noise alone.
A power estimated from samples samples has at most that many
degrees of freedom, and a chi-squared with dof has a relative
standard deviation of sqrt(2/dof); in dB that is 4.343 times it, for
the small values this deals in.
A floor and not an estimate. Real records scatter more, and by a
lot: the full-level stretch of a closed-loop run scatters about a dB
against a floor here of a quarter of one, because the loop is
adjusting the whole time and the response really is wandering.
Band-limited data has fewer independent lines than samples, too. So
this bounds the answer from below and observed_scatter supplies
the rest.
Source code in src/visualdynamics/core/detect.py
observed_scatter
¶
How much this record actually scatters hop to hop, in dB.
From the first differences, which a level change barely touches — one step in a hundred hops moves one difference — so what is left is the wander within a level. Robust rather than a standard deviation, for the same reason: the steps must not set the tolerance for what counts as the same level.
Source code in src/visualdynamics/core/detect.py
hop_levels
¶
hop_levels(
ordinate: ArrayLike,
hop: int,
sparse: bool = False,
loudest: bool = False,
) -> ndarray
The record as one level per hop, in dB.
Returns an empty array when there is not a whole hop of record, or when every channel is flat — a file of zeros has no level to speak of, and saying so is better than reporting a floor.
loudest reduces across channels by the maximum instead of the
median. The median is the right voter for detection — one bad
channel is a vote, not the answer — but it is blind to one channel
still ringing under twenty quiet ones, and a shock window sized on
it closes while that channel's spectrum is still being written.
sparse changes what counts as a flat channel, and it exists
because the two detectors ask different questions of the same
compression. A stationary run's channel is live when its median
hop carries power: anything quieter than that half the time is a
channel that dropped out. A shock record's channel is quiet most of
the time by definition — three events in a minute leaves the median
hop empty — so there, a channel is live if it carries power
anywhere. Judged by the stationary rule, a record of sparse
transients reports no level at all.
Source code in src/visualdynamics/core/detect.py
plateau
¶
plateau(
level: ndarray,
sigma: float,
want_hops: int,
tolerance: float = TOLERANCE,
) -> tuple[int, int]
The highest level the record holds for want_hops hops.
Level alone is the wrong thing to maximise. A ten-second overload in the middle of a two-minute test is the highest level in the record and the last thing anyone wants to average; sought that way it wins, and returns a fifth of the frames the test itself would have. So duration comes first and level second.
Sweeping a threshold down from the top does both at once: the longest run above a threshold only grows as the threshold falls, so the first threshold whose run is long enough is the highest level that is held long enough. When nothing is held that long — a short record, or a test that never settled — the longest stretch near the top is taken instead, and it is the caller's business that it is shorter than asked for.
Source code in src/visualdynamics/core/detect.py
trim
¶
Take the settled middle of a stretch.
Closed-loop control arrives at a level over several frames rather than at once, so the front of a stretch is not yet the level the rest of it is. The tail is trimmed a little as well, because the level often starts to come off before the environment formally stops. What is left is the part that is all one thing.
Source code in src/visualdynamics/core/detect.py
compress
¶
compress(
history: TimeHistory,
overlap: float = 0.5,
frame_length: int | None = None,
) -> tuple[ndarray, int, int, float]
(levels in dB, hops per frame, hop in samples, scatter floor).
The one pass over the samples, and everything a caller needs to reason about the record afterwards. Separated out because more than one question gets asked of the same compression, and asking twice means reading three gigabytes twice.
Source code in src/visualdynamics/core/detect.py
analysis_window
¶
analysis_window(
history: TimeHistory,
want_frames: int = WANT_FRAMES,
overlap: float = 0.5,
frame_length: int | None = None,
keep: tuple[float, float] = KEEP,
) -> tuple[int, int]
(first sample, last sample) worth averaging in this history.
The whole record when there is nothing to choose between one part of it and another — a noise floor capture is a level like any other, and the point of finding it is to be able to put a PSD on it.
Source code in src/visualdynamics/core/detect.py
suggest
¶
suggest(
history: TimeHistory,
want_frames: int = WANT_FRAMES,
window: str = "hann",
overlap: float = 0.5,
frame_length: int | None = None,
keep: tuple[float, float] = KEEP,
) -> Averaging
The averaging to use for a record nobody has set parameters on.
Hann at half overlap, a frame about a second long, and as many frames as the settled stretch will carry — which is the answer to 'how many averages', rather than a number chosen in advance and hoped for.