Skip to content
This documentation describes version 4.0.0, which is not released yet. The current version on PyPI is 3.3.0 and does not carry everything described here.

vibration.structural.impact_mobility

Mechanical mobility measured by impact excitation (ISO 7626-5:2019).

ISO 7626-5 measures the same frequency-response functions as ISO 7626-2, but excites the structure with a hammer or another impactor that is not attached to it. Each record holds one impact and the free decay it leaves behind, and that shape is what the part is about: the force occupies a small fraction of the record, so the noise around it matters (8.5.1); the response may not have died away by the end of the record, so the spectrum leaks (8.3, 8.5.2); a second impact inside the record cuts notches into the force spectrum (6.4); and the averaging runs over impacts rather than over segments of one long record (8.6). This module holds those pieces and reuses the FRF machinery of .mechanical_mobility for the rest.

Spectra. The records are sampled at fs and transformed with the discrete Fourier transform scaled by the sampling interval, , which for a transient wholly inside the record samples its continuous Fourier transform (8.3). The force energy spectral density is , in N²·s/Hz: the power spectral density of 3.3, , multiplied by the record length as 3.4 prescribes.

Averaging (8.6). Over impacts at one point the estimate is the averaged cross-spectrum of response and force divided by the averaged auto-spectrum of the force,

with the ordinary coherence of 9.1 beside it. The FRF comes out in the kind of the response channel (accelerance, mobility or dynamic compliance) and is converted to mobility with .convert_frf.

Windows (8.5). The force window has unity gain over the part of the record that holds the force pulse and its filter response and sets the rest to exactly zero (8.5.1). The exponential window starts at unity and adds a known decay to the data (8.3 a), 8.5.2). Applied to force and response alike, it replaces every pole of the impulse response by (Annex A, Formula (A.2)), so a mode appears more damped than it is, and Formula (A.3) takes the added damping away again:

with the damping ratio estimated from the windowed data and the damped natural frequency in rad/s. The corrected mobility is synthesized from the corrected modes, and for a lightly damped, well separated mode the measured peak is multiplied by . Annex A leaves the damping estimator open; single_mode_fit is the one this library offers, a rational-fraction fit of one mode over a band.

Formula (A.3) is the first-order form of the pole shift the annex states in words. The synthesis here moves the pole itself, which is exact, and the correction reports both: ExponentialWindowCorrection.damping_ratio is (A.3) as printed and ExponentialWindowCorrection.exact_damping_ratio is the damping of the shifted pole; they differ by about .

Records. Every function that takes a record takes a bare array with its sample rate, or a Signal, which brings its own rate (an explicit fs that disagrees is refused). A calibration factor the Signal carries is deliberately not applied: the records of this part are forces in newtons and motions in m/s², m/s or m, not pressures. The checks hand the samples they judged back as bare arrays, in the unit they arrived in.

Checks. Every record check returns a verdict object with passes: check_double_hit (6.4), check_force_spectrum (6.2, 6.3), check_overload (8.4), check_response_decay (8.3, 8.5.2) and check_coherence (9.1); verify_channel_match judges the channel-to-channel match of the analyser (8.1). The operational calibration of 7.2 is the rigid-mass check of ISO 7626-2:2015, 7.5.2, which 7.2 calls essentially the same procedure: .rigid_mass_calibration_check.

The part prints no worked example, so the implementation is anchored in closed form: a single-degree-of-freedom structure struck by a smooth pulse gives its mobility back from the averaged estimate, and its damping and mobility back after exponential windowing and the Annex A correction.

Auto-generated from the source docstrings by scripts/generate_api_docs.py (make api-docs). Do not edit by hand.

Constant (float).

CHANNEL_MAGNITUDE_TOLERANCE = 0.05

Constant (float).

CHANNEL_PHASE_TOLERANCE_DEG = 5.0
ChannelMatchVerification(frequencies: np.ndarray, response: np.ndarray)

Do two analyser channels match in gain and phase? (8.1)

The channel-to-channel match of the filters and the analyser is checked by connecting one broad-band signal to both channels and measuring the frequency response between them: its magnitude should equal unity within +/- 5 % over the frequency range of interest and its phase zero within +/- 5 degrees (8.1).

Attributes

NameDescription
frequenciesFrequencies in the range of interest, in hertz.
responseComplex frequency response between the channels.

ChannelMatchVerification.magnitude_deviation

Section titled “ChannelMatchVerification.magnitude_deviation”

property

Relative deviation of the magnitude from unity, |H| - 1.

property

Per frequency, whether the magnitude is within +/- 5 % of unity.

The magnitude is compared with the bounds 1 - 0,05 and 1 + 0,05 themselves, so a response of exactly 0,95 or 1,05 is within them.

property

Whether both requirements hold at every frequency.

property

Phase of the response, in degrees.

property

Per frequency, whether the phase is within +/- 5 degrees of zero.

ChannelMatchVerification.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes | np.ndarray

Plot the magnitude and phase deviations against their tolerances.

With no ax a two-panel figure is drawn; with ax only the magnitude deviation.

Requires matplotlib (pip install phonometry[plot]).

Parameters

NameDescription
axExisting axes, or None to create a figure.
languageLabel language, "en" (default) or "es".
kwargsForwarded to the magnitude-deviation curve.

Returns: The axes, or the two-axes array.

check_coherence(
result: ImpactMobilityResult,
*,
frequency_range_hz: tuple[float, float],
exclude_hz: Sequence[tuple[float, float]] = (),
minimum_coherence: float = 0.9,
minimum_records: int = 5,
) -> CoherenceCheck

Is the averaged estimate’s coherence high over the range of interest? (9.1)

The check passes when the coherence exceeds minimum_coherence (0,9, what 9.1 calls high) at every frequency of the range that is not excluded, and at least minimum_records impacts were averaged (five, the lower end of the five to ten 9.1 says a high coherence needs). A single impact has a coherence of 1 by construction and fails the second condition.

8.6 finds three to five impacts usually enough to verify data quality in a low-noise environment; the coherence estimate itself, 9.1 adds, needs five to ten records to be trusted. Averaging five impacts satisfies both, and a lower minimum_records states that fewer were judged enough.

Parameters

NameDescription
resultThe averaged estimate, from impact_mobility.
frequency_range_hz(f_low, f_high), in hertz.
exclude_hzBands (f_low, f_high) to leave out of the verdict, such as anti-resonances (9.1, NOTE 1) (Default: none).
minimum_coherenceThe coherence to exceed (Default: 0.9).
minimum_recordsThe fewest impacts to accept, a whole number of at least 1 (Default: 5).

Returns: A CoherenceCheck.

Raises

ExceptionWhen
ValueErrorfor an invalid range, exclusion or limit, a minimum_records that is not a whole number of at least 1, or exclusions that leave no frequency of the range to judge.
check_double_hit(
force: SignalInput,
fs: float | None = None,
*,
threshold_ratio: float = 0.1,
) -> DoubleHitCheck

Does the force record hold one impact, or a double hit? (6.4)

An impact is a run of samples whose force exceeds threshold_ratio times the largest force in the record, in the direction of that largest force; the check passes when there is exactly one. Run it on the unwindowed, and preferably unfiltered, force: 6.4 recommends watching the unfiltered signal so the anti-aliasing filter does not hide a secondary impact in the ringing of the primary one, and forbids using a force window to remove one.

The threshold is this library’s, not the part’s, which only says that a small second impact shows as a slight ripple and that moderate dips may be tolerated. At the default of 0,1 an undetected second pulse of the same shape as the first ripples the force spectrum by at most dB peak to notch (DoubleHitCheck.ripple_db). A softer, longer rebound, the usual hammer bounce, carries more of the low-frequency spectrum than its peak says: it can stay under the threshold and still ripple the spectrum by several decibels. That is why 6.4 finds multiple impacts most easily in the frequency domain, which check_force_spectrum reads.

A filtered record has a limit of its own. When the spectrum of the pulse reaches the cut-off of the anti-aliasing filter, the ringing the filter leaves after the pulse can itself rise above the threshold, most of all with a steep elliptic or Chebyshev filter, and a single impact then reads as several. The unfiltered force avoids both effects.

Parameters

NameDescription
forceOne force record, 1-D, in N. Accepts a phonometry.io.Signal for its rate; a calibration factor it carries is deliberately not applied, because this record is a force in newtons and not a pressure.
fsSample rate, in Hz. Required for a bare array; a Signal brings its own, and an explicit value that disagrees with it raises instead of silently winning.
threshold_ratioFraction of the largest force an excursion must exceed to count as an impact, in (0, 1) (Default: 0.1).

Returns: A DoubleHitCheck.

Raises

ExceptionWhen
ValueErrorfor a record that is not 1-D and finite, a non-positive sample rate, a threshold outside (0, 1), or a record with no force in it.
check_force_spectrum(
force: SignalInput,
fs: float | None = None,
*,
frequency_range_hz: tuple[float, float],
max_drop_db: float,
) -> ForceSpectrumCheck

Is the force spectrum flat enough across the frequency range of interest?

Computes the energy spectral density of one force record (3.3, 3.4) and reads how far it falls between its highest and lowest value inside the range. The check passes when that fall is at most max_drop_db. The part gives no number for it, so the caller states the one their practice uses; the share of force energy above the range, which 6.3 asks to keep small, is reported beside it as ForceSpectrumCheck.energy_fraction_above.

Parameters

NameDescription
forceOne force record, 1-D, in N (windowed or not, as it will be processed). Accepts a phonometry.io.Signal for its rate; a calibration factor it carries is deliberately not applied, because this record is a force in newtons and not a pressure.
fsSample rate, in Hz. Required for a bare array; a Signal brings its own, and an explicit value that disagrees with it raises instead of silently winning.
frequency_range_hz(f_low, f_high), the frequency range of interest, in hertz.
max_drop_dbThe largest fall across the range to accept, in dB.

Returns: A ForceSpectrumCheck.

Raises

ExceptionWhen
ValueErrorfor an invalid record, sample rate, range or limit.
check_overload(
record: SignalInput,
fs: float | None = None,
*,
full_scale: float,
) -> OverloadCheck

Did one channel of the record stay below its full scale? (8.4)

Parameters

NameDescription
recordOne record, 1-D, in any unit. Accepts a phonometry.io.Signal for its rate; a calibration factor it carries is deliberately not applied, so the samples stay in the unit the channel recorded them in, the unit of its full scale.
fsSample rate, in Hz. Required for a bare array; a Signal brings its own, and an explicit value that disagrees with it raises instead of silently winning.
full_scaleThe manufacturer’s maximum for linear operation, in the unit of the record.

Returns: An OverloadCheck, which passes when every sample stays strictly below the full scale.

Raises

ExceptionWhen
ValueErrorfor an invalid record, sample rate or full scale.
check_response_decay(
response: SignalInput,
fs: float | None = None,
*,
exponential_window: bool = False,
segment_s: float | None = None,
start_s: float = 0.0,
force: SignalInput | None = None,
) -> ResponseDecayCheck

Has the response decayed enough by the end of the record? (8.3, 8.5.2)

Without an exponential window the response should end at about 1 % of its highest peak (8.3, 8.5.2); with one, its natural decay should reach 25 % or less, the general guideline of 8.5.2. The level at the end is read at the last sample and compared with the figure to a whole per cent, so any level below 1,5 % passes “about 1 %” (see ResponseDecayCheck). 8.3 calls the 1 % a compromise, and the check is one-sided: it asks whether the response has decayed far enough, not whether it decayed too far. Pass the response as recorded, before any window.

Parameters

NameDescription
responseOne response record, 1-D, in any unit. Accepts a phonometry.io.Signal for its rate; a calibration factor it carries is deliberately not applied, because this record is a motion and not a pressure.
fsSample rate, in Hz. Required when the records are bare arrays; a Signal brings its own, and an explicit value, or a force Signal, that disagrees with it raises instead of silently winning.
exponential_windowWhether the record is to be processed with an exponential window (Default: False).
segment_sLength of the segments the levels are read over, in seconds; make it at least one period of the lowest mode (Default: None, one tenth of the record).
start_sTime from which the highest peak is sought, in seconds (Default: 0, the whole record, as the clause reads).
forceThe force record of the same impact, same length (Default: None). When given, the highest peak is sought only after the force pulse, where the force has fallen back to 1 % of its peak, so a driving-point acceleration is judged on its free decay and not on the impact it follows (the mass line). The later of this and start_s is used. A Signal likewise, its factor not applied.

Returns: A ResponseDecayCheck.

Raises

ExceptionWhen
ValueErrorfor an invalid record, sample rate, segment or start, a response that is zero at every sample or at every sample from the start on (a dead channel has measured nothing, so it has not decayed either), or a force record of another length, without force or whose pulse does not end inside the record.

Constant (int).

COHERENCE_RECORDS = 5
CoherenceCheck(
frequencies: np.ndarray,
coherence: np.ndarray,
judged: np.ndarray,
*,
impacts: int,
minimum_coherence: float = 0.9,
minimum_records: int = 5,
)

Is the coherence high, over enough impacts to trust it? (9.1)

The coherence expresses how linearly the response follows the force at each frequency; a value below 1 flags possible poor data (9.1). The clause calls a coherence above 0,9 high, and says only a few records, five to ten, are then needed for high statistical confidence in its estimate. NOTE 1 adds that low coherence at an anti-resonance is not generally a concern: the response is near the noise floor there. Such frequencies can be excluded from the judgement.

Attributes

NameDescription
frequenciesFrequencies in the range of interest, in hertz.
coherenceOrdinary coherence at each.
judgedPer frequency, whether it enters the verdict (False where excluded as an anti-resonance).
impactsNumber of impacts averaged.
minimum_coherenceThe coherence each judged frequency must exceed.
minimum_recordsThe fewest impacts the check accepts.

property

Whether enough impacts were averaged for the coherence to be trusted.

property

Per frequency, whether the coherence exceeds the minimum.

property

Whether every judged frequency is high and the records are enough.

CoherenceCheck.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot the coherence against its minimum, excluded frequencies shaded.

Requires matplotlib (pip install phonometry[plot]).

Parameters

NameDescription
axExisting axes, or None to create a figure.
languageLabel language, "en" (default) or "es".
kwargsForwarded to the coherence curve.

Returns: The axes.

property

Normalized random error of the FRF magnitude, in percent (ISO 7626-2 Annex A).

NaN where the coherence is zero.

DoubleHitCheck(
force: np.ndarray,
fs: float,
impact_indices: np.ndarray,
*,
threshold_ratio: float,
)

Was there more than one impact in the force record? (ISO 7626-5, 6.4)

If more than a single impact occurs within the record, the Fourier transforms of the pulses tend to cancel at certain frequencies and cut sharp notches into the force spectrum (Figure 5), where the low signal-to-noise ratio spoils the mobility. For two pulses of the same shape, the second times the first and later, the spectrum is the single pulse’s times : it ripples with a period of in frequency between and , so the notches are deep only when the second impact is about as strong as the first. That is why 6.4 says a small second impact shows as a slight ripple, and why moderate dips “may normally be tolerated”.

Attributes

NameDescription
forceThe force record judged, in N, as a bare array (a Signal passed in is read without its calibration factor, and its samples are what is kept).
fsIts sample rate, in Hz.
impact_indicesSample index of the peak of every impact found, in time order; the first entry is not necessarily the largest.
threshold_ratioThe fraction of the largest peak an excursion had to exceed to count as an impact.

property

Time from the primary impact to the largest secondary one, in s.

None for a single impact.

property

Force at the peak of every impact, in N.

property

Time of the peak of every impact, in seconds.

property

How many impacts the record holds.

property

Spacing of the notches the two largest impacts cut, .

None for a single impact.

property

Whether the record holds a single impact.

DoubleHitCheck.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes | np.ndarray

Plot the force record with its impacts, and its energy spectral density.

With no ax a two-panel figure is drawn: the time history with every impact marked, and the energy spectral density with the notches the two largest impacts cut (Figure 5). With ax only the time history is drawn on it.

Requires matplotlib (pip install phonometry[plot]).

Parameters

NameDescription
axExisting axes, or None to create a figure.
languageLabel language, "en" (default) or "es".
kwargsForwarded to the force curve.

Returns: The axes, or the two-axes array.

property

Peak-to-notch ripple of the force spectrum, in dB.

for a secondary impact r times the primary one, the same shape and the only other; 0 dB for a single impact and infinite for two equal ones, whose notches go to zero.

property

Largest secondary peak over the primary one (0 for a single impact).

property

Time of every sample of the record, in seconds.

energy_spectral_density(
record: SignalInput,
fs: float | None = None,
) -> tuple[np.ndarray, np.ndarray]

One-sided energy spectral density of a transient record (3.3, 3.4).

, with the discrete Fourier transform scaled by the sampling interval: the power spectral density of 3.3, , multiplied by the record length as 3.4 prescribes for a transient wholly inside the record. For a force in newtons the unit is N²·s/Hz, the unit of Figures 3, 4, 5, 7 and 8. The DC bin and, for an even record, the Nyquist bin are not doubled.

Parameters

NameDescription
recordOne record, 1-D. Accepts a phonometry.io.Signal for its rate; a calibration factor it carries is not applied.
fsSample rate, in Hz. Required for a bare array; a Signal brings its own, and an explicit value that disagrees with it raises instead of silently winning.

Returns: (frequencies, esd): the DFT bin frequencies from 0 Hz to the Nyquist frequency, in hertz, and the density at each, in the record’s unit squared times s/Hz.

Raises

ExceptionWhen
ValueErrorfor a record that is not 1-D and finite, or a non-positive sample rate.
exponential_decay_rate(
n_samples: int,
fs: float,
*,
final_value: float,
) -> float

The decay rate of an exponential window that ends the record at a value.

8.5.2 describes an exponential window by the value it decays to at the end of the record: Figure 10 uses one that decays to 5 % of its initial value. The end of the record is its last sample, at , so and exponential_window(n_samples, fs, decay_rate_per_s=a)[-1] is final_value.

Parameters

NameDescription
n_samplesRecord length N, in samples.
fsSample rate, in Hz.
final_valueWindow value at the last sample, in (0, 1] (dimensionless; 0.05 for the window of Figure 10).

Returns: The decay rate a, in 1/s.

Raises

ExceptionWhen
ValueErrorfor fewer than 16 samples, a non-positive sample rate or a final value outside (0, 1].
exponential_window(
n_samples: int,
fs: float,
*,
decay_rate_per_s: float,
) -> np.ndarray

The exponential window of 8.5.2: .

The window has an initial value of unity at the start of the record and decreases exponentially towards its end, adding a known amount of artificial decay to the data (8.3 a), 8.5.2). Applied to force and response alike it moves every pole of the impulse response by -a (Annex A, Formula (A.2)); exponential_window_correction takes the added damping away again.

Parameters

NameDescription
n_samplesRecord length N, in samples.
fsSample rate, in Hz.
decay_rate_per_sDecay rate a, in 1/s (>= 0; 0 is no window). exponential_decay_rate finds the rate that ends the record at a stated value.

Returns: The window, one value per sample, .

Raises

ExceptionWhen
ValueErrorfor fewer than 16 samples, a non-positive sample rate or a negative decay rate.
exponential_window_correction(
damped_natural_frequency_hz: ArrayLike,
apparent_damping_ratio: ArrayLike,
*,
exponential_decay_rate_per_s: float,
) -> ExponentialWindowCorrection

Remove an exponential window’s damping from estimated modes (Formula (A.3)).

for each mode, with the damped natural frequency. Estimate from the windowed data with any curve fitter (Annex A leaves the method open; single_mode_fit is one).

Parameters

NameDescription
damped_natural_frequency_hzDamped natural frequency of each mode, in hertz (scalar or 1-D).
apparent_damping_ratioDamping ratio estimated from the windowed data, per mode, in (0, 1).
exponential_decay_rate_per_sDecay rate a of the window, in 1/s.

Returns: An ExponentialWindowCorrection.

Raises

ExceptionWhen
ValueErrorfor mismatched or invalid inputs, or a window whose damping reaches a mode’s apparent damping.
ExponentialWindowCorrection(
damped_natural_frequency_hz: np.ndarray,
apparent_damping_ratio: np.ndarray,
*,
exponential_decay_rate_per_s: float,
)

The damping an exponential window added, taken away (Annex A).

Formula (A.3), , per mode, with the damping ratio estimated from the windowed data, the window’s decay rate and the damped natural frequency in rad/s. For a lightly damped, well separated mode the measured peak is multiplied by to give the true one.

Attributes

NameDescription
damped_natural_frequency_hzDamped natural frequency of each mode, , in hertz.
apparent_damping_ratioDamping ratio of each mode estimated from the windowed data, .
exponential_decay_rate_per_sThe window’s decay rate a, in 1/s.

property

True damping ratio of each mode, Formula (A.3): .

ExponentialWindowCorrection.exact_damping_ratio

Section titled “ExponentialWindowCorrection.exact_damping_ratio”

property

Damping ratio of the pole moved back by a, of which (A.3) is the first order.

The window replaces the pole by (Annex A, after Formula (A.2)), so the decay rate of the windowed mode is at the same damped frequency. From , the true ratio is with . It and (A.3) differ by about .

ExponentialWindowCorrection.peak_correction_factor

Section titled “ExponentialWindowCorrection.peak_correction_factor”

property

Factor on the measured peak of each mode, .

ExponentialWindowCorrection.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot each mode’s apparent damping split into the true and the window’s part.

Requires matplotlib (pip install phonometry[plot]).

Parameters

NameDescription
axExisting axes, or None to create a figure.
languageLabel language, "en" (default) or "es".
kwargsForwarded to the bars of the true damping.

Returns: The axes.

ExponentialWindowCorrection.window_damping_ratio

Section titled “ExponentialWindowCorrection.window_damping_ratio”

property

The damping the window added to each mode, .

force_window(
n_samples: int,
fs: float,
*,
width_s: float,
start_s: float = 0.0,
taper_s: float = 0.0,
) -> np.ndarray

The force window of 8.5.1: unity over the pulse, exactly zero elsewhere.

The window has unity gain for the part of the record that holds the force signal, including the filter response, and sets the remaining samples to exactly zero before Fourier processing. It removes broad-band noise from the force auto-spectrum without distortion as long as none of the force data is attenuated (8.5.1, Figure 7). A rectangular window spreads periodic noise and DC offset over a wide band (Figure 8), and 8.5.1 notes that a smooth transition between zero and one narrows that spread: taper_s adds a half-cosine ramp of that length on each side of the unity part, outside it, so the pulse itself is never attenuated. The shape of the ramp is this library’s choice; the part does not prescribe one.

Never use a force window to cut a second impact out of the record (6.4): the response still carries it. check_double_hit finds one.

Parameters

NameDescription
n_samplesRecord length N, in samples.
fsSample rate, in Hz.
width_sLength of the unity part, in seconds (> 0). Sample n is inside it when start_s <= n / fs < start_s + width_s.
start_sWhere the unity part begins, in seconds (Default: 0, the beginning of the record, which keeps the pre-trigger data of 8.2).
taper_sLength of each half-cosine ramp, in seconds (Default: 0, a rectangular window).

Returns: The window, one value per sample.

Raises

ExceptionWhen
ValueErrorfor fewer than 16 samples, a non-positive sample rate or width, a negative start or taper, or a window, its falling ramp included, that is still open at the last sample of the record, at .
ForceSpectrumCheck(
frequencies: np.ndarray,
energy_spectral_density: np.ndarray,
*,
frequency_range_hz: tuple[float, float],
max_drop_db: float,
)

Does the force spectrum cover the frequency range of interest? (6.2, 6.3)

The spectrum of a force pulse is a main lobe at low frequency followed by side lobes that fall rapidly, with a usable bandwidth inversely proportional to the pulse duration (6.2). The tip stiffness and the impactor mass set that bandwidth (6.3), and a double hit cuts notches into it (6.4). The check reads how far the energy spectral density falls across the frequency range of interest, from its highest to its lowest value there, against a limit the caller states: the part prints none.

Attributes

NameDescription
frequenciesFrequencies above 0 Hz, in hertz.
energy_spectral_densityOne-sided force energy spectral density, in N²·s/Hz (3.4).
frequency_range_hzThe frequency range of interest (3.2), in hertz.
max_drop_dbThe largest fall across the range the check accepts, in dB.

property

How far the density falls across the range, highest to lowest, in dB.

property

Share of the force energy above the range of interest (6.3).

property

Per frequency, whether it lies in the frequency range of interest.

property

The density in dB re its highest value inside the range.

property

Whether the density falls no further than the limit across the range.

ForceSpectrumCheck.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot the force energy spectral density against the range and the limit.

Requires matplotlib (pip install phonometry[plot]).

Parameters

NameDescription
axExisting axes, or None to create a figure.
languageLabel language, "en" (default) or "es".
kwargsForwarded to the density curve.

Returns: The axes.

Constant (float).

HIGH_COHERENCE = 0.9
impact_mobility(
force: SignalInput,
response: SignalInput,
fs: float | None = None,
*,
response_quantity: ResponseQuantity = 'acceleration',
force_window_s: float | None = None,
force_window_start_s: float = 0.0,
force_window_taper_s: float = 0.0,
exponential_decay_rate_per_s: float = 0.0,
exponential_on_force: bool = True,
driving_point: bool = True,
frequency_range_hz: tuple[float, float] | None = None,
) -> ImpactMobilityResult

Mobility from impact records, averaged as 8.6 prescribes.

Each row of force and response is one impact at the same point, triggered as 8.2 describes. The force is multiplied by the force window (8.5.1) and both records by the exponential window (8.5.2), both are transformed, and the estimate is the averaged cross-spectrum of response and force over the averaged auto-spectrum of the force (8.6), with the ordinary coherence of 9.1 beside it.

The DFT runs up to the Nyquist frequency, but a force pulse has a finite usable bandwidth (6.2) and the anti-aliasing filter cuts below the Nyquist frequency (8.3), so the top of the spectrum is noise divided by almost no force. frequency_range_hz keeps the estimate to the frequency range of interest of 3.2, the span at which mobility data are to be obtained, and everything built on the result (its plot, its fiche, its fit) stays there.

Where the force spectrum vanishes the frequency response is undefined, and a range that holds such a frequency is refused. A spectrum that vanishes in exact arithmetic, at a notch between two equal impacts for one, comes out of the transform at the level of its rounding rather than at zero, so any bin at or below that level counts as vanished.

With exponential_on_force=False the exponential window multiplies the response only, and the force of each impact is multiplied instead by the window’s value at the instant of its peak, the approximation Annex A describes for the common practice of windowing the response alone.

A force window that zeroes an impact the record holds raises ImpactExcitationWarning: 6.4 forbids removing a second impact that way.

Parameters

NameDescription
forceForce records, in N: one record (1-D) or one per row (2-D). Accepts a phonometry.io.Signal, one channel per impact, for its rate; a calibration factor it carries is deliberately not applied, because this record is a force and not a pressure.
responseResponse records, same shape, in m/s², m/s or m as response_quantity says; a Signal likewise, its factor not applied.
fsSample rate, in Hz. Required when both records are bare arrays; a Signal brings its own, and an explicit value, or a second Signal, that disagrees with it raises.
response_quantity"acceleration" (default), "velocity" or "displacement".
force_window_sWidth of the unity part of the force window, in seconds (Default: None, no force window).
force_window_start_sWhere the unity part begins, in seconds (Default: 0).
force_window_taper_sHalf-cosine ramp on each side, in seconds (Default: 0).
exponential_decay_rate_per_sDecay rate a of the exponential window, in 1/s (Default: 0, none).
exponential_on_forceWindow the force too (Default: True, the exact case of Formula (A.2)).
driving_pointWhether force and response are co-located (Default: True).
frequency_range_hz(f_low, f_high), the frequency range of interest in hertz, inside the Nyquist frequency (Default: None, every bin above 0 Hz).

Returns: An ImpactMobilityResult.

Raises

ExceptionWhen
ValueErrorfor records of different shapes, not finite or too short, a non-positive sample rate, an unknown response quantity, an invalid window, a range that holds no bin above 0 Hz, or a force spectrum that vanishes at some frequency of the range.

Warns

WarningWhen
ImpactExcitationWarningwhen the force window removes an impact.

Advisory when an ISO 7626-5 record breaks a rule of the part.

ImpactMobilityResult(
frequencies: np.ndarray,
mobility: np.ndarray,
coherence: np.ndarray,
force_energy_spectral_density: np.ndarray,
*,
impacts: int,
exponential_decay_rate_per_s: float = 0.0,
response_quantity: ResponseQuantity = 'acceleration',
driving_point: bool = True,
)

Mobility averaged over impacts at one point (ISO 7626-5:2019, 8.6).

Attributes

NameDescription
frequenciesDFT bin frequencies above 0 Hz, in hertz: up to the Nyquist frequency, or across the frequency range of interest the estimate was kept to (3.2).
mobilityComplex mobility Y per frequency, in m/(N·s): the averaged estimate of 8.6 as measured, exponential window included. Its resonance peaks are therefore lower than the structure’s when a window was used; fit_mode and the correction of Annex A restore them.
coherenceOrdinary coherence per frequency (9.1); identically 1 for a single impact.
force_energy_spectral_densityAveraged one-sided energy spectral density of the windowed force, in N²·s/Hz (3.4).
impactsNumber of impacts averaged.
exponential_decay_rate_per_sDecay rate a of the exponential window applied, in 1/s (0 for none).
response_quantityWhat the response channel measured: "acceleration", "velocity" or "displacement".
driving_pointTrue if force and response are co-located.
ImpactMobilityResult.fit_mode(
band_hz: tuple[float, float],
) -> SingleModeFitResult

Fit the mode in a band, knowing the window this estimate carries.

The fit runs on the FRF kind the response channel measured, because that is the record the exponential window multiplied, and the result carries the window’s decay rate so that SingleModeFitResult.correction applies Annex A.

Parameters

NameDescription
band_hz(f_low, f_high) around one resonance, in hertz.

Returns: A SingleModeFitResult.

property

Mobility magnitude |Y|, in m/(N·s).

property

The FRF kind the response channel gives: accelerance, mobility or receptance.

property

The estimate as a .MobilityResult (ISO 7626-1 vocabulary).

property

Mobility phase, in radians.

ImpactMobilityResult.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes | np.ndarray

Plot the averaged mobility and its coherence.

With no ax a two-panel figure is drawn: |Y(f)| on log-log axes and the coherence beneath it. With ax only the magnitude is drawn.

Requires matplotlib (pip install phonometry[plot]).

Parameters

NameDescription
axExisting axes, or None to create a figure.
languageLabel language, "en" (default) or "es".
kwargsForwarded to the magnitude curve.

Returns: The axes, or the two-axes array.

ImpactMobilityResult.report(
path: str,
*,
metadata: ReportMetadata | None = None,
engine: str = 'reportlab',
verbose: bool = False,
language: str = 'en',
) -> str

Render the ISO 7626 mobility fiche for an impact measurement to a PDF.

The one-page fiche of .MobilityResult.report, with the standard-basis line naming ISO 7626-5:2019 and the characteristic points extended by the number of impacts averaged, the exponential window and the coherence at the mobility peak.

Parameters

NameDescription
pathDestination path of the PDF file.
metadataOptional ReportMetadata.
engineRendering back end; only "reportlab" is supported.
verboseAccepted for a uniform .report() signature; the fiche has a single body layout, so it has no effect.
language"en" (default) or "es".

Returns: The written path as a str.

Raises

ExceptionWhen
ValueErrorIf engine is not "reportlab" or language is unknown.
ImportErrorIf reportlab or matplotlib is not installed.
ImpactMobilityResult.to(target: str) -> np.ndarray

Convert the mobility to another FRF kind (see .convert_frf).

OverloadCheck(record: np.ndarray, fs: float, *, full_scale: float)

Did the record stay inside the linear range of its channel? (8.4)

Impact excitation risks saturating the measurement system, because the signals carry out-of-band energy and the dynamic range is used to its limit (8.4). Saturation is not always visible as a clipped waveform, so 8.4 asks that the manufacturer’s maximum for linear operation be observed, and warns that filtered digital records give poor definition of the actual waveform: pass the widest-band record available. A pass here is necessary, not sufficient.

Attributes

NameDescription
recordThe record judged, in its own unit, as a bare array (a Signal passed in is read without its calibration factor, and its samples are what is kept).
fsIts sample rate, in Hz.
full_scaleThe largest magnitude the channel handles linearly, in the unit of the record.

property

How many samples reach the full scale.

property

Distance from the peak to the full scale, in dB (infinite for silence).

property

Whether every sample stays below the full scale.

property

Largest magnitude in the record, in its unit.

OverloadCheck.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot the record against its full scale, clipped samples marked.

Requires matplotlib (pip install phonometry[plot]).

Parameters

NameDescription
axExisting axes, or None to create a figure.
languageLabel language, "en" (default) or "es".
kwargsForwarded to the record curve.

Returns: The axes.

Constant (float).

RESPONSE_END_RATIO = 0.01

Constant (float).

RESPONSE_MIDPOINT_RATIO = 0.1
ResponseDecayCheck(
record: np.ndarray,
fs: float,
*,
segment_s: float,
exponential_window: bool,
start_s: float = 0.0,
)

Did the response decay far enough within the record? (8.3, 8.5.2)

Without a window the response should decay to about 1 % of its initial magnitude at the end of the record, or truncation leaks (8.3). 8.3 calls that figure a compromise: a longer record, reaching further down, improves the frequency resolution but can lower the signal-to-noise ratio, so a much faster decay is no better. With an exponential window, as a general guideline, the natural decay should reach 25 % or less, or the corrections of Annex A become very sensitive to errors in the damping estimates (8.5.2). 8.5.2 also offers a convenient check: the peak response at the midpoint of the record is about 10 % of the highest one, which for a steady exponential decay is the same statement as 1 % at the end.

Levels are peaks of the magnitude over segments of the record, relative to the highest peak from start_s on; a segment needs at least one period of the lowest mode for its peak to be a peak. The midpoint level is the peak over the segment that starts at the midpoint. The level at the end is read at the last sample: the peak over the last segment sits at its start, so it is carried to the last sample at the rate the peaks decay from the midpoint segment to the last one, the steady decay the midpoint check itself assumes. A response that has stopped decaying, on a noise floor, is read at the peak of its last segment, never higher.

The verdict compares the level at the end, rounded to a whole per cent (the precision both figures are printed in), with the figure: “about 1 %” accepts any level below 1,5 %, which rounds to 1 %, and “25 % or less” any level below 25,5 %.

A driving-point acceleration follows the force itself during the impact (the mass line of the structure), so its highest peak can be the impact rather than the decay. start_s just after the force pulse, or the force record passed to check_response_decay, judges the free decay alone.

Attributes

NameDescription
recordThe response record judged, unwindowed, in its own unit, as a bare array (a Signal passed in is read without its calibration factor, and its samples are what is kept).
fsIts sample rate, in Hz.
segment_sLength of the segments the end and midpoint levels are read over, in seconds.
exponential_windowWhether the record is to be processed with an exponential window (25 % limit) or without (1 %).
start_sTime from which the highest peak is sought, in seconds.

property

Level at the last sample of the record, relative to the highest peak.

The peak over the last segment, carried over the rest of the segment at the rate the segment peaks decay from the midpoint to the last segment (never raised).

property

Peak over the last segment, relative to the highest peak.

For a response still decaying this is its level at the start of the segment, above the level at the end of the record.

property

The figure the clause prints: 0,01 without a window, 0,25 with one.

property

Peak over the segment starting at the midpoint, relative to the highest peak.

property

Whether the level at the end, to a whole per cent, is at most the figure.

property

The highest peak of the response from start_s on, in its unit.

property

When the highest peak from start_s on occurs, in seconds.

ResponseDecayCheck.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot the response with the levels it reaches at the midpoint and the end.

Requires matplotlib (pip install phonometry[plot]).

Parameters

NameDescription
axExisting axes, or None to create a figure.
languageLabel language, "en" (default) or "es".
kwargsForwarded to the response curve.

Returns: The axes.

single_mode_fit(
frequencies: ArrayLike,
frf: ArrayLike,
*,
band_hz: tuple[float, float],
kind: str = 'mobility',
exponential_decay_rate_per_s: float = 0.0,
) -> SingleModeFitResult

Fit one mode to an FRF over a band around its resonance.

Annex A needs the apparent damping of each mode and leaves the estimator open (“suitable parameter estimation capability is now widely available through a variety of curve-fitting methods”). This is a rational-fraction fit of one mode: the FRF over the band is fitted by in linear least squares, reweighted by the previous denominator until it settles (Sanathanan and Koerner), and the result is written as a pole, its residue and a direct term (SingleModeFitResult). The model is exact for a single-degree-of-freedom structure; on a real one, choose a band that holds one resonance and little else.

Annex A asks that the apparent damping of each mode “can be accurately determined”, and a band without a resonance determines none: a constant, a mass line or a spring line fits the numerator alone and leaves the pole to rounding. The fit is refused when its system does not determine the five coefficients to half the digits of a double, or when the pole it finds has no decay rate or no damped natural frequency above that same fraction of the band’s centre frequency.

Fit the FRF in the kind the response channel measured, because that is the record the exponential window multiplied: ImpactMobilityResult.fit_mode does so.

Parameters

NameDescription
frequenciesFrequencies of the FRF, in hertz (> 0).
frfComplex FRF of kind kind, same length.
band_hz(f_low, f_high), in hertz, holding at least three frequencies.
kind"accelerance", "mobility" (default) or "receptance".
exponential_decay_rate_per_sDecay rate of the exponential window the data carry, in 1/s (Default: 0).

Returns: A SingleModeFitResult.

Raises

ExceptionWhen
ValueErrorfor mismatched or non-finite data, a band that is not a pair of positive numbers or holds fewer than three frequencies, an unknown kind, or a band whose data hold no lightly damped mode.
SingleModeFitResult(
frequencies: np.ndarray,
frf: np.ndarray,
pole_rad_s: complex,
residue: complex,
direct_term: complex,
*,
kind: str = 'mobility',
exponential_decay_rate_per_s: float = 0.0,
)

One mode fitted over a band: pole, residue and a direct term.

The model is , with the pole of the data fitted, its residue and a real constant that stands in for the modes outside the band. For a single-degree-of-freedom structure the model is exact in every FRF kind: for dynamic compliance and mobility and for accelerance.

Attributes

NameDescription
frequenciesThe frequencies fitted, in hertz.
frfThe FRF fitted, complex, of kind kind.
pole_rad_sThe pole of the data, upper half plane, in rad/s.
residueIts residue , in the FRF’s unit times rad/s.
direct_termThe constant , in the FRF’s unit.
kindThe FRF kind fitted: "accelerance", "mobility" or "receptance".
exponential_decay_rate_per_sDecay rate of the exponential window the data carry, in 1/s (0 for none).

SingleModeFitResult.apparent_damping_ratio

Section titled “SingleModeFitResult.apparent_damping_ratio”

property

Damping ratio of the fitted pole, .

With an exponential window this is the apparent damping of Annex A, the window’s included.

SingleModeFitResult.corrected_frf(frequencies: ArrayLike) -> np.ndarray

The mode with the window taken out, of kind kind (Annex A).

The pole is moved back by the decay rate, , and the residue and direct term are kept: the window multiplies the impulse response by , which changes the pole and nothing else (Formula (A.2)).

Parameters

NameDescription
frequenciesFrequencies, in hertz.

Returns: The complex FRF.

SingleModeFitResult.corrected_mobility(frequencies: ArrayLike) -> np.ndarray

The corrected mode as mobility, in m/(N·s).

Parameters

NameDescription
frequenciesFrequencies, in hertz (> 0).

Returns: The complex mobility.

property

The Annex A correction of this mode for the window its data carry.

SingleModeFitResult.damped_natural_frequency_hz

Section titled “SingleModeFitResult.damped_natural_frequency_hz”

property

Damped natural frequency , in hertz.

SingleModeFitResult.fitted_frf(frequencies: ArrayLike) -> np.ndarray

The fitted model at any frequencies, window included, of kind kind.

Parameters

NameDescription
frequenciesFrequencies, in hertz.

Returns: The complex FRF.

SingleModeFitResult.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot the data fitted, the fitted mode and the mode with the window taken out.

Requires matplotlib (pip install phonometry[plot]).

Parameters

NameDescription
axExisting axes, or None to create a figure.
languageLabel language, "en" (default) or "es".
kwargsForwarded to the fitted curve.

Returns: The axes.

verify_channel_match(
frequencies: ArrayLike,
response: ArrayLike,
*,
frequency_range_hz: tuple[float, float],
) -> ChannelMatchVerification

Is the analyser’s channel-to-channel match inside 8.1’s tolerances?

Feed the same broad-band signal to both channels, estimate the frequency response between them (for instance with phonometry.electroacoustics.frequency_response.transfer_function), and pass it here. The verification passes when its magnitude is unity within +/- 5 % and its phase zero within +/- 5 degrees at every frequency of the range of interest.

Parameters

NameDescription
frequenciesFrequencies of the response, in hertz.
responseComplex frequency response between the channels.
frequency_range_hz(f_low, f_high), in hertz.

Returns: A ChannelMatchVerification over the range.

Raises

ExceptionWhen
ValueErrorfor mismatched or non-finite data, or a range that holds none of the frequencies.

Constant (float).

WINDOWED_RESPONSE_END_RATIO = 0.25