Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Chapter 4: Frequency Domain Analysis

Authors
Affiliations
University of Alberta
University of Alberta

1Overview

Structural responses often consist of multiple vibration modes excited simultaneously. In the time domain these modes superpose into a complex signal, but in the frequency domain they appear as distinct peaks. These frequency signatures allow engineers to identify modal properties. Frequency domain techniques excel at separating signal from noise, isolating individual modes even in complex multi-degree-of-freedom systems, and providing reliable identification under ambient excitation conditions.

This chapter introduces the fundamental tools for frequency-domain analysis in SHM: the Fourier transform and its digital implementation (FFT), spectral analysis, the short-time Fourier transform (STFT) for time-frequency analysis, and wavelet transforms. We then discuss how these tools enable non-parametric modal identification of bridges.


2Fourier Transform

2.1Inner Product and Function Similarity

Before introducing the Fourier transform, we need to measure similarity between functions. The inner product quantifies how much two signals “align” with each other.

For two functions f(t)f(t) and g(t)g(t), the inner product is defined as:

⟨f,g⟩=1T∫0Tf(t)g∗(t)dt\langle f, g \rangle = \frac{1}{T}\int_{0}^{T} f(t) g^*(t) dt

where g∗(t)g^*(t) denotes the complex conjugate of g(t)g(t). This operation measures the total overlap between the two functions across all time.

2.2Fourier Transform as Frequency Similarity

The Fourier transform uses this similarity concept to decompose any signal into its frequency components by comparing the signal with sine and cosine waves at different frequencies.

For a pure cosine wave at frequency ff: cos⁡(2πft)\cos(2\pi ft) For a pure sine wave at frequency ff: sin⁡(2πft)\sin(2\pi ft)

Using Euler’s identity, we can combine these into a complex exponential:

ej2πft=cos⁡(2πft)+jsin⁡(2πft)e^{j2\pi ft} = \cos(2\pi ft) + j\sin(2\pi ft)

Here, j=−1j = \sqrt{-1} is the imaginary unit. The Fourier transform coefficient at frequency ff is simply the inner product between our signal, with duration TT, and this complex exponential:

X(f)=12π∫0Tx(t)e−j2πftdt=⟨x(t),ej2πft⟩X(f) = \frac{1}{2\pi} \int_{0}^{T} x(t) e^{-j2\pi ft} dt = \langle x(t), e^{j2\pi ft} \rangle

The magnitude ∣X(f)∣|X(f)| tells us how similar our signal is to a sinusoid at frequency ff. Large values mean that frequency is a strong component of the signal, while small values indicate little content at that frequency.

Fourier Transform as similarity measurement [Source].

Figure 1:Fourier Transform as similarity measurement [Source].

The inverse Fourier transform rebuilds a signal by summing all of its frequency components:

x(t)=∫−fmax+fmaxX(f)ej2πftdfx(t) = \int_{-f_{max}}^{+f_{max}} X(f) e^{j2\pi ft} df

In words, x(t)x(t) is obtained by adding sinusoids at every frequency ff, each weighted by the spectrum X(f)X(f) and aligned by its phase. These sinusoids form a complete basis, so with the right amplitudes and phases the original signal is recovered exactly.

Fourier series reconstruction. Adding harmonics (with amplitude and phase) progressively rebuilds a square wave [Source].

Figure 2:Fourier series reconstruction. Adding harmonics (with amplitude and phase) progressively rebuilds a square wave [Source].

The Fourier transform produces complex numbers at each frequency, containing two essential pieces of information:

For frequency identification in structural health monitoring, amplitude is often sufficient to locate natural frequencies. However, for complete signal reconstruction or understanding system behavior, both amplitude and phase are required.

The magnitude spectrum ∣X(f)∣|X(f)| reveals the frequency content, while the phase spectrum ∠X(f)\angle X(f) contains timing information. In Bode plots commonly used in engineering, magnitude is plotted in decibels (dB):

∣X(f)∣dB=20log⁡10(∣X(f)∣)|X(f)|_{dB} = 20 \log_{10}(|X(f)|)

2.3Fundamental Properties of the Fourier Transform

The Fourier transform operator F{⋅}\mathcal{F}\{\cdot\} possesses several important mathematical properties that simplify analysis of complex signals:

Table 1:Fundamental Properties of the Fourier Transform.

PropertyMathematical EquationDescription
1. LinearityF{ax(t)+by(t)}=aX(f)+bY(f)\mathcal{F}\{ax(t) + by(t)\} = aX(f) + bY(f)Linear combinations transform as linear combinations
2. Time ShiftingF{x(t−a)}=e−j2πfaX(f)\mathcal{F}\{x(t - a)\} = e^{-j2\pi fa} X(f)Time delay creates phase shift in frequency
3. Frequency TranslationF{ej2πf0tx(t)}=X(f−f0)\mathcal{F}\{e^{j2\pi f_0 t}x(t)\} = X(f - f_0)Modulation shifts spectrum in frequency
4. Time ScalingF{x(at)}=1∣a∣X(fa)\mathcal{F}\{x(at)\} = \frac{1}{|a|}X\left(\frac{f}{a}\right)Time compression expands frequency spectrum
5. DifferentiationF{x˙(t)}=j2πfX(f)\mathcal{F}\{\dot{x}(t)\} = j2\pi f X(f)Derivatives become multiplication by j2πfj2\pi f
6. IntegrationF{∫−∞tx(τ)dτ}=X(f)j2πf\mathcal{F}\left\{\int_{-\infty}^{t} x(\tau) d\tau\right\} = \frac{X(f)}{j2\pi f}Integration becomes division by j2πfj2\pi f
7. ConvolutionF{g(t)∗x(t)}=G(f)X(f)\mathcal{F}\{g(t) * x(t)\} = G(f)X(f)Convolution in time is multiplication in frequency

2.4Common Fourier Transform Pairs

The following table presents fundamental transform pairs that frequently appear in signal processing applications. Each pair shows how a specific time-domain function maps to its frequency-domain counterpart.

Table 2:Common Fourier Transform Pairs.

Time Domain x(t)x(t)Time PlotFrequency Domain X(f)X(f)Frequency Plot
δ(t)\delta(t)
<Figure size 150x100 with 1 Axes>
12π\frac{1}{2\pi}
<Figure size 150x100 with 1 Axes>
1
<Figure size 150x100 with 1 Axes>
12πδ(f)\frac{1}{2\pi}\delta(f)
<Figure size 150x100 with 1 Axes>
tt
<Figure size 150x100 with 1 Axes>
j(2πf)2\frac{j}{(2\pi f)^2}
<Figure size 150x100 with 1 Axes>
cos⁡(2πf0t)\cos(2\pi f_0 t)
<Figure size 150x100 with 1 Axes>
14π[δ(f−f0)+δ(f+f0)]\frac{1}{4\pi}[\delta(f-f_0) + \delta(f+f_0)]
<Figure size 150x100 with 1 Axes>
sin⁡(2πf0t)\sin(2\pi f_0 t)
<Figure size 150x100 with 1 Axes>
j4π[δ(f−f0)−δ(f+f0)]\frac{j}{4\pi}[\delta(f-f_0) - \delta(f+f_0)]
<Figure size 150x100 with 1 Axes>
e−atu(t)e^{-at}u(t)
<Figure size 150x100 with 1 Axes>
12π(a+j2πf)\frac{1}{2\pi(a + j2\pi f)}
<Figure size 150x100 with 1 Axes>
te−atu(t)te^{-at}u(t)
<Figure size 150x100 with 1 Axes>
12π(a+j2πf)2\frac{1}{2\pi(a + j2\pi f)^2}
<Figure size 150x100 with 1 Axes>
e−atcos⁡(ω0t) u(t)e^{-at}\cos(\omega_0 t)\,u(t)
<Figure size 150x100 with 1 Axes>
14π[1a+j2π(f−f0)+1a+j2π(f+f0)]\frac{1}{4\pi}\left[\frac{1}{a + j 2\pi(f - f_0)} + \frac{1}{a + j 2\pi(f + f_0)}\right]
<Figure size 150x100 with 1 Axes>
e−atsin⁡(ω0t) u(t)e^{-at}\sin(\omega_0 t)\,u(t)
<Figure size 150x100 with 1 Axes>
14πj[1a+j2π(f−f0)−1a+j2π(f+f0)]\frac{1}{4\pi j}\left[\frac{1}{a + j 2\pi(f - f_0)} - \frac{1}{a + j 2\pi(f + f_0)}\right]
<Figure size 150x100 with 1 Axes>

where u(t)u(t) is the unit step function and a>0a > 0.

2.5Storage Efficiency and Signal Decomposition

Consider a signal composed of three sinusoidal components:

x(t)=2cos⁡(2π⋅5t)+1.5cos⁡(2π⋅12t)+0.8cos⁡(2π⋅25t)x(t) = 2\cos(2\pi \cdot 5t) + 1.5\cos(2\pi \cdot 12t) + 0.8\cos(2\pi \cdot 25t)

In the time domain, storing this signal for 10 seconds at 200 Hz requires 2000 data points. After Fourier transformation, only three frequency values (5, 12, 25 Hz) and their corresponding amplitudes (2, 1.5, 0.8), plus phase information, are needed: 9 numbers instead of 2000.

This efficiency makes frequency-domain analysis useful for both data compression and system understanding.

Source
<Figure size 1080x690 with 6 Axes>

Figure 3:Fourier decomposition demonstration showing how a multi-component signal can be reconstructed from its frequency components, illustrating the efficiency and interpretability of frequency domain representation.

2.6Discrete Fourier Transform (DFT)

Real measurements are discrete samples taken at regular intervals. The sampling process can be modeled using pulse amplitude modulation, where the continuous signal is multiplied by a train of impulses:

xs(t)=∑n=0N−1x[n]δ(t−nTs)x_s(t) = \sum_{n=0}^{N-1} x[n] \delta(t - nT_s)

where TsT_s is the sampling period and x[n]x[n] are the sample values.

Taking the Fourier transform of this sampled signal:

Xs(f)=12π∫0Txs(t)e−j2πftdt=1N×Ts∑n=0N−1x[n]e−j2πfnTs×TsX_s(f) = \frac{1}{2\pi} \int_{0}^{T} x_s(t) e^{-j2\pi ft} dt = \frac{1}{N \times T_s} \sum_{n=0}^{N-1} x[n] e^{-j2\pi fnT_s} \times T_s

For a finite record of N=T/TsN = T/T_s samples, the DFT evaluates the spectrum at NN equally spaced frequency points:

X[k]=1N∑n=0N−1x[n]e−j2πkn/N,k=0,1,…,N−1X[k] = \frac{1}{N} \sum_{n=0}^{N-1} x[n] e^{-j2\pi kn/N}, \quad k = 0, 1, \ldots, N-1

The corresponding frequencies are:

fk=kNTs=kFsNf_k = \frac{k}{NT_s} = \frac{kF_s}{N}

The inverse DFT reconstructs the time samples:

x[n]=∑k=0N−1X[k]ej2πkn/N,n=0,1,…,N−1x[n] = \sum_{k=0}^{N-1} X[k] e^{j2\pi kn/N}, \quad n = 0, 1, \ldots, N-1

2.7Simple FFT Implementation in Python

The practical implementation in Python is straightforward using NumPy’s Fast Fourier Transform (FFT) algorithms.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np

# Forward FFT: Time domain → Frequency domain
X = np.fft.fft(x)

# Create frequency axis
freq = np.fft.fftfreq(len(x), 1/fs)

# Inverse FFT: Frequency domain → Time domain
x_reconstructed = np.fft.ifft(X)

# For real signals, use rfft for efficiency (positive frequencies only)
X_real = np.fft.rfft(x)
freq_real = np.fft.rfftfreq(len(x), 1/fs)

# Inverse of real FFT
x_from_rfft = np.fft.irfft(X_real)

2.7.1Symmetry and Nyquist Frequency

For real-valued signals, the DFT exhibits conjugate symmetry: X[k]=X∗[N−k]X[k] = X^*[N-k]. The spectrum from k=N/2+1k = N/2+1 to N−1N-1 is therefore redundant, containing the same information as frequencies from k=1k = 1 to N/2−1N/2-1.

The maximum meaningful frequency is the Nyquist frequency:

fNyquist=Fs2f_{Nyquist} = \frac{F_s}{2}

In practice, we only plot the spectrum from DC (0 Hz) to the Nyquist frequency.

<Figure size 1050x750 with 4 Axes>

Figure 4:DFT symmetry for a real signal. The two-sided magnitude spectrum (dB) is even about zero frequency, while the two-sided phase is odd. The one-sided spectrum (dB) folds the negative frequencies onto the positive axis, where the signal peaks are identified.

2.8Examples

2.8.1Bridge under Impact Testing Example

Impact testing excites all structural frequencies simultaneously, making it well suited for modal identification using FFT analysis. A hammer strike approximates an impulse input, which contains equal energy at all frequencies up to the Nyquist limit.

The experimental setup is a 30-meter simply supported bridge instrumented with four accelerometers. The impact hammer is applied at 40% of the span length (12 meters from the left support) to excite all target modes. The sensor configuration and experimental geometry are shown in the setup diagram below.

Bridge impact-testing example setup: a 30 m simply supported beam with four accelerometers S_1–S_4 at 7.5 / 15.0 / 22.5 / 27.5 m, an impact location at 12.0 m, and target modal frequencies f_1 \approx 3.2, f_2 \approx 8.7, f_3 \approx 16.4 Hz.

Figure 5:Bridge impact-testing example setup: a 30 m simply supported beam with four accelerometers S1S_1–S4S_4 at 7.5 / 15.0 / 22.5 / 27.5 m, an impact location at 12.0 m, and target modal frequencies f1≈3.2f_1 \approx 3.2, f2≈8.7f_2 \approx 8.7, f3≈16.4f_3 \approx 16.4 Hz.

For a simply supported beam, the theoretical natural frequencies follow fn=n2π22L2EIρAf_n = \frac{n^2 \pi^2}{2L^2} \sqrt{\frac{EI}{\rho A}}, giving the first three bending modes at approximately 3.2, 8.7, and 16.4 Hz for typical bridge parameters. Sensors are placed at 25%, 50%, 75%, and 92% of the span to maintain good observability of all target modes while avoiding nodal points where certain modes exhibit minimal response.

Source
<Figure size 1050x810 with 6 Axes>

Figure 6:Bridge impact testing analysis showing time domain responses and frequency domain identification results for four-sensor configuration.

2.8.2Bridge under Ambient Loading

Ambient loading (traffic, wind) excites structures differently from impact testing. Ambient forces typically concentrate energy near certain frequencies, making some modal frequencies difficult to detect using direct FFT analysis.

Source
<Figure size 1050x810 with 6 Axes>

Figure 7:Bridge ambient loading demonstration showing limitations of direct FFT analysis.

This illustrates why direct FFT analysis works well for impact testing but struggles with ambient loading. The next section on spectral analysis addresses these limitations by using statistical methods to extract modal information from noisy, non-uniformly excited responses.

3Frequency Response Functions

The frequency response function completely characterizes linear dynamic systems in the frequency domain. Starting from the time-domain convolution relationship:

x(t)=g(t)∗u(t)=∫0tg(t−τ)u(τ)dτx(t) = g(t) * u(t) = \int_{0}^{t} g(t-\tau) u(\tau) d\tau

Taking the Fourier transform of both sides and applying the convolution theorem:

X(f)=G(f)⋅U(f)X(f) = G(f) \cdot U(f)

where G(f)G(f) is the frequency response function (FRF), representing the Fourier transform of the impulse response function g(t)g(t):

G(f)=F{g(t)}=∫0∞g(t)e−j2πftdtG(f) = \mathcal{F}\{g(t)\} = \int_{0}^{\infty} g(t) e^{-j2\pi ft} dt

This converts the time-domain convolution into multiplication in the frequency domain, simplifying the analysis of system response to arbitrary inputs.

3.1Single Degree of Freedom Systems

For a single degree of freedom system with mass mm, damping coefficient cc, and stiffness kk, the frequency response function has the analytical form:

G(f)=1k−m(2πf)2+jc(2πf)=1/k1−(f/fn)2+j2ζ(f/fn)G(f) = \frac{1}{k - m(2\pi f)^2 + jc(2\pi f)} = \frac{1/k}{1 - (f/f_n)^2 + j2\zeta(f/f_n)}

where fn=12πk/mf_n = \frac{1}{2\pi}\sqrt{k/m} is the natural frequency and ζ=c2km\zeta = \frac{c}{2\sqrt{km}} is the damping ratio.

The magnitude and phase of the FRF together describe the system’s dynamic characteristics:

∣G(f)∣=1/k[1−(f/fn)2]2+[2ζ(f/fn)]2∠G(f)=−arctan⁡(2ζ(f/fn)1−(f/fn)2)\begin{align} |G(f)| &= \frac{1/k}{\sqrt{[1-(f/f_n)^2]^2 + [2\zeta(f/f_n)]^2}} \\ \angle G(f) &= -\arctan\left(\frac{2\zeta(f/f_n)}{1-(f/f_n)^2}\right) \end{align}

3.2System Response Simulation

Once G(f)G(f) is known, the response to any arbitrary input u(t)u(t) is computed in three steps:

  1. Transform input to frequency domain: U(f)=F{u(t)}U(f) = \mathcal{F}\{u(t)\}

  2. Multiply by FRF: X(f)=G(f)⋅U(f)X(f) = G(f) \cdot U(f)

  3. Transform back to time domain: x(t)=F−1{X(f)}x(t) = \mathcal{F}^{-1}\{X(f)\}

This replaces the convolution integral with efficient FFT operations.

Source
<Figure size 1050x750 with 4 Axes>

Figure 8:Simulating an LTI system’s response to an arbitrary input via FRF.

3.3Experimental FRF Extraction: Impact Hammer Testing

The FRF can be extracted experimentally using impact hammer testing. When the input is an ideal impulse u(t)=δ(t)u(t) = \delta(t), the Fourier transform becomes:

U(f)=F{δ(t)}=1U(f) = \mathcal{F}\{\delta(t)\} = 1

The system response to an impulse therefore directly yields the FRF:

X(f)=G(f)⋅1=G(f)X(f) = G(f) \cdot 1 = G(f)

In practice, the impact hammer approximates the impulse function over a finite frequency range, allowing direct measurement of G(f)G(f) through:

G(f)=X(f)U(f)G(f) = \frac{X(f)}{U(f)}

where X(f)X(f) and U(f)U(f) are the Fourier transforms of the measured response and force signals, respectively.

<Figure size 1050x750 with 3 Axes>

Figure 9:SDOF frequency response function demonstration showing analytical FRF, system response simulation using frequency domain multiplication, and comparison with time domain convolution results.

3.4Multiple Degree of Freedom Systems

For MDOF systems with NdN_d degrees of freedom, the frequency response relationship becomes:

[X1(f)X2(f)⋮XNd(f)]=[G11(f)G12(f)⋯G1Nd(f)G21(f)G22(f)⋯G2Nd(f)⋮⋮⋱⋮GNd1(f)GNd2(f)⋯GNdNd(f)][U1(f)U2(f)⋮UNd(f)]\begin{bmatrix} X_1(f) \\ X_2(f) \\ \vdots \\ X_{N_d}(f) \end{bmatrix} = \begin{bmatrix} G_{11}(f) & G_{12}(f) & \cdots & G_{1N_d}(f) \\ G_{21}(f) & G_{22}(f) & \cdots & G_{2N_d}(f) \\ \vdots & \vdots & \ddots & \vdots \\ G_{N_d1}(f) & G_{N_d2}(f) & \cdots & G_{N_d N_d}(f) \end{bmatrix} \begin{bmatrix} U_1(f) \\ U_2(f) \\ \vdots \\ U_{N_d}(f) \end{bmatrix}

or in compact matrix form:

X(f)=G(f)U(f)\mathbf{X}(f) = \mathbf{G}(f) \mathbf{U}(f)

Each element Gij(f)G_{ij}(f) is the FRF relating the response at DOF ii to a unit harmonic input at DOF jj, with all other inputs equal to zero.

For a system with NmN_m modes, each FRF element has the modal expansion:

Gij(f)=∑r=1NmϕirϕjrMr1ωnr2−(2πf)2+j2ζrωnr(2πf)G_{ij}(f) = \sum_{r=1}^{N_m} \frac{\phi_{ir} \phi_{jr}}{M_r} \frac{1}{\omega_{nr}^2 - (2\pi f)^2 + j2\zeta_r\omega_{nr}(2\pi f)}

where:

Each FRF element thus contains contributions from all modes, weighted by the modal participation at the input and output locations.

3.5System Response for Multiple Inputs

For systems with multiple simultaneous inputs, the response at each DOF is the superposition of responses to all inputs:

Xi(f)=∑j=1NdGij(f)Uj(f)X_i(f) = \sum_{j=1}^{N_d} G_{ij}(f) U_j(f)

This allows prediction of complex loading scenarios by combining individual FRF measurements with arbitrary input combinations.

Source
<Figure size 1080x690 with 7 Axes>
Loading...
Loading...

Figure 10:Frequency response function (FRF) behavior of a 5-DOF bridge model with three modes. (a) Mode shapes. (b) The driving-point FRF G22 as a superposition of modal contributions, with peaks at the natural frequencies. (c) Normalized magnitude of the full FRF matrix at the first resonance, showing the symmetric (reciprocal) coupling between DOFs. (d) Reciprocity: the transfer FRFs G13 and G31 coincide. (e) Three simultaneous input forces and (f) the resulting response at every DOF, obtained by combining the FRF matrix with the inputs.

3.6Limitations and Practical Considerations

Measurement noise significantly affects FRF quality, particularly in anti-resonance regions between modes where response amplitudes are naturally low. This makes accurate damping estimation difficult and can mask higher-order modes with low modal participation at the measurement locations.

These limitations motivate the spectral analysis approaches discussed in the following section, which extract modal information from noisy measurements under ambient excitation without requiring controlled input forces.

4Frequency Domain Filtering and Applications

Frequency-domain filtering decomposes a signal into its frequency components via the Fourier transform, selectively attenuates or removes specific frequency ranges, and then reconstructs the signal via the inverse transform. This approach provides precise control over which frequencies are preserved or removed.

The process has three steps: the FFT converts the time-domain signal to its frequency representation; a filter function multiplies the frequency components, scaling or eliminating unwanted ones; and the inverse FFT reconstructs the filtered signal. Many noise sources and signal artifacts occupy distinct frequency ranges, making this approach effective at targeting them specifically.

The mathematical foundation rests on the convolution theorem, which states that convolution in the time domain corresponds to multiplication in the frequency domain:

F{f∗g}=F{f}⋅F{g}\mathcal{F}\{f * g\} = \mathcal{F}\{f\} \cdot \mathcal{F}\{g\}

where F\mathcal{F} denotes the Fourier transform operator, ∗* represents convolution, and ⋅\cdot indicates pointwise multiplication. This relationship makes frequency-domain filtering computationally efficient, particularly for long signals where direct convolution is expensive.

4.1Filter Types and Characteristics

4.1.1Lowpass Filters

Lowpass filters preserve low-frequency components while attenuating high-frequency content above a specified cutoff frequency. They are used for removing high-frequency noise, preventing aliasing during sampling, and smoothing signals. The ideal lowpass filter has a rectangular frequency response, but practical implementations use smoother transitions to avoid time-domain artifacts. The cutoff frequency fcf_c marks where the filter begins significant attenuation, typically at the half-power point where amplitude drops to 1/21/\sqrt{2} of its maximum value.

<Figure size 1050x390 with 1 Axes>

Figure 11:Lowpass filter frequency response demonstrating the preservation of low frequencies while attenuating high frequencies above the cutoff.

4.1.2Highpass Filters

Highpass filters preserve high-frequency components while attenuating low-frequency content below the cutoff frequency. They remove DC offsets, eliminate low-frequency drift, and reduce baseline wandering. Highpass filtering is important when sensor data exhibits slow variations unrelated to the phenomenon of interest. The ideal highpass filter response is:

H(f)={0if ∣f∣<fc1if ∣f∣≥fcH(f) = \begin{cases} 0 & \text{if } |f| < f_c \\ 1 & \text{if } |f| \geq f_c \end{cases}

where fcf_c represents the cutoff frequency.

<Figure size 1050x390 with 1 Axes>

Figure 12:Highpass filter frequency response showing attenuation of low frequencies below the cutoff while preserving high-frequency content.

4.1.3Bandpass Filters

Bandpass filters preserve frequencies within a specific range while attenuating components outside that band. They are useful for isolating signals that occupy known frequency ranges, such as extracting specific vibrational modes from structural response data or focusing on particular frequency bands in seismic analysis. The bandpass response is the product of highpass and lowpass responses:

H(f)=Hhp(f)⋅Hlp(f)H(f) = H_{hp}(f) \cdot H_{lp}(f)

where Hhp(f)H_{hp}(f) and Hlp(f)H_{lp}(f) represent the highpass and lowpass components, respectively.

<Figure size 1050x390 with 1 Axes>

Figure 13:Bandpass filter frequency response demonstrating selective preservation of frequencies within a specified band while rejecting components outside this range.

4.2Practical Applications

4.2.1Aliasing Prevention

Aliasing occurs when high-frequency components exceed the Nyquist frequency during sampling, causing them to appear as false low-frequency artifacts in the digitized signal. The Nyquist-Shannon sampling theorem requires the sampling frequency to exceed twice the highest frequency component in the signal. Lowpass anti-aliasing filters applied before sampling ensure that no frequency components above the Nyquist frequency reach the analog-to-digital converter.

The relationship between sampling frequency and maximum signal frequency is governed by:

fs>2fmaxf_s > 2f_{max}

where fsf_s is the sampling frequency and fmaxf_{max} is the highest frequency component in the signal.

<Figure size 1050x750 with 3 Axes>

Figure 14:Demonstration of aliasing effects showing how high-frequency components appear as false low frequencies when the sampling rate is insufficient, and how anti-aliasing filtering prevents this distortion.

4.2.2DC Offset and Baseline Correction

DC offsets and low-frequency baseline drift commonly affect sensor measurements, particularly in accelerometer and displacement data. These artifacts can severely distort subsequent processing steps, especially integration operations that accumulate errors over time. Highpass filtering removes DC components and low-frequency drift while preserving the dynamic content of interest. The highpass cutoff frequency must be chosen carefully to avoid removing genuine low-frequency components.

When integrating acceleration data to obtain velocity and displacement, any DC bias is amplified through integration, producing unrealistic linear trends in velocity and quadratic trends in displacement. This is particularly problematic in structural monitoring where true displacement measurements are needed to assess structural performance.

<Figure size 1050x750 with 3 Axes>

Figure 15:Impact of DC offset on double integration of acceleration data, showing how small bias errors lead to unrealistic displacement trends and how highpass filtering corrects this issue.

4.2.3Highpass Filter Implementation

Implementing highpass filters in the frequency domain transforms the signal, applies the filter function, and applies the inverse transform while avoiding artifacts. Proper implementation requires attention to filter design parameters, frequency resolution, and edge effects.

Compared to time-domain approaches, frequency-domain implementation offers computational efficiency for long signals and precise frequency control. Transition regions must be designed carefully to minimize ringing artifacts in the time domain.

<Figure size 1050x810 with 4 Axes>

Figure 16:Practical implementation of highpass filtering in Python showing the complete process from signal analysis through filter design to filtered output, with frequency domain visualization.

The effectiveness of frequency-domain filtering depends on proper filter design and parameter selection. The cutoff frequency must be chosen based on the signal’s frequency content and the application requirements. Transition regions should provide adequate attenuation while minimizing time-domain ringing. In practice, iterative refinement of filter parameters is often necessary to preserve the essential characteristics of the signal.

5Spectral Analysis: From Correlation to Power Spectra

In the previous chapter, we established relationships between input and output correlations through the convolution of impulse response functions. For a system with impulse response g(t)g(t) excited by zero-mean white noise, the output autocorrelation function contains the same modal information as the impulse response:

rxx(τ)=σuu2[g(t)∗g(−t)]r_{xx}(\tau) = \sigma_{uu}^2 [g(t) * g(-t)]

The frequency domain offers a direct path to extracting modal parameters and understanding energy distribution across frequencies. Spectral analysis transforms correlation functions into power spectral densities, revealing frequency content that can be obscured in time-domain representations.

Stochastic processes have power spectral densities that describe how signal power is distributed across frequencies. This representation is effective for identifying structural modes because each mode contributes energy at specific frequencies that appear as peaks in the power spectrum.

5.1Power Spectral Density

5.1.1Continuous-Time Definition

For a continuous-time stochastic process x(t)x(t), the power spectral density Sxx(f)S_{xx}(f) is defined as the Fourier transform of the autocorrelation function rxx(τ)r_{xx}(\tau):

Sxx(f)=12π∫−∞∞rxx(τ)e−j2πfτdτS_{xx}(f) = \frac{1}{2\pi} \int_{-\infty}^{\infty} r_{xx}(\tau) e^{-j2\pi f \tau} d\tau

The inverse relationship recovers the autocorrelation function from the power spectral density:

rxx(τ)=∫−∞∞Sxx(f)ej2πfτdfr_{xx}(\tau) = \int_{-\infty}^{\infty} S_{xx}(f) e^{j2\pi f \tau} df

The inverse relationship of the PSD highlights its initial value: rxx(0)=∫−∞∞Sxx(f)dfr_{xx}(0) = \int_{-\infty}^{\infty} S_{xx}(f) df, which follows from Parseval’s theorem. For zero-mean signals, this integral equals the signal variance σx2=E[x2]\sigma_x^2 = E[x^2], so the area under the spectral density curve equals the variance.

5.1.2Discrete-Time Formulation

For discrete-time stochastic processes x[n]x[n] with sampling interval TsT_s, the power spectral density is obtained through the Discrete-Time Fourier Transform (DTFT) of the autocorrelation sequence rxx[h]r_{xx}[h]:

Sxx(f)=1hmax∑h=0hmaxrxx[h]e−j2πfhTsS_{xx}(f) = \frac{1}{h_{\text{max}}} \sum_{h=0}^{h_{\text{max}}} r_{xx}[h] e^{-j2\pi f h T_s}

The inverse DTFT relationship becomes:

rxx[h]=∫−fmax/2fmax/2Sxx(f)ej2πfhTsdfr_{xx}[h] = \int_{-f_{\text{max}}/2}^{f_{\text{max}}/2} S_{xx}(f) e^{j2\pi f h T_s} df

where fmax=Fs/2f_{\text{max}} = F_s/2. This discrete formulation connects directly to the digital signal processing techniques used in experimental modal analysis.

5.1.3Fundamental Properties

The PSD has several important properties. It is always real and non-negative, Sxx(f)∈RS_{xx}(f) \in \mathbb{R} and Sxx(f)≥0S_{xx}(f) \geq 0, ensuring it is physically meaningful as an energy density. For real-valued processes it also exhibits even symmetry, Sxx(f)=Sxx(−f)S_{xx}(f) = S_{xx}(-f), so complete information is contained in the positive frequency range [0,Fs/2][0, F_s/2] for discrete-time signals.

The total variance of the process equals the area under the power spectral density curve:

σx2=rxx[0]=∫−fmax/2fmax/2Sxx(f)df\sigma_{x}^2 = r_{xx}[0] = \int_{-f_{\text{max}}/2}^{f_{\text{max}}/2} S_{xx}(f) df

The power spectrum thus provides a frequency-by-frequency decomposition of the signal’s variance, where each infinitesimal band dfdf contributes Sxx(f)dfS_{xx}(f)df to the total variance.

<Figure size 1050x750 with 4 Axes>

Figure 18:Demonstration of power spectral density properties including symmetry, non-negativity, and variance relationship for a multi-component signal.

5.2Cross-Power Spectral Density

The cross-power spectral density characterizes the frequency-domain relationship between excitation and response in structural systems. For two stochastic processes u(t)u(t) and x(t)x(t), the cross-PSD is the Fourier transform of their cross-correlation function.

5.2.1Continuous-Time Cross-PSD

The cross-power spectral density Sux(f)S_{ux}(f) between input u(t)u(t) and output x(t)x(t) is:

Sux(f)=12π∫0∞rux(τ)e−j2πfτdτS_{ux}(f) = \frac{1}{2 \pi} \int_{0}^{\infty} r_{ux}(\tau) e^{-j2\pi f \tau} d\tau

with the inverse relationship:

rux(τ)=∫−∞∞Sux(f)ej2πfτdfr_{ux}(\tau) = \int_{-\infty}^{\infty} S_{ux}(f) e^{j2\pi f \tau} df

5.2.2Discrete-Time Cross-PSD

For discrete-time processes, the cross-power spectral density becomes:

Sux(f)=∑h=0hmaxrux[h]e−j2πfhTsS_{ux}(f) = \sum_{h=0}^{h_{\text{max}}} r_{ux}[h] e^{-j2\pi f h T_s}

Unlike auto-PSDs, cross-PSDs are generally complex-valued, Sux(f)∈CS_{ux}(f) \in \mathbb{C}, containing both magnitude and phase information about the relationship between the two processes. The phase provides information about time delays and system dynamics, while the magnitude indicates coupling strength at each frequency.

The cross-PSD satisfies the conjugate symmetry relationship Sux(−f)=Sxu∗(f)S_{ux}(-f) = S_{xu}^*(f), which ensures that the cross-correlation function remains real-valued.

5.3Magnitude-Squared Coherence

Coherence quantifies the degree of linear relationship between two signals as a function of frequency. It ranges from 0 to 1, where 1 indicates perfect linear correlation at a given frequency and 0 indicates no linear relationship. Magnitude-squared coherence is useful for identifying coupled oscillations in structural vibrations and other applications where two signals share common spectral components.

γxy(f)=∣Sxy(f)∣2Sxx(f)×Syy(f)\gamma_{xy}(f) = \frac{|S_{xy}(f)|^2}{S_{xx}(f) \times S_{yy}(f)}

where Sxy(f)S_{xy}(f) is the cross-PSD between signals x(t)x(t) and y(t)y(t), and Sxx(f)S_{xx}(f) and Syy(f)S_{yy}(f) are the respective auto-PSDs. This normalization keeps coherence values between zero and unity regardless of signal amplitude differences.

Source
<Figure size 1050x690 with 3 Axes>

Figure 19:Coherence analysis of two signals.

5.4Input-Output Relationships in Frequency Domain

A central result of spectral analysis is that the time-domain convolution relationship becomes multiplication in the frequency domain. From the previous chapter:

rxx[h]=gx[h]∗ruu[h]r_{xx}[h] = g_x[h] * r_{uu}[h]

Taking the Fourier transform of both sides and applying the convolution theorem, this time-domain convolution becomes frequency-domain multiplication:

Sxx(f)=∣G(f)∣2Suu(f)S_{xx}(f) = |G(f)|^2 S_{uu}(f)

where G(f)G(f) is the frequency response function (Fourier transform of the impulse response g[h]g[h]) and ∣G(f)∣2|G(f)|^2 is its squared magnitude.

The output power spectrum therefore equals the input power spectrum multiplied by ∣G(f)∣2|G(f)|^2. For white noise excitation where Suu(f)=σuu2S_{uu}(f) = \sigma_{uu}^2 (constant across all frequencies), the output power spectrum directly reflects the system’s frequency response:

Sxx(f)=σuu2∣G(f)∣2S_{xx}(f) = \sigma_{uu}^2 |G(f)|^2

5.5Practical Spectral Estimation

5.5.1DFT-Based Spectral Estimation

In practice, PSDs are estimated from finite-length data records using the DFT. For measured time series u[n]u[n] and x[n]x[n] of length NN samples, the basic spectral estimates are:

S^uu[f]=∣U[f]∣2S^xx[f]=∣X[f]∣2S^ux[f]=X[f]‾U[f]\begin{align} \hat{S}_{uu}[f] &= |U[f]|^2 \\ \hat{S}_{xx}[f] &= |X[f]|^2 \\ \hat{S}_{ux}[f] &= \overline{X[f]}U[f] \end{align}

where U[f]U[f] and X[f]X[f] are the DFT coefficients, and the overline denotes complex conjugation.

5.6Challenges in Spectral Estimation

Direct DFT-based spectral estimation has several limitations. Spectral leakage occurs when signal frequencies do not align with DFT bin centers, spreading energy across multiple bins. Windowing the data reduces leakage at the cost of frequency resolution. The variance of spectral estimates remains high regardless of record length, requiring averaging techniques to achieve reliable estimates.

5.7Welch’s Method for Improved Estimation

Welch’s method addresses these limitations by dividing the data record into overlapping segments, windowing each segment, computing the DFT, and averaging the resulting periodograms. This trades frequency resolution for reduced variance in the spectral estimates.

For a data record of length NN divided into LL overlapping segments of length MM, the Welch estimate becomes:

S^xxWelch[f]=1L∑l=1L1MU∣Xl[f]∣2\hat{S}_{xx}^{Welch}[f] = \frac{1}{L} \sum_{l=1}^{L} \frac{1}{M \mathcal{U}} |X_l[f]|^2

where Xl[f]X_l[f] is the DFT of the ll-th windowed segment, and U\mathcal{U} is the normalization factor accounting for the window power.

Source
<Figure size 1050x750 with 4 Axes>
Loading...

Figure 20:Comparison of spectral estimation methods showing the trade-offs between frequency resolution and variance reduction.

6Non-Parametric Frequency-Domain System Identification

Non-parametric frequency-domain system identification extracts modal parameters directly from measured data without assuming specific mathematical models. These methods work with frequency response functions or power spectral densities to identify natural frequencies, damping ratios, and mode shapes. Unlike parametric methods that fit predetermined model structures, non-parametric approaches let the data reveal the system’s characteristics.

Two approaches dominate practical applications. Peak picking estimates FRFs and extracts modal parameters from individual peaks. Frequency Domain Decomposition (FDD) uses singular value decomposition (SVD) of PSD matrices to separate closely spaced modes and extract mode shapes from output-only measurements.

Both methods are valuable for ambient vibration testing where input forces cannot be measured. They handle multiple modes simultaneously and can identify mode shapes from multiple measurement points, making them well suited for large structural systems such as bridges and buildings.

6.1Peak Picking Method

6.1.1Frequency Response Function Estimation

The FRF G(f)G(f) describes the relationship between input and output in the frequency domain. For measured input u(t)u(t) and output x(t)x(t), it is estimated as:

G(f)=Sxu(f)Suu(f)G(f) = \frac{S_{xu}(f)}{S_{uu}(f)}

where Sxu(f)S_{xu}(f) is the cross-power spectral density and Suu(f)S_{uu}(f) is the input auto-power spectral density.

For ambient excitation modeled as white noise, the input PSD is constant: Suu(f)=σu2S_{uu}(f) = \sigma_u^2. The FRF estimate simplifies to:

G(f)=Sxu(f)σu2G(f) = \frac{S_{xu}(f)}{\sigma_u^2}

The magnitude ∣G(f)∣|G(f)| peaks at natural frequencies, with peak heights related to modal amplitudes and peak widths related to damping ratios.

Near each natural frequency fnf_n, the FRF for a lightly damped SDOF system takes the form:

G(f)=Afn2−f2+j2ζfnfG(f) = \frac{A}{f_n^2 - f^2 + j2\zeta f_n f}

where AA is the modal amplitude, fnf_n is the natural frequency, and ζ\zeta is the damping ratio.

The natural frequency corresponds to the peak frequency in ∣G(f)∣|G(f)|. The damping ratio can be extracted using the half-power bandwidth method:

ζ=f2−f12fn\zeta = \frac{f_2 - f_1}{2f_n}

where f1f_1 and f2f_2 are the frequencies on either side of the peak where ∣G(f)∣2=∣G(fn)∣2/2|G(f)|^2 = |G(f_n)|^2/2.

Other methods fit theoretical models to the measured spectra near each peak. The Rational Fraction Polynomial method represents the FRF as a ratio of polynomials in frequency, enabling simultaneous identification of multiple modes.

Source
Loading...

6.3Frequency Domain Decomposition (FDD)

FDD addresses the limitations of peak picking for output-only measurements and closely spaced modes. It uses SVD of PSD matrices to extract modal information from multiple measurement points simultaneously.

For a structure with nn measurement points under ambient excitation, the output PSD matrix is:

Syy(f)=G(f)Suu(f)G∗(f)\mathbf{S}_{yy}(f) = \mathbf{G}(f) \mathbf{S}_{uu}(f) \mathbf{G}^*(f)

where G(f)\mathbf{G}(f) is the FRF matrix, Suu(f)\mathbf{S}_{uu}(f) is the input PSD matrix, and the asterisk denotes complex conjugate transpose.

For ambient excitation modeled as white noise with Suu(f)=σ2I\mathbf{S}_{uu}(f) = \sigma^2 \mathbf{I}, this simplifies to:

Syy(f)=σ2G(f)G∗(f)\mathbf{S}_{yy}(f) = \sigma^2 \mathbf{G}(f) \mathbf{G}^*(f)

6.3.1Singular Value Decomposition

Performing SVD of the output PSD matrix at each frequency yields:

Syy(f)=V(f)Σ(f)V∗(f)\mathbf{S}_{yy}(f) = \mathbf{V}(f) \boldsymbol{\Sigma}(f) \mathbf{V}^*(f)

where V(f)\mathbf{V}(f) contains the singular vectors and Σ(f)=diag[σ1(f),σ2(f),…,σn(f)]\boldsymbol{\Sigma}(f) = \text{diag}[\sigma_1(f), \sigma_2(f), \ldots, \sigma_n(f)] contains the singular values.

At natural frequencies where a single mode dominates, σ1(f)\sigma_1(f) peaks and the corresponding singular vector v1(f)\mathbf{v}_1(f) approximates the mode shape:

ϕr≈v1(fr)\boldsymbol{\phi}_r \approx \mathbf{v}_1(f_r)

where frf_r is the rr-th natural frequency.

The MAC between singular vectors ensures continuity:

MAC(vi(f),v1(fr))=∣vi∗(f)v1(fr)∣2(vi∗(f)vi(f))(v1∗(fr)v1(fr))>threshold\text{MAC}(\mathbf{v}_i(f), \mathbf{v}_1(f_r)) = \frac{|\mathbf{v}_i^*(f) \mathbf{v}_1(f_r)|^2}{(\mathbf{v}_i^*(f) \mathbf{v}_i(f))(\mathbf{v}_1^*(f_r) \mathbf{v}_1(f_r))} > \text{threshold}

Typically, threshold=0.8\text{threshold} = 0.8 ensures that only singular vectors corresponding to the same mode are included.

6.4Example for Peak Picking Method

Before demonstrating FDD, we show the simpler peak picking approach using ambient vibration data from the same two-span bridge. Peak picking traditionally uses FRFs from impact testing, but it can also be applied to PSD estimates from ambient vibration measurements. This approach identifies natural frequencies from peaks in the auto-PSD but cannot extract mode shapes without multiple measurement points.

The bridge dataset includes ambient vibration tests under broadband random excitation. A single sensor at mid-span of the longer span is selected to demonstrate how peak picking extracts frequencies from output-only measurements.

6.4.1Loading Ambient Test Data

The ambient vibration test data contains acceleration time series from all eight sensors. One sensor is used to demonstrate single-channel peak picking:

Notebook Cell
[OK] Loaded compressed data from /home/runner/work/SHM-Live-Book/SHM-Live-Book/common_utility_data/two_span_bridge_data.json.gz
Loading...
<Figure size 1050x390 with 1 Axes>

Figure 22:Vibration time history for sensor 7 showing stochastic response to environmental excitation.

6.4.2Computing the Power Spectral Density

For ambient vibration data, the auto-PSD of the acceleration response is estimated. Peaks in the PSD indicate natural frequencies where the structure responds strongly to ambient excitation. Welch’s method provides smooth spectral estimates:

Loading...

6.4.3Peak Detection and Modal Parameter Extraction

Peaks in the PSD are identified and the damping ratio is extracted for each peak using the half-power bandwidth method. Damping estimates from ambient PSD are less reliable than from impact FRF because of varying excitation characteristics:

Source
Processing sensor 1/8...
Processing sensor 3/8...
Processing sensor 5/8...
Processing sensor 7/8...
PSD computation complete. Averaged across 4 sensors.
Loading...

6.4.4Visualizing Peak Picking Results

The PSD plot shows clear peaks at natural frequencies. The half-power bandwidth method extracts damping from the peak width; sharper peaks indicate lower damping:

<Figure size 1050x810 with 3 Axes>

Figure 23:Peak picking analysis results from ambient vibration data.

The peak picking analysis identifies the first five natural frequencies with errors below 2%, demonstrating its effectiveness on ambient vibration PSD estimates. However, it has important limitations compared to FDD:

For complete modal characterization including mode shapes, FDD is more capable.

6.5Example for FDD Analysis

This example demonstrates the FDD method on experimental data from a two-span continuous bridge. The bridge has a 30-meter span followed by a 50-meter span, with a hinge support at the first support and roller supports at the intermediate and final supports. Eight accelerometers are positioned uniformly along the bridge.

The dataset includes both intact and damaged scenarios. The damaged case simulates a 50% stiffness reduction in the second span. Each scenario contains ambient vibration measurements under broadband excitation, providing the output-only data required for FDD analysis.

The FDD utility developed for this course implements the complete algorithm: PSD estimation, SVD decomposition, peak detection, and mode shape extraction. It also provides comparison with analytical ground truth mode shapes and computes MAC values for validation.

FDD Analysis Utility (click to expand)
fdd_utility.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
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
"""
FDD Utility for Structural Health Monitoring
============================================

Simple and clean Frequency Domain Decomposition (FDD) tool for students to perform 
operational modal analysis on ambient vibration data.

Course: Sensing Techniques and Data Analytics
Instructor: Mohammad Talebi-Kalaleh, University of Alberta

Usage Example:
--------------
```python
import numpy as np
from fdd_utility import FDD_Analysis

# Load your acceleration data (samples x sensors)
acceleration_data = np.array(...)  # Shape: (time_samples, n_sensors)
sampling_frequency = 200  # Hz

# Create FDD analyzer
fdd = FDD_Analysis(sampling_frequency)

# Perform analysis
results = fdd.analyze(acceleration_data, n_modes=4)

# Display results
fdd.print_results(results)

# Plot singular values
fdd.plot_singular_values(results)

# Plot mode shapes (if sensor positions provided)
sensor_positions = np.array([2.5, 7.5, 12.5, ...])  # positions in meters
fdd.plot_mode_shapes(results, sensor_positions)
```
"""

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import csd as cpsd
from scipy.linalg import svd
from scipy.signal import find_peaks
from scipy.interpolate import interp1d
import warnings

class FDD_Analysis:
    """
    Frequency Domain Decomposition (FDD) for Operational Modal Analysis
    
    A clean and simple implementation for students to identify modal parameters
    from ambient vibration data.
    """
    
    def __init__(self, sampling_frequency):
        """
        Initialize FDD analyzer
        
        Parameters:
        -----------
        sampling_frequency : float
            Sampling frequency in Hz
        """
        self.fs = sampling_frequency
        
    def compute_psd_matrix(self, acceleration_data):
        """
        Compute Cross-Power Spectral Density (CPSD) matrix
        
        Parameters:
        -----------
        acceleration_data : numpy.ndarray
            Acceleration data matrix (samples x sensors)
            
        Returns:
        --------
        frequencies : numpy.ndarray
            Frequency vector
        psd_matrix : numpy.ndarray
            PSD matrix (frequency x sensors x sensors)
        """
        n_samples, n_sensors = acceleration_data.shape
        
        # Choose appropriate segment length for good frequency resolution
        nperseg = min(4096, n_samples//8)
        noverlap = nperseg // 2
        
        print(f"Computing PSD matrix for {n_sensors} sensors...")
        print(f"Segment length: {nperseg}, Overlap: {noverlap}")
        
        # Initialize PSD matrix
        frequencies = None
        psd_matrix = None
        
        for i in range(n_sensors):
            for j in range(n_sensors):
                # Compute cross-power spectral density
                freqs, psd_ij = cpsd(acceleration_data[:, i], acceleration_data[:, j],
                                   nperseg=nperseg, noverlap=noverlap, 
                                   nfft=nperseg, fs=self.fs, window='hann')
                
                # Initialize arrays on first iteration
                if frequencies is None:
                    frequencies = freqs
                    n_freq = len(frequencies)
                    psd_matrix = np.zeros((n_freq, n_sensors, n_sensors), dtype=complex)
                
                psd_matrix[:, i, j] = psd_ij
        
        return frequencies, psd_matrix
    
    def perform_svd_analysis(self, psd_matrix):
        """
        Perform Singular Value Decomposition on PSD matrix
        
        Parameters:
        -----------
        psd_matrix : numpy.ndarray
            PSD matrix (frequency x sensors x sensors)
            
        Returns:
        --------
        singular_values : numpy.ndarray
            First singular values at each frequency
        mode_shapes : numpy.ndarray
            Mode shape vectors (sensors x frequency)
        """
        n_freq, n_sensors, _ = psd_matrix.shape
        
        print("Performing SVD analysis...")
        
        singular_values = np.zeros(n_freq)
        mode_shapes = np.zeros((n_sensors, n_freq), dtype=complex)
        
        for k in range(n_freq):
            # SVD of PSD matrix at frequency k
            u, s, vh = svd(psd_matrix[k, :, :])
            
            # Store first singular value and corresponding mode shape
            singular_values[k] = s[0]
            mode_shapes[:, k] = u[:, 0]
        
        return singular_values, mode_shapes
    
    def identify_modal_peaks(self, frequencies, singular_values, n_modes=4, 
                           freq_range=(0.5, 10), peak_prominence=None):
        """
        Identify modal frequencies from singular value peaks
        
        Parameters:
        -----------
        frequencies : numpy.ndarray
            Frequency vector
        singular_values : numpy.ndarray
            First singular values
        n_modes : int
            Number of modes to identify
        freq_range : tuple
            Frequency range to search (min_freq, max_freq) in Hz
        peak_prominence : float, optional
            Minimum prominence for peak detection
            
        Returns:
        --------
        modal_frequencies : list
            Identified modal frequencies
        peak_indices : list
            Indices of identified peaks in frequency vector
        """
        print(f"Identifying {n_modes} modal peaks in range {freq_range} Hz...")
        
        # Convert to dB scale for better peak detection
        sv_db = 10 * np.log10(singular_values + 1e-12)
        
        # Limit search to specified frequency range
        freq_mask = (frequencies >= freq_range[0]) & (frequencies <= freq_range[1])
        freq_limited = frequencies[freq_mask]
        sv_limited = sv_db[freq_mask]
        
        # Automatic prominence if not specified
        if peak_prominence is None:
            peak_prominence = (np.max(sv_limited) - np.min(sv_limited)) * 0.1
        
        # Find peaks
        peaks_idx, properties = find_peaks(sv_limited, prominence=peak_prominence, 
                                         distance=len(sv_limited)//20)
        
        if len(peaks_idx) == 0:
            print("Warning: No peaks found. Try adjusting peak_prominence or freq_range.")
            return [], []
        
        # Sort peaks by prominence (highest first)
        prominences = properties['prominences']
        sorted_indices = np.argsort(prominences)[::-1]
        
        # Select top n_modes peaks
        selected_peaks = peaks_idx[sorted_indices[:min(n_modes, len(peaks_idx))]]
        
        # Convert back to original frequency indices
        original_indices = np.where(freq_mask)[0][selected_peaks]
        modal_frequencies = frequencies[original_indices]
        
        # Sort by frequency
        sort_order = np.argsort(modal_frequencies)
        modal_frequencies = modal_frequencies[sort_order]
        peak_indices = original_indices[sort_order]
        
        print(f"Identified frequencies: {[f'{f:.3f}' for f in modal_frequencies]} Hz")
        
        return modal_frequencies.tolist(), peak_indices.tolist()
    
    def extract_mode_shapes(self, mode_shapes_complex, peak_indices):
        """
        Extract and process mode shapes at identified frequencies
        
        Parameters:
        -----------
        mode_shapes_complex : numpy.ndarray
            Complex mode shapes (sensors x frequency)
        peak_indices : list
            Indices of identified modal peaks
            
        Returns:
        --------
        mode_shapes_real : numpy.ndarray
            Real-valued mode shapes (sensors x modes)
        """
        n_sensors = mode_shapes_complex.shape[0]
        n_modes = len(peak_indices)
        
        mode_shapes_real = np.zeros((n_sensors, n_modes))
        
        for i, peak_idx in enumerate(peak_indices):
            # Get complex mode shape at peak frequency
            complex_mode = mode_shapes_complex[:, peak_idx]
            
            # Convert to real by phase alignment
            # Use the sensor with maximum amplitude as reference
            max_idx = np.argmax(np.abs(complex_mode))
            reference_phase = np.angle(complex_mode[max_idx])
            
            # Align all components to reference phase
            aligned_mode = complex_mode * np.exp(-1j * reference_phase)
            real_mode = np.real(aligned_mode)
            
            # Normalize (unit maximum amplitude)
            max_val = np.max(np.abs(real_mode))
            if max_val > 0:
                real_mode = real_mode / max_val
            
            mode_shapes_real[:, i] = real_mode
        
        return mode_shapes_real
    
    def analyze(self, acceleration_data, n_modes=4, freq_range=(0.5, 10), 
                peak_prominence=None):
        """
        Complete FDD analysis pipeline
        
        Parameters:
        -----------
        acceleration_data : numpy.ndarray
            Acceleration data matrix (samples x sensors)
        n_modes : int
            Number of modes to identify
        freq_range : tuple
            Frequency range to search (min_freq, max_freq) in Hz
        peak_prominence : float, optional
            Minimum prominence for peak detection
            
        Returns:
        --------
        results : dict
            Dictionary containing all analysis results:
            - 'frequencies': Frequency vector
            - 'singular_values': First singular values
            - 'modal_frequencies': Identified modal frequencies
            - 'mode_shapes': Identified mode shapes
            - 'peak_indices': Indices of modal peaks
            - 'psd_matrix': Cross-power spectral density matrix
        """
        print("="*50)
        print("FREQUENCY DOMAIN DECOMPOSITION (FDD) ANALYSIS")
        print("="*50)
        
        # Remove mean from each sensor (remove gravity/DC component)
        acc_data = acceleration_data - np.mean(acceleration_data, axis=0)
        
        print(f"Data shape: {acc_data.shape}")
        print(f"Duration: {acc_data.shape[0]/self.fs:.1f} seconds")
        print(f"Sampling frequency: {self.fs} Hz")
        
        # Step 1: Compute PSD matrix
        frequencies, psd_matrix = self.compute_psd_matrix(acc_data)
        
        # Step 2: Perform SVD analysis
        singular_values, mode_shapes_complex = self.perform_svd_analysis(psd_matrix)
        
        # Step 3: Identify modal peaks
        modal_frequencies, peak_indices = self.identify_modal_peaks(
            frequencies, singular_values, n_modes, freq_range, peak_prominence)
        
        # Step 4: Extract mode shapes
        if len(peak_indices) > 0:
            mode_shapes = self.extract_mode_shapes(mode_shapes_complex, peak_indices)
        else:
            mode_shapes = np.array([])
        
        # Compile results
        results = {
            'frequencies': frequencies,
            'singular_values': singular_values,
            'modal_frequencies': modal_frequencies,
            'mode_shapes': mode_shapes,
            'peak_indices': peak_indices,
            'psd_matrix': psd_matrix,
            'sampling_frequency': self.fs,
            'n_sensors': acc_data.shape[1],
            'duration': acc_data.shape[0] / self.fs
        }
        
        print("="*50)
        print("FDD ANALYSIS COMPLETE")
        print("="*50)
        
        return results
    
    def print_results(self, results):
        """
        Print formatted analysis results
        
        Parameters:
        -----------
        results : dict
            Results dictionary from analyze() method
        """
        print("\nFDD Analysis Results:")
        print("-" * 30)
        print(f"Number of sensors: {results['n_sensors']}")
        print(f"Data duration: {results['duration']:.1f} seconds")
        print(f"Frequency resolution: {results['frequencies'][1]:.4f} Hz")
        print(f"Number of modes identified: {len(results['modal_frequencies'])}")
        
        if len(results['modal_frequencies']) > 0:
            print("\nIdentified Modal Frequencies:")
            for i, freq in enumerate(results['modal_frequencies']):
                print(f"  Mode {i+1}: {freq:.3f} Hz")
    
    def plot_singular_values(self, results, save_filename=None, show_plot=True):
        """
        Plot singular values with identified peaks
        
        Parameters:
        -----------
        results : dict
            Results dictionary from analyze() method
        save_filename : str, optional
            Filename to save plot
        show_plot : bool
            Whether to display the plot
        """
        frequencies = results['frequencies']
        singular_values = results['singular_values']
        modal_frequencies = results['modal_frequencies']
        peak_indices = results['peak_indices']
        
        # Convert to dB
        sv_db = 10 * np.log10(singular_values + 1e-12)
        
        # Create plot
        fig, ax = plt.subplots(figsize=(7.0, 2.8), constrained_layout=True)

        # Plot singular values
        ax.plot(frequencies, sv_db, '-', color='#000000', linewidth=1.3, label='1st Singular Value')

        # Mark identified peaks
        for i, (freq, peak_idx) in enumerate(zip(modal_frequencies, peak_indices)):
            ax.plot(freq, sv_db[peak_idx], 'o', color='#D55E00', markersize=6,
                    markeredgewidth=0.6, markeredgecolor='white')
            ax.axvline(x=freq, color='#D55E00', linestyle='--', alpha=0.7, linewidth=0.8)
            ax.annotate(rf'$f_{{{i+1}}}$ = {freq:.3f} Hz',
                       xy=(freq, sv_db[peak_idx]),
                       xytext=(10, 20), textcoords='offset points',
                       arrowprops=dict(arrowstyle="->", color='#D55E00', lw=0.7),
                       fontsize=8,
                       bbox=dict(boxstyle="round,pad=0.3", facecolor="white",
                                 edgecolor='#dcdcdc', linewidth=0.5, alpha=0.9))

        # Formatting
        ax.set_xlabel('Frequency (Hz)')
        ax.set_ylabel('1st Singular Values (dB)')
        ax.grid(True, which='major', linewidth=0.5, color='#dcdcdc', alpha=0.6)

        # Set reasonable limits
        if len(modal_frequencies) > 0:
            ax.set_xlim([0, max(10, modal_frequencies[-1] * 2)])
        else:
            ax.set_xlim([0, 10])
        
        if save_filename:
            plt.savefig(save_filename, dpi=300, bbox_inches='tight')
            print(f"Singular value plot saved: {save_filename}")
        
        if show_plot:
            plt.show()
        else:
            plt.close()
    
    def plot_mode_shapes(self, results, sensor_positions, save_filename=None, 
                        show_plot=True, bridge_spans=None, support_positions=None,
                        ground_truth_shapes=None):
        """
        Plot identified mode shapes with optional ground truth comparison
        
        Parameters:
        -----------
        results : dict
            Results dictionary from analyze() method
        sensor_positions : numpy.ndarray
            Sensor positions along the structure (in meters)
        save_filename : str, optional
            Filename to save plot
        show_plot : bool
            Whether to display the plot
        bridge_spans : list, optional
            Span boundaries for vertical lines (e.g., [30] for 2-span bridge)
        support_positions : list or numpy.ndarray, optional
            Support positions to mark with triangles and enforce zero displacement
        ground_truth_shapes : numpy.ndarray, optional
            Ground truth mode shapes (sensors x modes) for comparison
        """
        modal_frequencies = results['modal_frequencies']
        mode_shapes = results['mode_shapes']
        
        if len(modal_frequencies) == 0:
            print("No modes to plot")
            return
        
        n_modes = len(modal_frequencies)
        
        # Create subplots
        fig, axes = plt.subplots(n_modes, 1, figsize=(7.0, 1.8*n_modes), constrained_layout=True)
        if n_modes == 1:
            axes = [axes]
        
        # Prepare ground truth data if provided
        gt_mode_shapes_with_supports = []
        if ground_truth_shapes is not None:
            for i in range(ground_truth_shapes.shape[1]):
                # Start with sensor positions and their corresponding mode shape values
                all_positions = sensor_positions.copy()
                all_values = ground_truth_shapes[:, i].copy()
                
                # Flip the sign of the 4th and 5th mode shapes (indices 3 and 4)
                if i == 3 or i == 4:  # 4th and 5th modes (0-indexed)
                    all_values = -all_values
                
                # Add support positions with zero y-values (boundary conditions)
                if support_positions is not None:
                    for support_pos_val in support_positions:
                        # Check if support position is not already in sensor positions
                        if not np.any(np.isclose(all_positions, support_pos_val, atol=0.1)):
                            all_positions = np.append(all_positions, support_pos_val)
                            all_values = np.append(all_values, 0.0)  # Zero at support
                
                # Sort by position
                sort_idx = np.argsort(all_positions)
                gt_mode_shapes_with_supports.append((all_positions[sort_idx], all_values[sort_idx]))

        for i in range(n_modes):
            mode_shape = mode_shapes[:, i]
            freq = modal_frequencies[i]
            
            # Add zero values at support locations if provided
            if support_positions is not None:
                support_positions_array = np.array(support_positions)
                # Combine sensor positions with support positions
                all_positions = np.concatenate([sensor_positions, support_positions_array])
                all_mode_values = np.concatenate([mode_shape, np.zeros(len(support_positions_array))])
                # Sort by position
                sort_idx = np.argsort(all_positions)
                plot_positions = all_positions[sort_idx]
                plot_mode_shape = all_mode_values[sort_idx]
            else:
                plot_positions = sensor_positions
                plot_mode_shape = mode_shape
            
            # Plot FDD mode shape
            axes[i].plot(plot_positions, plot_mode_shape, '-', color='#D55E00', linewidth=1.6,
                        marker='o', markersize=5, markeredgewidth=0.6,
                        markeredgecolor='white', label=f'FDD Mode {i+1}')
            axes[i].axhline(y=0, color='#000000', linestyle='--', alpha=0.5, linewidth=0.7)
            
            # Plot ground truth mode shape if provided
            if ground_truth_shapes is not None and i < len(gt_mode_shapes_with_supports):
                gt_positions, gt_values = gt_mode_shapes_with_supports[i]
                
                # Create fine position array for smooth ground truth curves
                x_gt_min = 0
                x_gt_max = support_positions[-1] if support_positions is not None else sensor_positions[-1]
                x_fine_gt = np.linspace(x_gt_min, x_gt_max, 200)
                
                # Interpolate ground truth for smooth curve using support points
                gt_interp = interp1d(gt_positions, gt_values, kind='cubic', 
                                   bounds_error=False, fill_value='extrapolate')
                gt_smooth = gt_interp(x_fine_gt)
                axes[i].plot(x_fine_gt, gt_smooth, '--', color='#7F7F7F', linewidth=1.0, alpha=0.85,
                            label='Ground Truth')
            
            # Plot triangles at support locations
            if support_positions is not None:
                for support_pos in support_positions:
                    axes[i].plot(support_pos, 0, marker='^', color='#009E73', markersize=8,
                               markerfacecolor='#009E73', markeredgecolor='#2b2b2b',
                               markeredgewidth=0.6, zorder=10)
                # Add support label only on first plot
                if i == 0:
                    axes[i].plot([], [], marker='^', color='#009E73', markersize=8,
                               markerfacecolor='#009E73', markeredgecolor='#2b2b2b',
                               linestyle='None', label='Supports')
            
            # Add span boundaries if provided
            if bridge_spans:
                for span_boundary in bridge_spans:
                    axes[i].axvline(x=span_boundary, color='#7F7F7F',
                                  linestyle=':', alpha=0.6, linewidth=0.8)

            # Formatting
            axes[i].set_xlabel('Position along Structure (m)')
            axes[i].set_ylabel('Normalized Amplitude')
            axes[i].set_title(f'Mode {i+1}: f = {freq:.3f} Hz')
            axes[i].grid(True, which='major', linewidth=0.5, color='#dcdcdc', alpha=0.6)
            axes[i].legend()
            
            # Set limits - extend ±2m from first and last support
            if support_positions is not None:
                x_min = support_positions[0] - 2
                x_max = support_positions[-1] + 2
            else:
                x_min = plot_positions[0]
                x_max = plot_positions[-1]
            axes[i].set_xlim([x_min, x_max])
            
            y_max = max(abs(plot_mode_shape.min()), abs(plot_mode_shape.max()))
            axes[i].set_ylim([-y_max*1.1, y_max*1.1])
        
        if save_filename:
            plt.savefig(save_filename, dpi=300, bbox_inches='tight')
            print(f"Mode shapes plot saved: {save_filename}")
        
        if show_plot:
            plt.show()
        else:
            plt.close()
    
    def calculate_mac(self, mode1, mode2):
        """
        Calculate Modal Assurance Criterion between two mode shapes
        
        Parameters:
        -----------
        mode1, mode2 : numpy.ndarray
            Mode shape vectors
            
        Returns:
        --------
        mac : float
            MAC value (0 to 1)
        """
        if len(mode1) != len(mode2):
            return 0.0
        
        numerator = (np.dot(mode1, mode2))**2
        denominator = np.dot(mode1, mode1) * np.dot(mode2, mode2)
        
        if denominator == 0:
            return 0.0
        
        return numerator / denominator

6.5.1Loading and Preparing the Data

The bridge data is stored in compressed JSON format. The data reader utility handles decompression and provides access to ground truth modal parameters, sensor locations, and acceleration time series:

Source
[OK] Loaded compressed data from /home/runner/work/SHM-Live-Book/SHM-Live-Book/common_utility_data/two_span_bridge_data.json.gz
Loading...

6.5.2Performing FDD Analysis

The FDD analysis utility implements the complete algorithm with parameters suited for bridge structures. The analysis includes PSD estimation using Welch’s method, SVD at each frequency, automatic peak detection, and mode shape extraction:

==================================================
FREQUENCY DOMAIN DECOMPOSITION (FDD) ANALYSIS
==================================================
Data shape: (6000, 8)
Duration: 60.0 seconds
Sampling frequency: 100 Hz
Computing PSD matrix for 8 sensors...
Segment length: 750, Overlap: 375
Performing SVD analysis...
Identifying 5 modal peaks in range (0, 20) Hz...
Identified frequencies: ['1.467', '3.733', '5.467', '10.800', '14.533'] Hz
==================================================
FDD ANALYSIS COMPLETE
==================================================
Loading...

6.5.3Visualizing FDD Results

The FDD utility provides singular value plots for peak identification and mode shape comparisons with ground truth. The singular value plot shows clear peaks at natural frequencies, with the first singular value dominating at each resonance:

<Figure size 1050x420 with 1 Axes>

Figure 24:FDD singular value decomposition showing the first three singular values as a function of frequency.

The mode shape comparison plot shows the FDD-identified mode shapes (discrete points connected with lines) and the analytical ground truth (smooth curves). Support locations are marked with green triangles:

<Figure size 1050x1350 with 5 Axes>

Figure 25:FDD-identified mode shapes (red dots connected with lines) compared with analytical ground truth (gray dashed curves) for the intact two-span bridge.

The MAC quantifies the correlation between FDD-identified mode shapes and ground truth. Values close to 1 indicate excellent agreement; values near 0 suggest uncorrelated mode shapes:

<Figure size 1050x600 with 2 Axes>

Figure 26:MAC Matrix Plot.

The MAC matrix shows diagonal values exceeding 0.99, confirming that each FDD-identified mode corresponds to its true counterpart. Off-diagonal values near zero indicate proper orthogonality between modes.


7Time-Frequency Analysis

Classical Fourier analysis works well for stationary signals, but structural signals change over time. The Fourier transform shows which frequencies are present but gives no information about when they occur. This is a problem for SHM applications where knowing when frequency changes happen is important for damage detection.

Consider a bridge experiencing damage during loading. Its natural frequencies drop as stiffness decreases, but the traditional power spectrum cannot localize this change in time. Sudden events such as impacts or connection failures create frequency content at specific times, but Fourier analysis spreads this energy across the entire time record.

Physics imposes a fundamental trade-off between time and frequency precision. Sharp time localization spreads energy across all frequencies, while precise frequency measurement requires long time windows. Different approaches are therefore needed to balance time and frequency information.

7.1Short-Time Fourier Transform (STFT)

The Short-Time Fourier Transform extends Fourier analysis to non-stationary signals by computing the transform over short, overlapping time windows. The STFT of a signal x(t)x(t) is defined as:

X(τ,ω)=∫−∞∞x(t) w(t−τ) e−jωt dtX(\tau,\omega) = \int_{-\infty}^{\infty} x(t)\, w(t- \tau)\, e^{-j\omega t}\, dt

where w(τ−t)w(\tau - t) is a window function centered at time tt. The discrete version for sampled data becomes:

X[h,k]=1Nw∑n=0Nw−1x[n] w[n−hR] e−j2πNwkn,h=0,1,…,(N−Nw)/RX[h, k] = \frac{1}{N_w} \sum_{n=0}^{N_w-1} x[n]\, w[n - h R]\, e^{-j \tfrac{2\pi}{N_w}kn}, \qquad h = 0, 1, \ldots, (N-N_w)/R

where w[n]w[n] is a window function of length NwN_w, hh is the time frame index, RR is the hop size between successive windows, and kk is the frequency bin index. The magnitude squared creates the spectrogram:

S[h,k]=∣X[h,k]∣2S[h, k] = |X[h, k]|^2

This creates a two-dimensional map showing how signal energy changes across time and frequency.

Short-Time Fourier Transform concept: A time-varying signal is segmented using overlapping windows, with each windowed segment transformed via DFT to reveal local frequency content [Source].

Figure 27:Short-Time Fourier Transform concept: A time-varying signal is segmented using overlapping windows, with each windowed segment transformed via DFT to reveal local frequency content [Source].

7.1.1Window Function Selection

The window function choice controls the time-frequency trade-off. Shorter windows give better time precision but worse frequency precision; longer windows give the opposite. Common choices include:

The window length NN controls the fundamental trade-off:

Δt⋅Δf≥14π\Delta t \cdot \Delta f \geq \frac{1}{4\pi}

Improving resolution in one domain always degrades it in the other.

Table 3:Common Window Functions for STFT Analysis.

Window TypeTime Domain PlotMathematical ExpressionKey Properties
Rectangular
<Figure size 300x180 with 1 Axes>
w[n]=1w[n] = 1Best frequency resolution, high sidelobes
Hanning
<Figure size 300x180 with 1 Axes>
w[n]=0.5(1−cos⁡(2πnNw−1))w[n] = 0.5\left(1 - \cos\left(\frac{2\pi n}{N_w-1}\right)\right)Good compromise, smooth tapering
Hamming
<Figure size 300x180 with 1 Axes>
w[n]=0.54−0.46cos⁡(2πnNw−1)w[n] = 0.54 - 0.46\cos\left(\frac{2\pi n}{N_w-1}\right)Lower sidelobes than Hanning
Gaussian
<Figure size 300x180 with 1 Axes>
w[n]=e−12(n−(Nw−1)/2σ(Nw−1)/2)2w[n] = e^{-\frac{1}{2}\left(\frac{n-(N_w-1)/2}{\sigma(N_w-1)/2}\right)^2}Optimal time-frequency localization

7.1.2Basic STFT Implementation

The core STFT computation in Python uses scipy.signal.stft:

1
2
3
4
5
6
7
8
9
10
11
12
from scipy import signal
import numpy as np

# Basic STFT computation
f, t_stft, Zxx = signal.stft(signal_data, fs, 
                            window='hann',      # Window function
                            nperseg=256,        # Window length
                            noverlap=192,       # Overlap samples
                            return_onesided=True)

# Create spectrogram
magnitude = np.abs(Zxx)
<Figure size 1050x810 with 5 Axes>

Figure 28:Short-Time Fourier Transform concept. (a) A non-stationary signal whose content changes every second, with the strip of overlapping Hann windows used by the STFT (four representative windows highlighted). (b) Each window tapers its segment of the signal. (c) The local spectrum of each window (decibel, self-normalized) reveals different dominant frequencies. (d) Stacking these local spectra in time gives the STFT spectrogram, which tracks how frequency content evolves.

7.1.3STFT as Feature Representation for Machine Learning

Spectrograms convert one-dimensional time series into two-dimensional images, enabling convolutional neural networks (CNNs) to analyze structural data. CNNs are well suited for image-like inputs and can detect patterns that are difficult to find in raw time series.

CNNs trained on spectrograms can automatically learn features for anomaly detection, damage classification, and remaining-life prediction. The time-frequency representation contains both spectral content and timing information, providing more information than time-domain or frequency-domain analysis alone.

For damage detection, CNNs trained on spectrograms can identify subtle frequency changes over time. The network learns normal operation patterns and flags deviations that suggest damage, eliminating manual feature selection.

<Figure size 1050x750 with 5 Axes>

Figure 29:Time domain and time-frequency analysis comparison for healthy and damaged structures.

7.2Wavelet Transform

STFT uses the same window length at all frequencies, giving uniform time-frequency resolution. This is suboptimal for structural applications where low frequencies need longer windows for frequency precision while high-frequency events need shorter windows for time precision.

Wavelet analysis addresses this with adaptive resolution. Wavelets use long windows at low frequencies and short windows at high frequencies, matching the natural behavior of structural signals that contain both slow modal responses and fast transient events.

7.2.1Continuous Wavelet Transform

The Continuous Wavelet Transform (CWT) breaks down a signal x(t)x(t) using scaled and shifted versions of a mother wavelet ψ(t)\psi(t):

CWT(s,τ)=1s∫−∞∞x(t)ψ∗(ts−τ)dtCWT(s,\tau) = \frac{1}{\sqrt{s}} \int_{-\infty}^{\infty} x(t) \psi^*\left(\frac{t}{s}-\tau \right) dt

where ss is the scale parameter (related to frequency), τ\tau is the translation parameter (time location), and the asterisk denotes complex conjugation. The factor 1/s1/\sqrt{s} keeps energy constant across scales.

The scale parameter ss relates to frequency as f≈fc/sf \approx f_c/s, where fcf_c is the center frequency of the mother wavelet. Small scales give high frequencies with good time precision; large scales give low frequencies with good frequency precision.

Wavelet transform as a multiscale microscope [Source].

Figure 30:Wavelet transform as a multiscale microscope [Source].

7.2.2Common Mother Wavelets and Their Transform Characteristics

Unlike sinusoidal Fourier basis functions that extend infinitely in time, wavelets are finite-duration oscillatory functions that decay to zero. This enables wavelets to capture transient phenomena and analyze signals with time-varying spectral content. The choice of mother wavelet determines the time-frequency resolution trade-off. Each wavelet has unique properties in terms of regularity, symmetry, compact support, and vanishing moments, which determine its suitability for different applications.

Table 4:Common Wavelets and Their Characteristics.

Wavelet TypeTime DomainMathematical ExpressionFrequency Domain
Morlet
<Figure size 300x180 with 1 Axes>
ψ(t)=1π1/2e−t2/2ejω0t\psi(t) = \frac{1}{\sqrt{\pi^{1/2}}} e^{-t^2/2} e^{j\omega_0 t}
<Figure size 300x180 with 1 Axes>
Mexican Hat (Ricker)
<Figure size 300x180 with 1 Axes>
ψ(t)=23 σ1/2π1/4(1−t2σ2)e−t2/2σ2\psi(t) = \frac{2}{\sqrt{3}\,\sigma^{1/2}\pi^{1/4}} \left(1-\frac{t^2}{\sigma^2}\right) e^{-t^2/2\sigma^2}
<Figure size 300x180 with 1 Axes>
Haar
<Figure size 300x180 with 1 Axes>
ψ(t)={10≤t<1/2−11/2≤t<10otherwise\psi(t) = \begin{cases} 1 & 0 \leq t < 1/2 \\ -1 & 1/2 \leq t < 1 \\ 0 & \text{otherwise} \end{cases}
<Figure size 300x180 with 1 Axes>
Shannon
<Figure size 300x180 with 1 Axes>
ψ(t)=sinc(t)−12[sinc(t−12)+sinc(t+12)]\psi(t) = \text{sinc}(t) - \frac{1}{2}[\text{sinc}(\frac{t-1}{2}) + \text{sinc}(\frac{t+1}{2})]
<Figure size 300x180 with 1 Axes>
Meyer
<Figure size 300x180 with 1 Axes>
Defined in frequency domain with smooth transitions
<Figure size 300x180 with 1 Axes>

7.2.3Discrete Wavelet Transform

The Discrete Wavelet Transform (DWT) implements wavelet analysis efficiently by sampling scales and translations on a dyadic grid, reducing computational cost from O(N2)O(N^2) to O(N)O(N) while preserving multiresolution properties.

The DWT decomposes a signal using dilated and translated versions of the mother wavelet ψ(t)\psi(t):

ψj,h(t)=12jψ(t2j−h)\psi_{j,h}(t) = \frac{1}{\sqrt{2^j}} \psi\left(\frac{t}{2^j} - h \right)

where jj controls the scale (frequency content) and hh controls the time position. The wavelet coefficients are:

dj,h=∫−∞∞x(t)ψj,h(t)dtd_{j,h} = \int_{-\infty}^{\infty} x(t) \psi_{j,h}(t) dt

The DWT uses a two-channel filter bank with high-pass (wavelet) and low-pass (scaling) filters. At each decomposition level jj, the signal splits into detail coefficients djd_j (high-frequency) and approximation coefficients aja_j (low-frequency).

7.2.4Basic CWT Implementation

The CWT in Python using PyWavelets:

1
2
3
4
5
6
7
8
9
import pywt
import numpy as np

# Basic CWT computation
scales = np.arange(1, 64)  # Scale range
coefficients, frequencies = pywt.cwt(signal_data, scales, 'cmor1.5-1.0', 1/fs)

# Create scalogram
magnitude = np.abs(coefficients)

Key Parameters:

<Figure size 1050x810 with 5 Axes>

Figure 31:STFT and CWT demonstration showing multi-resolution analysis capabilities. Wavelets provide adaptive time-frequency resolution optimal for analyzing multi-scale structural responses.


8Exercises


End of Chapter 4

This chapter has introduced the key frequency-domain analysis techniques for structural health monitoring of bridges, from basic Fourier transforms to wavelet analysis and non-parametric modal identification. The next chapter covers Feature Engineering and Statistical Anomaly Detection.


9References

  1. Brincker, R., Zhang, L., & Andersen, P. (2000). Modal identification from ambient responses using frequency domain decomposition. Proceedings of the 18th International Modal Analysis Conference (IMAC), San Antonio, Texas.

  2. Bendat, J. S., & Piersol, A. G. (2010). Random data: analysis and measurement procedures (4th ed.). John Wiley & Sons.

  3. Welch, P. (1967). The use of fast Fourier transform for the estimation of power spectra: a method based on time averaging over short, modified periodograms. IEEE Transactions on audio and electroacoustics, 15(2), 70-73.

  4. Daubechies, I. (1992). Ten lectures on wavelets. Society for Industrial and Applied Mathematics.

  5. Peeters, B., & De Roeck, G. (2001). Stochastic system identification for operational modal analysis: a review. Journal of Dynamic Systems, Measurement, and Control, 123(4), 659-667.

  6. Reynders, E. (2012). System identification methods for (operational) modal analysis: review and comparison. Archives of Computational Methods in Engineering, 19(1), 51-124.

  7. Brownjohn, J. M. W. (2007). Structural health monitoring of civil infrastructure. Philosophical Transactions of the Royal Society A, 365(1851), 589-622.

  8. Farrar, C. R., & Worden, K. (2012). Structural health monitoring: a machine learning perspective. John Wiley & Sons.

  9. Mallat, S. (1999). A wavelet tour of signal processing. Academic Press.

  10. Rainieri, C., & Fabbrocino, G. (2014). Operational modal analysis of civil engineering structures. Springer.

  11. Magalhaes, F., Cunha, Ã., & Caetano, E. (2012). Vibration based structural health monitoring of an arch bridge: from automated OMA to damage detection. Mechanical Systems and Signal Processing, 28, 212-228.

  12. Au, S. K. (2017). Operational modal analysis: modeling, Bayesian inference, uncertainty laws. Springer.

  13. Deraemaeker, A., Reynders, E., De Roeck, G., & Kullaa, J. (2008). Vibration-based structural health monitoring using output-only measurements under changing environment. Mechanical Systems and Signal Processing, 22(1), 34-56.

  14. Worden, K., Farrar, C. R., Manson, G., & Park, G. (2007). The fundamental axioms of structural health monitoring. Proceedings of the Royal Society A, 463(2082), 1639-1664.

  15. Zhang, L., Brincker, R., & Andersen, P. (2005). An overview of operational modal analysis: major development and issues. Proceedings of the 1st International Operational Modal Analysis Conference, 179-190.

References
  1. Brincker, R., & Ventura, C. E. (2015). Introduction to Operational Modal Analysis. Wiley. 10.1002/9781118535141