visualdynamics.core.shocks¶
shocks
¶
Find the shocks in a recording, and the window to analyze each one in.
A shock test is a series of events in one continuous record: the
article is quiet, something hits it, it rings down, and it is quiet
again until the next one. Nothing in the file says where the events
are. detect finds the one stretch of a stationary run to average;
this finds every event in a transient run, and a record usually holds
more than one.
As in detect, the record is first compressed to one level per hop
(1 ms by default) across all channels at once, and everything after
that is decided on that short series. A shock record can be minutes
long at 50 kHz, so every later step works on thousands of points
instead of millions of samples.
Three choices set how the events are found:
- The trigger has hysteresis (a Schmitt trigger). An event starts when
the level rises
ARMdB (12) over the floor and ends only when it falls back underRELEASEdB (6), so a ringdown is followed down instead of being cut where it dips. With a single threshold, a ringdown crosses it on every cycle and one event comes back as forty. - The floor is a low quantile of the levels (the 10th percentile). Shocks are short and large, and averaging them into the floor would raise it by tens of dB; a low quantile stays in the quiet however large or numerous the shocks are.
- The thresholds are measured from the floor, not from each event's peak. A shock series is usually run up in level, so the last event can be ten times the first, and thresholds relative to each peak would give the events different windows for no physical reason.
The window is longer than the event at both ends. It opens before the
rise (by LEAD, 0.15 of the event's length), because an SRS oscillator has
to start from rest, and a window that opens partway into the pulse adds
a step that was never there. It closes past the release, once the
loudest channel's ringing has settled SETTLED dB (30) under its peak
or reached the quiet, because the low-frequency oscillators are still
responding to the ringdown above the floor. By default every event in
one record gets the same window length (COMMON_LENGTH).
Classes:
| Name | Description |
|---|---|
Shock |
One event's analysis window: when it opens and how long it runs. |
Functions:
| Name | Description |
|---|---|
hop_for |
Samples per hop of |
envelope |
(levels in dB, hop in samples): the record compressed to one |
loudest |
The loudest channel's level per hop of |
floor_of |
The quiet level the events are measured against, in dB, or None |
uniform |
Whether these windows are all one length, compared to |
with_added |
The series with one more window, placed where there is room. |
drag_settled |
What a drag of one window's edges means for the whole series. |
held_apart |
The windows with no two of them sharing a sample. |
same_length |
Every window given |
find |
The shocks in a record, as the windows to analyze them in. |
suggest |
|
Classes¶
Shock
dataclass
¶
One event's analysis window: when it opens and how long it runs.
start and duration are in seconds, start from the beginning
of the record; everything else is derived from the two. The window
already includes the lead-in and the tail, since an SRS is computed
from the window, and storing the window saves working it out again
each time it is used. A window with no length, or one that starts
before the record, is refused.
Methods:
| Name | Description |
|---|---|
start_sample |
The index of the sample the window opens on, at |
samples |
How many samples the window holds at |
bounds |
(first, last) sample indices at |
fits |
Whether the window ends inside a record of |
clipped |
This window moved and cut to lie inside a record of |
cut |
The samples this window covers, every channel, as a view. |
Attributes:
| Name | Type | Description |
|---|---|---|
stop |
float
|
When the window closes, in seconds from the start of the |
Attributes¶
Methods:¶
start_sample
¶
samples
¶
How many samples the window holds at sample_rate, at least
two, since one sample is not a transient.
bounds
¶
(first, last) sample indices at sample_rate, last
exclusive.
fits
¶
clipped
¶
clipped(samples: int, sample_rate: float) -> Shock
This window moved and cut to lie inside a record of samples
samples, at least two samples long.
Source code in src/visualdynamics/core/shocks.py
cut
¶
cut(history: TimeHistory) -> ndarray
The samples this window covers, every channel, as a view.
Functions:¶
hop_for
¶
Samples per hop of seconds at sample_rate, at least one.
envelope
¶
envelope(history: TimeHistory, seconds: float = HOP_SECONDS) -> tuple[ndarray, int]
(levels in dB, hop in samples): the record compressed to one
level per hop of seconds.
This is the compression the averaging detector uses
(detect.hop_levels): the median across channels, so one bad
channel does not decide the answer, with the mean removed within
each hop, so a DC offset is not read as level. The hop is
milliseconds instead of a large fraction of a second, because the
events last milliseconds. A channel counts as live if it carries
power anywhere, since a shock record's channels are quiet most of
the time.
Source code in src/visualdynamics/core/shocks.py
loudest
¶
loudest(history: TimeHistory, seconds: float = RING_HOP) -> ndarray
The loudest channel's level per hop of seconds, in dB (25 ms by
default, RING_HOP).
The end of each window is found on this series. Detection uses the median across channels, but an SRS is one spectrum per channel, and a window sized on the median would cut short the spectrum of one channel still ringing under twenty quiet ones. The maximum across channels shows that channel. The coarse hop is what lets the series show slow ringing at all: a hop's level is the variance within the hop, so content much slower than the hop is removed with the mean.
Source code in src/visualdynamics/core/shocks.py
floor_of
¶
floor_of(level: ndarray, quantile: float = FLOOR_QUANTILE, dynamic: float = DYNAMIC) -> float | None
The quiet level the events are measured against, in dB, or None for an empty series.
The floor is the quantile percentile of the levels (10 by
default), not a mean or a median: shocks are short and large, and
any average of a record holding them sits well above the quiet
between them. It is never more than dynamic dB (100) under the
loudest hop, so a synthesized record whose gaps are exact zeros
still gets thresholds that mean something (see DYNAMIC).
Source code in src/visualdynamics/core/shocks.py
uniform
¶
uniform(shocks: Sequence[Shock], places: int = 6) -> bool
Whether these windows are all one length, compared to places
decimal places of a second.
The common-length mode is read off the windows instead of being stored beside them. A stored copy could disagree with the lengths it describes, and then the checkbox and the data would say different things about the same series. The lengths are rounded because they are built by rounding to samples, and two windows meant to match can differ in the last bit.
Source code in src/visualdynamics/core/shocks.py
with_added
¶
The series with one more window, placed where there is room.
This is what the Add button does: the detector can miss events the
user knows about, below the arm threshold or close under a louder
neighbor. limit is the record's length in seconds. The new window
takes the median length of the series (the common length, when they
share one), or a tenth of limit when there are no windows yet. It
is centered in the largest stretch with no window in it, and is
never longer than 90 % of that stretch. With no room anywhere the series comes back unchanged.
The caller drags or types the new window into place from there.
Source code in src/visualdynamics/core/shocks.py
drag_settled
¶
drag_settled(shocks: Sequence[Shock], index: int, low: float, high: float, common: bool, limit: float | None) -> tuple[Shock, ...] | None
What a drag of one window's edges means for the whole series.
index is the dragged window, and low and high are its new
edges in seconds. common says the series is held at one length,
and limit is the record's length in seconds, or None. The 2-D
regions and the 3-D stage's handles both commit through this. Only
the dragged window moves. A resize under a common length gives
every window the new length (same_length), since the length is an
analysis choice and not a measurement; a move, or any change
without a common length, holds the dragged window inside the gap
its neighbors leave (held_apart).
Returns the settled series, or None when the drag changed nothing or asked for something the series has no room for, in which case the caller puts its handles back.
Source code in src/visualdynamics/core/shocks.py
held_apart
¶
The windows with no two of them sharing a sample.
Detection already keeps the windows apart, but a window can also be dragged on the plot or typed into the table, and without this one shock's window could be pulled over its neighbor and its spectrum computed from the neighbor's ringdown. This is the one place that rule is applied after detection.
moved is the index of the window the edit was aimed at, and it
decides which window gives way. Given it, that window is held inside
the gap its neighbors leave: an edge dragged into the next event
stops at that event's edge, and the event that was not touched does
not move. Without it, for a list that did not come from one edit,
the earlier window of each overlapping pair is cut back to where the
later one opens. Neither rule moves a window that was not in the
way, so one bad edit cannot cascade down the record.
Source code in src/visualdynamics/core/shocks.py
same_length
¶
same_length(shocks: Sequence[Shock], length: float, limit: float | None = None) -> tuple[Shock, ...]
Every window given length, in seconds, each keeping its own
start.
This is what a resize does to a series held at one length: pulling
any edge sets the length for all of them. The length is capped by
the closest pair of starts, and by limit (the record's length in
seconds, when given) less the last start. Capping only the window
that collided would leave the series no longer uniform, which is
what the mode exists to prevent, so the cap is shared. The starts
are not touched, because a start belongs to the event it was
measured from; only the length, an analysis choice, is held in
common. When the cap leaves no length, the windows come back
unchanged.
Source code in src/visualdynamics/core/shocks.py
find
¶
find(history: TimeHistory, hop_seconds: float = HOP_SECONDS, arm: float = ARM, release: float = RELEASE, quantile: float = FLOOR_QUANTILE, merge: float = MERGE, lead: float = LEAD, tail: float = TAIL, minimum: float = MIN_DURATION, most: int = MAX_EVENTS, common: bool = COMMON_LENGTH) -> tuple[Shock, ...]
The shocks in a record, as the windows to analyze them in.
The defaults are the module's constants: hop_seconds is the
detection hop in seconds (HOP_SECONDS), arm and release are dB
over the floor, quantile is the floor's percentile, events closer
than merge seconds are joined, lead and tail are fractions of
each event's length, windows shorter than minimum seconds are
dropped, and at most most windows are returned.
Empty when there are none, which is the right answer for a random
vibration run: its level never rises arm dB over its own floor
because there is no quiet in it.
common (the default, COMMON_LENGTH) gives every event the length
of the longest window, capped so that no two windows overlap and
none runs past the record. Pass False for a record whose events are
not the same kind of event.
Source code in src/visualdynamics/core/shocks.py
suggest
¶
suggest(history: TimeHistory, **kwargs: Any) -> tuple[Shock, ...]
find, or one window over the whole record when it finds nothing.
This is for a caller that needs something to analyze. A record with
no quiet in it has no shock the detector can point at, but it is
still a transient someone wants a spectrum of. Keyword arguments go
to find. Empty only for a record of fewer than two samples.