visualdynamics.core.filters¶
filters
¶
Filtering, integration and differentiation of records.
These make up the shock workflow's chain: an acceleration record is low-pass filtered, integrated to velocity, and integrated again to displacement. Each step is a derived object with provenance, so a changed filter corner marks everything after it in the chain out of date, the same way a changed averaging does.
The whole record is processed, never the shock windows one at a time, for three reasons. A zero-phase filter's edge transients land at the record's ends, far from the events, where filtering each window would put them on the data that matters. Integrating each window would give each one its own arbitrary velocity baseline. And a derived record that keeps the source's abscissa keeps its marks: the shock windows and averaging frames carry over unchanged, so a report numbers the same events on the displacement as on the acceleration.
Integration and differentiation are done in the time domain. The frequency-domain route (dividing by iω) treats the record as periodic, so the FFT turns the mismatch between the first and last samples into content. The trapezoid rule and the central difference treat the record as the finite length it is. Their cost is a known amplitude roll-off near Nyquist, sin(ωT)/(ωT) for the central difference, which is why the chain filters first: with a corner at a tenth of the sample rate, the round trip from acceleration to displacement and back to acceleration comes back within about 2 % RMS (about 4 % with a corner at a fifth).
Differentiation is np.gradient (second-order central differences),
not the exact inverse of the trapezoid rule. That inverse,
x[i] = 2*(y[i]-y[i-1])/dt - x[i-1], has a pole at Nyquist in its
recursion, and on real noise its error reached 280 % where the central
difference stayed under 4 %.
References
The filter forms are textbook; these are where they and the drift a double integration invents are set out.
- Oppenheim, A. V., & Schafer, R. W. (2010). Discrete-Time Signal Processing, 3rd ed. Prentice Hall. The bilinear transform, the filter forms and zero-phase forward-backward filtering.
- ISO 18431-1:2005, Mechanical vibration and shock - Signal processing - Part 1: General introduction, and ISO 18431-2 on time-domain integration.
- Boore, D. M., & Bommer, J. J. (2005). "Processing of strong-motion accelerograms: needs, options and consequences." Soil Dynamics and Earthquake Engineering. The drift a double integration invents, and what filtering does and does not cure.
Classes:
| Name | Description |
|---|---|
Filtering |
The pass band a record is filtered to: its edges and order. |
Functions:
| Name | Description |
|---|---|
threaded_sosfiltfilt |
|
design |
The filter at |
carrying_marks |
Copy the source's averaging and shock windows onto a derived |
filtered |
The record through its filter, every channel, zero phase. |
integrate |
One integration, acceleration to velocity or velocity to |
differentiate |
One differentiation, displacement to velocity or velocity to |
response |
(frequencies, magnitude in dB) of the filter as the record sees |
Classes¶
Filtering
dataclass
¶
The pass band a record is filtered to: its edges and order.
low and high are the edges of the pass band, in Hz. Set high
alone for a low-pass (everything below it passes), low alone for
a high-pass, and both for a band-pass. Which filter it is follows
from which edges are set (kind), so an invalid pairing, such as a
band-pass with one corner, cannot be written. The fields are
keyword-only, so a bare Filtering(200.0) is an error. At least one
edge is required, edges must be above zero, low must be below
high, and order must be at least 1.
The settings are frozen, like Averaging, because they are stored
with the history and their fields are its staleness fingerprint; a
setting that could change in place would make that fingerprint
wrong.
The filter is a Butterworth applied forward and backward
(sosfiltfilt). It has zero phase, so a shock's peak stays at its
measured time; a causal filter would shift and smear the front by
its group delay. The double pass doubles the effective order;
order is the design order of the underlying filter. A band-pass of
design order N has N poles per edge (scipy's convention), so each
skirt rolls off like the low- or high-pass of the same order.
Methods:
| Name | Description |
|---|---|
describe |
The filter in words, such as 'low-pass at 500 Hz'. The status |
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
str
|
'low-pass', 'high-pass' or 'band-pass', from which edges are |
Attributes¶
kind
property
¶
'low-pass', 'high-pass' or 'band-pass', from which edges are set; never stored beside them.
Methods:¶
describe
¶
The filter in words, such as 'low-pass at 500 Hz'. The status line, the staleness message and the report text all use this wording.
Source code in src/visualdynamics/core/filters.py
Functions:¶
threaded_sosfiltfilt
¶
scipy.signal.sosfiltfilt over a record's channels, one channel
per thread.
The filter releases the interpreter lock, so a thread pool over the rows is about six times faster on a 32-channel record. The results are the serial ones to the bit, since each row is the same call. A single row, an array under a million values, or filtering along another axis than the last goes to one serial call.
Source code in src/visualdynamics/core/filters.py
design
¶
design(filtering: Filtering, sample_rate: float) -> Any
The filter at sample_rate, as second-order sections from
scipy.signal.butter.
This is the one place a Filtering becomes a scipy design:
filtering a record, the response plot and the GUI previews all take
their sections from here, so they agree on what the settings mean.
An edge at or above the Nyquist frequency raises ValueError here,
so every caller refuses it the same way.
Source code in src/visualdynamics/core/filters.py
carrying_marks
¶
Copy the source's averaging and shock windows onto a derived record, and return the record.
The derived record has the same abscissa, so the marks still mean what they meant: the averaging frames and shock windows land on the same samples, and a report numbers the same events on the displacement as on the acceleration, or on the modal coordinates as on the channels, since a transform through a shape set combines samples and moves none. The filtering is not carried: the result of a filter is already filtered, and offering to filter it again with the same settings would be a button that does nothing.
Source code in src/visualdynamics/core/filters.py
filtered
¶
filtered(data: TimeHistory, filtering: Filtering) -> TimeHistory
The record through its filter, every channel, zero phase.
Every channel is filtered whatever it measures: a filter acts on
the waveform, not the quantity, and the drive force belongs at the
same bandwidth as the responses it drove. The averaging and shock
windows are carried over (carrying_marks). A record with uneven
sampling, or an edge at or above Nyquist, is refused.
Source code in src/visualdynamics/core/filters.py
integrate
¶
integrate(data: TimeHistory, drift_corner: float | None = DRIFT_CORNER) -> TimeHistory
One integration, acceleration to velocity or velocity to displacement, channel by channel.
The cumulative trapezoid rule, with drift control: each channel's
mean is removed before integrating, and a second-order Butterworth
high-pass at drift_corner in Hz (DRIFT_CORNER, 2 Hz) is applied
forward and backward afterward. The mean is removed first because a
bias integrates to a ramp, and the high-pass's edge transients grow
with how far the ramp runs. Below the corner nothing in the data
tells drift from true motion. Pass None to integrate raw, which
says the record's own low end is trusted, drift and all.
Records of quantities the chain cannot take (forces, voltages, anything undefined) are left out of the result. A record with no acceleration or velocity channels is refused, as is a drift corner that is not between zero and Nyquist.
Source code in src/visualdynamics/core/filters.py
differentiate
¶
differentiate(data: TimeHistory) -> TimeHistory
One differentiation, displacement to velocity or velocity to acceleration, channel by channel.
Second-order central differences (np.gradient); the module
docstring says why the exact inverse of the trapezoid rule is not
used. There is no drift control, since differentiation removes
offsets instead of growing them. Records of other quantities are
left out, and a record with no displacement or velocity channels
is refused.
Source code in src/visualdynamics/core/filters.py
response
¶
response(filtering: Filtering, sample_rate: float, lines: int = 400, *, frequencies: Any = None) -> tuple[ndarray, ndarray]
(frequencies, magnitude in dB) of the filter as the record sees it.
This is the squared magnitude |H|², not the single-pass response.
filtered runs the filter forward and backward for zero phase, so
the data is multiplied by |H| twice. A Butterworth is -3 dB at its
nominal corner by definition, so the zero-phase pair is -6.02 dB
there, which is not what the settings suggest; for a fourth-order
low-pass (the default order) the -3 dB point is at about 0.90 of
the corner. Plotting |H| would draw a filter that is not the one
applied.
The lines frequencies are log-spaced from RESPONSE_DECADES (3)
decades below Nyquist up to Nyquist, extended down to half the
lowest edge when an edge is lower than that. Edges at or above
Nyquist are clamped just under it instead of refused, so a curve
can be drawn while the settings are being edited. Values are
floored at -240 dB.
frequencies, when given, are the frequencies the response is
read at instead, in Hz, so a reading can ask for the gain at the
lines it names (core.resample reads it at the span and at the
lowest frequency that folds into it).