---
title: How to Capture Field Shock and Vibration Data in 7 Steps
description: Learn how to capture high-frequency shock and vibration data in the field using a vibration data logger, from mounting to PSD analysis.
---

[enDAQ Blog for Data Sensing and Analyzing](https://blog.endaq.com)

# [How to Capture Field Shock and Vibration Data in 7 Steps](https://blog.endaq.com/how-to-capture-field-shock-and-vibration-data-in-7-steps)

 Written by [Giovanni Fava](https://blog.endaq.com/author/giovanni-fava) | Nov 3, 2026, 7:54:03 PM

## 7 Steps to Capture Field Shock & Vibration Data

Getting accurate shock and vibration data outside the lab is one of the trickier problems with testing prototypes. The combination of temperature, humidity, shock, and vibration are often difficult to predict, and mimic in the lab. Measuring the real combination in the field can be extremely useful.

When testing in the field, a data logger can be your best friend. In this article I will walk you through how to capture shock and vibration data in the field, and in the case of this post, I will also include an example where I will show how to collect data from a troublesome HVAC system. The goal of this post is to show you how I to configure your recording device, where and how to use it, and a brief analysis of the data. 

## Table of Contents:

- [Summary of data collection steps](https://blog.endaq.com/how-to-capture-field-shock-and-vibration-data-in-7-steps?hs_amp=true#quick)
- [How to capture shock and vibration data in real-world conditions - details](https://blog.endaq.com/how-to-capture-field-shock-and-vibration-data-in-7-steps?hs_amp=true#how)
- [What sample rate do you need for field vibration measurements](https://blog.endaq.com/how-to-capture-field-shock-and-vibration-data-in-7-steps?hs_amp=true#sample)
- [How does transit vibration differ from laboratory shaker testing](https://blog.endaq.com/how-to-capture-field-shock-and-vibration-data-in-7-steps?hs_amp=true#transit)
- [Frequently asked questions](https://blog.endaq.com/how-to-capture-field-shock-and-vibration-data-in-7-steps?hs_amp=true#faqs)
- [Conclusion](https://blog.endaq.com/how-to-capture-field-shock-and-vibration-data-in-7-steps?hs_amp=true#conclusion)

## Summary of Data Collection Steps

1. **Define your measurement objectives:** Figure out if you are looking to understand a shock event, get a vibrational profile, or measure environmental conditions. Once you know what you are measuring. You must figure out how often you need to record the data, is it constantly, periodically, or only once an event occurs?
2. **Select the right accelerometer(s) and environmental sensors:** Pick your accelerometer based on the shock and vibration profiles with room to spare so that you reduce the risk of clipping incase there are spikes in the data. The environmental sensors can be just as important as the accelerometers and can range from temperature and humidity sensors to even microphones to detect the noise level and frequency of a sound.
3. **Plan your sensor mounting:** Based on what you want to measure, figure out the best way to mount your data logger. If you're looking for accurate vibration recordings, mount your logger rigidly to the surface you want to measure. If you want environmental conditions, make sure that the device is experiencing the same environment you want.
4. **Configure sample rates and trigger settings:** For the accelerometer's sample rate, the Nyquist Theorem requires the sample frequency to be double the frequency of interest; in practice, a sample rate of 5x to 10x the max frequency is preferred. For audio recordings on the other hand, the max audible frequency is 20,000 KHz.
5. **Deploy and record data in the field:** Secure the logger (plug it in to power if necessary), press record, and let the device capture data on its own.
6. **Transfer and organize your recorded files:** Download files via USB or Wi-Fi and label them so that you know what each recording comes from.
7. **Analyze your field data with PSD and SRS:** Process your recordings based on what information you are looking for. PSDs can be used to better understand the energy of vibration data. Waterfall plots can help understand how frequencies in a system change over time. Simple time domain analysis can help with understanding temperature and humidity.

## How to Capture Shock and Vibration Data in Real-World Conditions

### 1. Define your measurement objectives

Before running to take a measurement (which can be a lot of fun), you need to know what you are measuring, and what the goal of the measurement is.  Are you testing to a specific spec, or do you want to begin defining a spec? Are you debugging an issue or you trying to identify a resonance in a structure?

The first thing to do is to check to see if there any potentially dangerous or damaging environmental conditions that could limit what logger you pick. Some of the most common environmental hazards are: high temperatures, low temperatures, and water. Every sensor and logger will have a rated temperature range, beyond which, you run the risk of degrading or damaging your components. Water is the next big issue; if you think that water or other fluids may be dangerous to your logger check out its [IP rating](https://www.iec.ch/ip-ratings) to see how much exposure to water it can handle.

Once you know what kind of environment the sensor will be in, the vibration usually comes next. First, you figure out what g level (or levels) you need to measure. general everyday vibration will be less than 10 g, meanwhile shock events can easily get above 500 g, up to even 2000 g or more. Finally recording frequency needs to be sorted. The main thing to look for is a sensor's frequency bandwidth, rather than just its sample rate, since frequencies past that bandwidth will often be distorted. 

Lab Experiment: In our engineering lab, whenever the AC kicks on, the sound is pretty abrupt and loud. I want to figure out how loud it is and what kind of vibrations might be causing this sound. The requirements for my measurement are that there must be a microphone on the data logger, and since the sound is a fairly low frequency, I only need my accelerometer to measure up to 500 Hz at 10 g.

### 2. Select the right accelerometer and data logger

Once you know what kind of vibration to measure, you can then choose the type of accelerometer. Different types of accelerometers are good at different things, and you can read in detail about those differences in [this blog](https://blog.endaq.com/accelerometer-selection). In short, piezoelectric accelerometers are great at high g high frequency signals, while piezo resistive are better at high g and low-high frequency signals, while mems accelerometers are best at low frequency low g acceleration.

The "extra" sensors that a logger has can be just as important as the main accelerometer since the whole point of the loggers is to simplify recordings, it is very beneficial that all the other sensors (temperature, humidity, audio...) measure what you need.

Lab experiment: The main thing I'm looking for in my data logger is a microphone to pick up audio signals in the audible frequency range. The next thing I'm looking for is a low g accelerometer that can measure up to 500 Hz.  Finally I also need a battery life that can exceed 24 hours, since I only want to pick it up once. In the case I chose to use an [enDAQ W8-D40](https://endaq.com/products/w8-vibration-sensor-w8-d40?srsltid=AU7gw4WoSzwnjvvqtZi3wfNOsflJOrxifGNqz3hxYiW0-sHRG6xFs1vr). As a disclaimer, any other good data logger with GPS can be used to capture these recordings, and you can also analyze the data using python through Jupyter notebook as well.

### 3. Plan your sensor mounting

Mounting can be one of the biggest sources of error when getting shock and vibration data. If the mounting is loose, the device can act like a hinge or spring and show natural frequencies in the acceleration data where none exist. On the other hand, it is also important to minimize the effect that a sensor has on the structure it is trying to measure. If your sensor is too heavy or too rigidly connected, it may end up shifting around the natural frequencies and mode shapes of the structure its trying to measure, particularly if that structure is small, delicate and flexible.

Sometimes mounting needs to be permanent and other times it can be temporary. For more permanent installation, mounting with studs, bolts, and even epoxy can work best. Meanwhile for temporary installations tape or magnets can be ideal. For a deeper discussion, check out our post on [accelerometer mounting](https://blog.endaq.com/accelerometer-mounting-best-practices-for-vibration-measurement).

Lab experiment: In the case of the test I want to perform, I don't plant on leaving my data logger on the vents long-term, and I don't want to leave any holes. I will use double-sided tape that is stuck to the back of the logger to attach it to the vent, and then I can remove it without a problem when I finish collecting data. 

  

**Figure 1:** Clear, concise caption describing the visual data or concept, incorporating relevant semantic keywords for answer engines to index.

### 4. Configure sample rates and trigger settings

When setting up a device, the first thing to do is to set up the sample rates. Depending on the application you can adjust your sample rate. At a bare minimum you want a sample rate double the highest frequency you are interested in; in reality 5x to 10x the max frequency is preferred. The other major limiter is storage space, the more data points you collect per second, the faster your sensor will run out of space, or the longer it will take to analyze the data. 

Trigger settings can be one of the easiest ways to only get the data you need, and save on space. If you are interested only in shock data, and the vibrations that occur around a shock, triggering can be a very useful way to pick when to capture data, letting you have high sample-rate recordings while not filling up your storage.

Lab experiment: For this experiment, I know my data logger has a lot of storage capacity, and I can control when I turn on the AC, so I don't need to set a trigger. On the other hand, I have no idea what kind of frequencies my logger will see, so I plan to max out the accelerometers and microphones at 20 kHz each.

**Figure 2:** Configuration settings for the data logger that I used to capture the vent recordings.

### 5. Deploy and record data in the field

Now that your device is set up for your experiment, and you have the mounting planned out, the next step is to collect the data itself. Place your sensor where it needs to go, and wait for the experiment to finish. With your sensor mounted and configured, deployment is straightforward, just turn on your logger and let it run.

Lab experiment: For this experiment, the data logger was taped to the side of the vent. Since I can control when the AC fan is turned on and off, I set up the device up. Waited about 100 seconds before turning on the fan to the vent, then left the vent on for about 8 minutes. This gave me a good understanding of the sound and vibration before, during, and after the AC fan comes on.

**Figure 3:** Data logger mounted to the vent for testing. The fan that feeds the vent is located directly above the vent.

### 6. Transfer and organize your recorded files

Once you have taken your recordings organizing them is the next step. The more files you have, especially if triggering is involved, the more important good organization is. make sure label your recordings, use appropriate folder structures and more. Once you have all the data you need, you may have to convert it to a readable format that the analysis software you are using is compatible with such as .MAT and .CSV. 

Lab experiment: From the experiment I carried out, I can see that when the AC vent is on, there is a consistent, loud sound and a spike in sound right as the AC comes on. Once I've checked out my data in the software. I will download the data, and analyze it in Jupyter notebook.

**Figure 4:** Microphone recording results from when the vent was being test showing that near 100 seconds the fan turns on, and then near 600 seconds it turns off.

### 7. Analyze your field data with PSD and SRS

This can be one of the more fun and exciting aspects of collecting shock and vibration data since you can discover all sorts of very interesting results, like unexpected frequencies or more vibration coming from a certain effect. 

For shock events (drops, impacts, rail coupling), compute a [shock response spectrum (SRS)](https://blog.endaq.com/shock-analysis-response-spectrum-srs-pseudo-velocity-severity). The SRS tells you the peak response of single-degree-of-freedom oscillators across a range of natural frequencies, which relates directly to the damage potential of the event.

[Spectrograms](https://blog.endaq.com/vibration-analysis-fft-psd-and-spectrogram) can also be very useful tools for dynamic systems. A spectrogram can reveal if how different excitations can lead to different structural responses. Similarly, a waterfall plot can show the effect through time on the structure.

Lab experiment: Now that I have collected my data, I will analyze it using [Jupyter notebook,](https://jupyter.org/)  they are a free and easy way to run python code through your browser. In the case of this experiment with the vent fan, I have a shock that comes from when the motor kicks on, and then a long, sustained vibration while the fan is running. You can see below that I have plotted a spectrogram of the a sound coming from the vent, and the acceleration signal. You can see the what happens when the vent turns on around 100 seconds, and then turns off around 600 seconds.

**Figure 5:** Sound pressure level (SPL) measured by the microphone (top). Spectrogram of SPL at each frequency for the duration of the recording (bottom).

**Figure 6:** Spectrogram of the resultant acceleration, showing that the main excited frequency when the fan is running is at around 60 Hz.

**Figure 7:** Shock response spectrum of the vent fan startup, showing a peak of 3.3 g at 57 Hz (left). PSD of the same startup of the vent fan (right).

The data shows that the exciting frequency is close to 60 Hz, which makes since since the motor that drives the fan is likely synchronized with the 60 Hz electricity. Below, you can 

```
FILE = r"M:\FS2003-Public\Giovanni\enDAQ_blog_Vent_Fan\DAQ18065_000059.IDE"   # the .IDE file from the W8

# Fan switch-on / switch-off, in seconds from the start of the recording.
# None = detect automatically from the 40 g accelerometer.
FAN_ON_TIME  = 109.5
FAN_OFF_TIME = None

# --- Microphone / SPL ---
SPL_TIME_CONSTANT = 0.125   # s. 0.125 = sound-level-meter "Fast", 1.0 = "Slow"
A_WEIGHTING       = False   # True -> SPL-vs-time trace in dBA (spectrogram stays unweighted)
MIC_HIGHPASS_HZ   = 20.0    # removes the mic's DC offset and sub-audio drift
MIC_SPEC_DF       = 5.0     # Hz, frequency resolution of the acoustic spectrogram

# --- Accelerometer spectrogram ---
ACC_SPEC_DF       = 1.0     # Hz, frequency resolution of the vibration spectrogram

# --- Both spectrograms ---
LOG_FREQ_AXIS     = True    # False -> linear frequency axis (easier to see harmonic spacing)
DB_RANGE          = 60      # dB of dynamic range in the colour scale

# --- Fan start-up: SRS and PSD ---
SHOCK_PRE         = 1.0     # s of quiet data kept before switch-on (also used to remove gravity)
SHOCK_POST        = 5.0     # s of data kept after switch-on
SRS_DAMPING       = 0.05    # fraction of critical damping (0.05 -> Q = 10)
SRS_FMIN          = 1.0     # Hz
SRS_FMAX          = None    # Hz; None -> accelerometer sample rate / 10
SRS_BINS_PER_OCT  = 12
PSD_DF            = 1.0     # Hz, target resolution of the start-up PSD

SAVE_FIGURES      = False   # True -> also save each figure as a PNG next to the .IDE file
# %pip install endaq          # <- uncomment and run once if the enDAQ library isnt installed
import warnings
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from scipy import signal
from scipy.ndimage import uniform_filter1d
import endaq.ide

try:
    from IPython.display import display
except ImportError:                      # running outside Jupyter
    display = print

plt.rcParams.update({
    "figure.dpi": 110, "font.size": 10, "axes.titlesize": 11, "axes.titleweight": "bold",
    "axes.titlelocation": "left", "axes.labelcolor": "#3a3936", "axes.edgecolor": "#b9b8b2",
    "axes.spines.top": False, "axes.spines.right": False, "axes.grid": True,
    "grid.color": "#e4e3df", "grid.linewidth": 0.6, "xtick.color": "#52514e", "ytick.color": "#52514e",
    "legend.frameon": False,
})

P_REF = 20e-6                                    # Pa, reference pressure for dB SPL
AXIS_COLORS = {"X": "#2a78d6", "Y": "#eb6834", "Z": "#1baf7a", "Resultant": "#1f1f1f"}
SPEC_CMAP = "magma"

# The W8 stores the microphone in Pa (newer firmware) or in raw "A" units (older firmware).
# The "A" -> Pa factor is the one enDAQ's own endaq.batch analyzer uses.
MIC_UNIT_TO_PA = {"pa": 1.0, "a": -5.307530522779073}

# ------------------------------- helpers -----------------------------------
def next_pow2(n):
    return int(2 ** np.ceil(np.log2(max(n, 2))))

def prev_pow2(n):
    return int(2 ** np.floor(np.log2(max(n, 2))))

def to_uniform(df):
    """enDAQ timestamps jitter slightly; put the samples on an evenly spaced grid at the mean rate."""
    t = df.index.to_numpy(dtype=float)
    fs = (len(t) - 1) / (t[-1] - t[0])
    tu = t[0] + np.arange(len(t)) / fs
    data = np.column_stack([np.interp(tu, t, df[c].to_numpy(dtype=float)) for c in df.columns])
    return pd.DataFrame(data, index=pd.Index(tu, name="time (s)"), columns=df.columns), fs

def highpass(x, fs, fc, order=4):
    sos = signal.butter(order, fc, btype="highpass", fs=fs, output="sos")
    return signal.sosfiltfilt(sos, x, axis=0)

def a_weighting_sos(fs):
    """IEC 61672 A-weighting, bilinear-transformed and normalised to 0 dB at 1 kHz.
    Tracks the standard curve closely up to about fs/10 (fine for a 20 kHz mic rate)."""
    w1, w2, w3, w4 = 2 * np.pi * np.array([20.598997, 107.65265, 737.86223, 12194.217])
    z, p = np.zeros(4), np.array([-w4, -w4, -w1, -w1, -w2, -w3])
    zd, pd_, kd = signal.bilinear_zpk(z, p, w4 ** 2, fs)
    sos = signal.zpk2sos(zd, pd_, kd)
    _, h = signal.sosfreqz(sos, worN=[1000.0], fs=fs)
    sos[0, :3] /= abs(h[0])
    return sos

def exp_time_weighting(p, fs, tau):
    """Running mean-square with an exponential time constant (sound-level-meter style)."""
    alpha = np.exp(-1.0 / (fs * tau))
    b, a = [1.0 - alpha], [1.0, -alpha]
    p2 = p ** 2
    zi = signal.lfilter_zi(b, a) * p2[: max(1, int(fs * tau))].mean()
    return signal.lfilter(b, a, p2, zi=zi)[0]

def spectrogram(x, fs, df, scaling, max_frames=2000):
    """x: (N,) or (N, n_axes). Returns f, t (s from start of x), S summed over axes.
    Frames are power-averaged in groups if there would be more than max_frames columns."""
    x = np.atleast_2d(np.asarray(x).T)                 # (n_axes, N)
    nper = min(next_pow2(fs / df), prev_pow2(x.shape[-1]))
    f, t, S = signal.spectrogram(x, fs=fs, window="hann", nperseg=nper, noverlap=nper // 2,
                                 detrend="constant", scaling=scaling, mode="psd")
    S = S.sum(axis=0)                                  # (n_freq, n_frames)
    n = S.shape[-1]
    if n > max_frames:
        k = int(np.ceil(n / max_frames)); m = n // k
        S = S[:, : m * k].reshape(S.shape[0], m, k).mean(-1)
        t = t[: m * k].reshape(m, k).mean(-1)
    return f, t, S

def detect_fan_on_off(acc, fs, block_s=0.05, smooth_s=0.5, min_run_s=3.0):
    """Find when the vibration level steps up (fan on) and back down (fan off).
    Uses a 0.5 s RMS envelope of the gravity-free resultant; brief bumps (< min_run_s) are ignored."""
    hp = highpass(acc.to_numpy(), fs, 2.0)
    r2 = (hp ** 2).sum(axis=1)
    blk = max(1, int(block_s * fs)); nb = len(r2) // blk
    env = np.sqrt(r2[: nb * blk].reshape(nb, blk).mean(axis=1))
    env = uniform_filter1d(env, size=max(1, int(smooth_s / block_s)))
    tb = acc.index[0] + (np.arange(nb) + 0.5) * blk / fs

    lo, hi = np.percentile(env, [2, 90])
    if hi < 2 * lo:
        warnings.warn("No clear step in vibration level was found; set FAN_ON_TIME / FAN_OFF_TIME by hand.")
        return None, None
    thr = np.sqrt(lo * hi)                              # halfway between quiet and running, in dB
    edges = np.diff(np.r_[0, (env > thr).astype(int), 0])
    starts, ends = np.flatnonzero(edges == 1), np.flatnonzero(edges == -1)
    keep = (ends - starts) * block_s >= min_run_s
    starts, ends = starts[keep], ends[keep]
    if not len(starts):
        warnings.warn("Vibration never stayed high for long; set FAN_ON_TIME / FAN_OFF_TIME by hand.")
        return None, None

    near_quiet = lo * (hi / lo) ** 0.1                  # 10 % of the way up (in dB)
    near_running = hi * (lo / hi) ** 0.1                # 10 % of the way down
    j = starts[0]                                       # walk back to where the rise began
    while j > 0 and env[j] > near_quiet:
        j -= 1
    k = ends[-1] - 1                                    # walk back to where the decay began
    while k > 0 and env[k] < near_running:
        k -= 1
    t_on = float(tb[j])
    t_off = float(tb[k]) if ends[-1] < nb else None     # still running at end of file -> no "off"
    return t_on, t_off

def shock_response_spectrum(acc, fs, freqs, damp):
    """Maximax absolute-acceleration SRS (Smallwood ramp-invariant digital filter).
    acc: (N, n_axes) with gravity removed. Returns per-axis (n_freq, n_axes) and resultant (n_freq,)."""
    acc = np.asarray(acc, dtype=float)
    dt = 1.0 / fs
    per_axis = np.empty((len(freqs), acc.shape[1])); resultant = np.empty(len(freqs))
    for i, fn in enumerate(freqs):
        wn = 2 * np.pi * fn
        E = np.exp(-damp * wn * dt)
        K = wn * np.sqrt(1 - damp ** 2) * dt
        C, S = E * np.cos(K), E * np.sin(K)
        Sp = S / K
        b = [1 - Sp, 2 * (Sp - C), E ** 2 - Sp]
        a = [1, -2 * C, E ** 2]
        y = signal.lfilter(b, a, acc, axis=0)        # absolute acceleration of the SDOF mass
        per_axis[i] = np.abs(y).max(axis=0)
        resultant[i] = np.sqrt((y ** 2).sum(axis=1)).max()
    return per_axis, resultant

def mark_events(ax, t_on, t_off, color="#52514e", where="top", on_side="right"):
    """Dashed lines at fan on / fan off. Labels sit inside the fan-running interval by default."""
    for t, name, side in ((t_on, "fan on", on_side), (t_off, "fan off", "left")):
        if t is None:
            continue
        ax.axvline(t, color=color, lw=1.2, ls="--", alpha=0.9)
        y, va, dy = (1, "top", -4) if where == "top" else (0, "bottom", 4)
        ax.annotate(name, (t, y), xycoords=("data", "axes fraction"), xytext=(4 if side == "right" else -4, dy),
                    textcoords="offset points", ha="left" if side == "right" else "right",
                    va=va, fontsize=9, color=color)

def save(fig, suffix):
    if SAVE_FIGURES:
        out = Path(FILE).with_name(f"{Path(FILE).stem}_{suffix}.png")
        fig.savefig(out, dpi=150, bbox_inches="tight")
        print("saved", out)

      doc = endaq.ide.get_doc(FILE)
display(endaq.ide.get_channel_table(doc))

# Microphone: the subchannel whose measurement type is "Audio" (named "Mic" or "Audio", normally 8.3)
mic_sc = next((sc for ch in doc.channels.values() for sc in ch.subchannels
               if str(sc.units[0]).lower() == "audio" or sc.name.lower() in ("mic", "audio", "microphone")), None)
if mic_sc is None:
    raise RuntimeError("No microphone channel in this file - check that the mic was enabled in the recorder config.")
mic_units = str(mic_sc.units[1])
to_pa = MIC_UNIT_TO_PA.get(mic_units.lower())
if to_pa is None:
    warnings.warn(f"Unrecognised microphone units '{mic_units}' - treating them as Pa.")
    to_pa = 1.0
mic_raw = endaq.ide.to_pandas(mic_sc, time_mode="seconds") * to_pa
mic_raw.columns = ["p"]

# 40 g DC accelerometer: the channel whose subchannels are "X (40g)", "Y (40g)", "Z (40g)" (normally 80)
acc_ch = next((ch for ch in doc.channels.values()
               if sum("(40g)" in sc.name.replace(" ", "") for sc in ch.subchannels) >= 3), doc.channels.get(80))
if acc_ch is None:
    raise RuntimeError("No 40 g accelerometer channel found in this file.")
acc_raw = endaq.ide.to_pandas(acc_ch, time_mode="seconds")
acc_raw = acc_raw[[c for c in acc_raw.columns if c.strip()[:1] in "XYZ"]]
acc_raw.columns = [c.strip()[0] for c in acc_raw.columns]          # -> "X", "Y", "Z"

print(f"Microphone : channel {mic_sc.parent.id}.{mic_sc.id} '{mic_sc.name}', stored in {mic_units} (x {to_pa:g} -> Pa)")
print(f"Accelerometer: channel {acc_ch.id} {[sc.name for sc in acc_ch.subchannels]}")

mic, fs_mic = to_uniform(mic_raw)
acc, fs_acc = to_uniform(acc_raw)
p = highpass(mic["p"].to_numpy(), fs_mic, MIC_HIGHPASS_HZ)        # acoustic pressure, Pa

detected_on, detected_off = detect_fan_on_off(acc, fs_acc)
t_on  = FAN_ON_TIME  if FAN_ON_TIME  is not None else detected_on
t_off = FAN_OFF_TIME if FAN_OFF_TIME is not None else detected_off

fmt = lambda t: "-" if t is None else f"{t:.2f} s"
print(f"Microphone   : {fs_mic:8.1f} Hz, {mic.index[0]:.2f}-{mic.index[-1]:.2f} s")
print(f"Accelerometer: {fs_acc:8.1f} Hz, {acc.index[0]:.2f}-{acc.index[-1]:.2f} s")
print(f"Fan on : {fmt(t_on)}   ({'set by hand' if FAN_ON_TIME is not None else 'auto-detected'})")
print(f"Fan off: {fmt(t_off)}   ({'set by hand' if FAN_OFF_TIME is not None else 'auto-detected'})")

# --- SPL vs time ---
p_w = signal.sosfilt(a_weighting_sos(fs_mic), p) if A_WEIGHTING else p
ms = exp_time_weighting(p_w, fs_mic, SPL_TIME_CONSTANT)
step = max(1, int(fs_mic * SPL_TIME_CONSTANT / 10))               # ~10 points per time constant
t_spl = mic.index.to_numpy()[::step]
spl = 10 * np.log10(np.maximum(ms[::step], 1e-30) / P_REF ** 2)

unit = "dBA" if A_WEIGHTING else "dB"
weighting = {0.125: "Fast", 1.0: "Slow"}.get(SPL_TIME_CONSTANT, f"τ = {SPL_TIME_CONSTANT} s")
leq_all = 10 * np.log10(np.mean(p_w ** 2) / P_REF ** 2)
leq_txt = f"Leq whole recording {leq_all:.1f} {unit}"
if t_on is not None:
    tt = mic.index.to_numpy()
    sel = (tt >= t_on) & (tt <= (t_off if t_off is not None else tt[-1]))
    leq_txt += f"  ·  Leq fan running {10 * np.log10(np.mean(p_w[sel] ** 2) / P_REF ** 2):.1f} {unit}"

# --- acoustic spectrogram, SPL per band ---
f_a, t_a, S_a = spectrogram(p, fs_mic, MIC_SPEC_DF, scaling="spectrum")   # S_a = mean-square Pa per band
t_a = t_a + mic.index[0]
df_a = f_a[1] - f_a[0]
keep = f_a >= max(MIC_HIGHPASS_HZ, df_a)
f_a, L_a = f_a[keep], 10 * np.log10(np.maximum(S_a[keep], 1e-30) / P_REF ** 2)
vmax = np.percentile(L_a, 99.9)

# --- figure ---
fig = plt.figure(figsize=(12, 8), layout="constrained")
gs = fig.add_gridspec(2, 2, width_ratios=[1, 0.022], height_ratios=[1, 1.7])
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[1, 0], sharex=ax1)
cax = fig.add_subplot(gs[1, 1])

ax1.plot(t_spl, spl, color="#2a78d6", lw=1.2)
ax1.set_ylabel(f"SPL ({unit} re 20 µPa)")
ax1.set_title(f"Sound pressure level ({weighting})")
ax1.text(1.0, 1.01, leq_txt, transform=ax1.transAxes, ha="right", va="bottom", fontsize=9, color="#52514e")
mark_events(ax1, t_on, t_off, where="bottom")
plt.setp(ax1.get_xticklabels(), visible=False)

mesh = ax2.pcolormesh(t_a, f_a, L_a, shading="nearest", cmap=SPEC_CMAP,
                      vmin=vmax - DB_RANGE, vmax=vmax, rasterized=True)
ax2.set_yscale("log" if LOG_FREQ_AXIS else "linear")
ax2.set_ylim(f_a[0], f_a[-1])
ax2.grid(False)
ax2.set_xlabel("Time from start of recording (s)")
ax2.set_ylabel("Frequency (Hz)")
ax2.set_title("Acoustic spectrogram")
ax2.set_xlim(mic.index[0], mic.index[-1])
mark_events(ax2, t_on, t_off, color="white")
fig.colorbar(mesh, cax=cax, label=f"SPL per {df_a:.1f} Hz band (dB re 20 µPa)")
save(fig, "1_spl_acoustic_spectrogram")
plt.show()
f_v, t_v, S_v = spectrogram(acc[["X", "Y", "Z"]].to_numpy(), fs_acc, ACC_SPEC_DF, scaling="density")
t_v = t_v + acc.index[0]
df_v = f_v[1] - f_v[0]
keep = f_v >= 2 * df_v                                             # skip the DC / gravity bins
f_v, S_v = f_v[keep], S_v[keep]
vmax = np.percentile(S_v, 99.9)

fig, ax = plt.subplots(figsize=(12, 5.5), layout="constrained")
mesh = ax.pcolormesh(t_v, f_v, S_v, shading="nearest", cmap=SPEC_CMAP,
                     norm=LogNorm(vmin=vmax / 10 ** (DB_RANGE / 10), vmax=vmax), rasterized=True)
ax.set_yscale("log" if LOG_FREQ_AXIS else "linear")
ax.set_ylim(f_v[0], f_v[-1])
ax.set_xlim(acc.index[0], acc.index[-1])
ax.grid(False)
ax.set_xlabel("Time from start of recording (s)")
ax.set_ylabel("Frequency (Hz)")
ax.set_title(f"Resultant acceleration spectrogram, 40 g accelerometer (Δf = {df_v:.2f} Hz)")
mark_events(ax, t_on, t_off, color="white")
fig.colorbar(mesh, ax=ax, pad=0.01, label="Resultant PSD (g²/Hz)")
save(fig, "2_accel_spectrogram")
plt.show()

if t_on is None:
    print("Fan-on time unknown - set FAN_ON_TIME in the settings cell and re-run this cell.")
else:
    t0, t1 = max(t_on - SHOCK_PRE, acc.index[0]), min(t_on + SHOCK_POST, acc.index[-1])
    win = acc.loc[t0:t1, ["X", "Y", "Z"]]
    pre = win.loc[: t_on]
    gravity = (pre if len(pre) >= 0.1 * fs_acc else win).mean()
    shock = win - gravity                                           # dynamic acceleration, g
    tw = shock.index.to_numpy()

    # SRS
    fmax = SRS_FMAX if SRS_FMAX is not None else fs_acc / 10
    n_f = int(np.floor(np.log2(fmax / SRS_FMIN) * SRS_BINS_PER_OCT)) + 1
    f_srs = SRS_FMIN * 2 ** (np.arange(n_f) / SRS_BINS_PER_OCT)
    srs_axes, srs_res = shock_response_spectrum(shock.to_numpy(), fs_acc, f_srs, SRS_DAMPING)

    # PSD of the same window
    nper = min(next_pow2(fs_acc / PSD_DF), prev_pow2(len(shock) // 4))
    f_psd, P = signal.welch(shock.to_numpy().T, fs=fs_acc, window="hann", nperseg=nper,
                            noverlap=nper // 2, detrend="constant")
    psd = pd.DataFrame(P.T, index=f_psd, columns=["X", "Y", "Z"])
    psd["Resultant"] = psd.sum(axis=1)
    psd = psd.iloc[1:]                                              # drop DC bin
    df_psd = f_psd[1] - f_psd[0]
    grms = np.sqrt(psd.sum() * df_psd)

    # --- figure ---
    fig = plt.figure(figsize=(12, 9), layout="constrained")
    gs = fig.add_gridspec(2, 2, height_ratios=[1, 1.35])
    ax_t = fig.add_subplot(gs[0, :])
    ax_s = fig.add_subplot(gs[1, 0])
    ax_p = fig.add_subplot(gs[1, 1])

    for c in ["X", "Y", "Z"]:
        ax_t.plot(tw, shock[c], color=AXIS_COLORS[c], lw=0.8, label=f"{c}  (peak {shock[c].abs().max():.3f} g)")
    mark_events(ax_t, t_on, None, on_side="left")
    ax_t.set_xlim(tw[0], tw[-1])
    ax_t.set_xlabel("Time from start of recording (s)")
    ax_t.set_ylabel("Acceleration (g)")
    ax_t.set_title("Fan start-up — 40 g acceleration, gravity removed")
    ax_t.legend(loc="lower right", bbox_to_anchor=(1, 1), ncol=3, borderaxespad=0)

    for i, c in enumerate(["X", "Y", "Z"]):
        ax_s.loglog(f_srs, srs_axes[:, i], color=AXIS_COLORS[c], lw=1.4, label=c)
    ax_s.loglog(f_srs, srs_res, color=AXIS_COLORS["Resultant"], lw=2, label="Resultant")
    i_pk = int(np.argmax(srs_res))
    ax_s.annotate(f"{srs_res[i_pk]:.3g} g @ {f_srs[i_pk]:.3g} Hz", (f_srs[i_pk], srs_res[i_pk]),
                  xytext=(0, 8), textcoords="offset points", ha="center", fontsize=9, color="#3a3936")
    ax_s.set_xlim(f_srs[0], f_srs[-1])
    ax_s.set_xlabel("Natural frequency (Hz)")
    ax_s.set_ylabel("Peak absolute acceleration (g)")
    ax_s.set_title(f"Shock response spectrum (Q = {1 / (2 * SRS_DAMPING):g})")
    ax_s.grid(True, which="both")
    ax_s.legend(loc="upper center", bbox_to_anchor=(0.5, -0.13), ncol=4)

    for c in ["X", "Y", "Z", "Resultant"]:
        ax_p.loglog(psd.index, psd[c], color=AXIS_COLORS[c], lw=2 if c == "Resultant" else 1.1,
                    label=f"{c}  ({grms[c]:.3g} gRMS)")
    ax_p.set_xlim(psd.index[0], psd.index[-1])
    ax_p.set_xlabel("Frequency (Hz)")
    ax_p.set_ylabel("PSD (g²/Hz)")
    ax_p.set_title(f"PSD of the start-up window (Δf = {df_psd:.2f} Hz)")
    ax_p.grid(True, which="both")
    ax_p.legend(loc="upper center", bbox_to_anchor=(0.5, -0.13), ncol=2)

    print(f"Window {t0:.2f}-{t1:.2f} s  |  gravity removed: " +
          ", ".join(f"{c} {gravity[c]:+.3f} g" for c in gravity.index))
    save(fig, "3_startup_srs_psd")
    plt.show()
```

## FAQs - How to Capture Field Shock and Vibration Data

#### What Sample Rate do you Need for Field Vibration Measurements?

Your required sample rate depends on the highest frequency content you want to capture. The Nyquist theorem says you must sample at a minimum of twice the highest frequency of interest. In practice, sampling at 5 to 10 times that frequency gives cleaner results.

For most transit vibration applications, a sample rate of 5,000 to 10,000 Hz covers the relevant content. Shock events from drops or pyrotechnic sources can contain energy above 10 kHz, so rates of 20,000 Hz or higher are appropriate. When in doubt, it is generally safer to sample faster and downsample later in software.

#### How Does Transit Vibration Differ from Laboratory Shaker Testing?

Transit data is often more complex than what a laboratory shaker will provide in a test. Real transit will have lots of shock events from changing terrain and bump in the road. These changes can be difficult to predict, and every unique combination will be slightly different, as a result, lab testing should be designed to test the worst-case scenarios to cover as many bases as possible. You can build your own [vibration test standard from field data](https://blog.endaq.com/how-to-develop-a-vibration-test-standard-psd-from-experimental-data), we have a dedicated walkthrough, but it is important to add a factor of safety.

#### Can I use a vibration data logger without a computer in the field?

Yes. data loggers are meant operate as standalone devices. You configure the logger before deployment, press the button to start recording (or set a trigger), and retrieve the data later. Nothing is needed beyond the logger and the equipment or tools needed to mount it.

#### Do I need to recalibrate my sensor before a field deployment?

If your sensor has been exposed to severe shocks or extended high-temperature environments, generally what the sensor is rated for, recalibration before the next campaign is a good practice. Loggers often come with recalibration schedules on a yearly or other time frame. 

#### How do I know if my mounting is good enough?

The first, and easiest check to do is to make sure that your sensor is not easily moved from its mounting point by hand. Once you feel confident that its not moving too much, an easy test is is to hit the surrounding area, near the sensor with a hammer, your hand or another object, depending on how delicate the mounting surface is, and then look at the FFT of the impact. If the FFT shows low frequency peaks that are fairly wide rather than pointy, that's an indication that there is a lack of rigidity in the setup, and that usually comes from you mounting method.

## Conclusion

Capturing field shock and vibration data is pretty easy to do with a data logger, and it can save you plenty of time when looking to get a representative shock and vibration profile for lab testing. All you need to do, to get good data, is configure your recording device to the situation at hand, mount it properly, then analyze the data you collect.

[View full post](https://blog.endaq.com/how-to-capture-field-shock-and-vibration-data-in-7-steps)

```json
{
  "@context" : "http://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Giovanni Fava"
  },
  "dateModified" : "2026-11-03T19:54:23.797Z",
  "datePublished" : "2026-11-03T19:54:03Z",
  "headline" : "How to Capture Field Shock and Vibration Data in 7 Steps",
  "image" : {
    "@type" : "ImageObject",
    "height" : 60,
    "url" : "/hs/hsstatic/content_shared_assets/static-1.4092/img/default-amp-logo.png",
    "width" : 60
  },
  "mainEntityOfPage" : "https://blog.endaq.com/how-to-capture-field-shock-and-vibration-data-in-7-steps",
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "height" : 60.0,
      "url" : "https://cdn2.hubspot.net/hubfs/637862/endaq-blog-logo-1.svg",
      "width" : 60.000004
    },
    "name" : "enDAQ Blog for Data Sensing and Analyzing"
  }
}
```