← [Documentation index](README.md)

# Human Vibration — Whole-Body and Hand-Arm Exposure

Vibration transmitted to a person is evaluated with the same measurement chain
whatever its origin: the acceleration is **frequency-weighted** to reflect how
the body responds at each frequency, reduced to a **weighted r.m.s.**
acceleration (with dose measures for shocks and long records), combined across
axes into a **vibration total value**, and finally normalised to an **8-hour
daily exposure** `A(8)` that is compared against the action and limit values of
the European directive.

The weightings themselves are defined once, in **ISO 8041-1:2017**, as a cascade
of analog filters; **ISO 2631-1** applies them to whole-body vibration,
**ISO 2631-2** to vibration in buildings, **ISO 2631-4** to rail ride comfort,
and **ISO 5349-1/-2** to hand-transmitted vibration. This page covers the whole
chain.

<picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/diagram_human_vibration_dark.svg"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/diagram_human_vibration.svg" alt="Whole-body vibration measurement chain: a triaxial accelerometer at the seat/body interface of a seated person measures the x, y and z acceleration; each axis is band-limited and frequency-weighted (Wk vertical, Wd horizontal) per ISO 8041-1, reduced to a weighted r.m.s. a_w and VDV per ISO 2631-1, combined into the vibration total value a_v, normalised to the daily exposure A(8) and assessed against the EAV and ELV of Directive 2002/44/EC" width="94%"></picture>

## 1. Frequency weightings (ISO 8041-1)

Every human-vibration weighting is the product of four analog stages evaluated
at $s = j\,2\pi f$ (ISO 8041-1 Formulae (1)–(5)): a second-order Butterworth
**high-pass** and **low-pass** band limiting, an **acceleration–velocity
transition** carrying the overall gain $K$, and an **upward step**:

$$
H(s) = H_h(s)\,H_l(s)\,H_t(s)\,H_s(s).
$$

A single Table 3 parameter set $(f_1, Q_1, f_2, Q_2, f_3, f_4, Q_4, f_5, Q_5,
f_6, Q_6, K)$ realises all nine weightings — `Wb, Wc, Wd, We, Wf, Wh, Wj, Wk,
Wm` — a corner set to infinity collapsing its stage to unity. The principal
whole-body weighting is `Wk` (vertical, seat surface); `Wd` is the horizontal
weighting, and `Wh` the hand-arm weighting.

```python
import phonometry as ph

# The overall weighting response at any frequencies (ISO 8041-1 Formula (5)).
resp = ph.frequency_weighting("Wk", [1.0, 6.3096, 20.0])
print(resp.magnitude.round(3))      # [0.482 1.054 0.636]  (factors)
print(resp.magnitude_db.round(2))   # [-6.33  0.46 -3.93]  (dB)
```

The factors reproduce the ISO 8041-1 Annex B design-goal tables to their four
significant figures: `Wk` plateaus near −6 dB below 2 Hz, peaks at +0.46 dB near
6.3 Hz and rolls off above.

<picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/vibration_weighting_dark.svg"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/vibration_weighting.svg" alt="The whole-body vertical weighting Wk in decibels over 0.4 to 100 Hz: a plateau near -6 dB below 2 Hz, a small +0.5 dB peak near 6 Hz and a roll-off to about -21 dB at 100 Hz" width="88%"></picture>

<details>
<summary>Show the code for this figure</summary>

```python
import numpy as np
import matplotlib.pyplot as plt
import phonometry as ph

result = ph.frequency_weighting("Wk", np.geomspace(0.4, 100.0, 240))

# One line:
result.plot()
plt.show()

# By hand, from the result's fields, mirroring what WeightingResponse.plot() draws:
fig, ax = plt.subplots()
ax.semilogx(result.frequencies, result.magnitude_db, color="#1f77b4")
ax.set_xlabel("Frequency [Hz]")
ax.set_ylabel("Weighting factor [dB]")
ax.set_title("Whole-body vertical weighting Wk (ISO 8041-1)")
plt.show()
```

</details>

To weight a time signal, `apply_weighting` applies the exact complex response in
the frequency domain (so magnitude *and* phase match the standard), which the
time-domain dose metrics below then consume.

## 2. Weighted acceleration and dose measures (ISO 2631-1)

The basic evaluation is the **weighted r.m.s. acceleration**. From a
one-third-octave spectrum it is (ISO 2631-1 Eq. (9); the identical construction
gives the hand-arm $a_{hw}$ of ISO 5349-1 Eq. (A.1)):

$$
a_w = \sqrt{\sum_i \left(W_i\,a_i\right)^2},
$$

with $W_i$ the weighting factor at band centre $i$ and $a_i$ the measured band
acceleration.

```python
import numpy as np
import phonometry as ph

# A measured vertical seat spectrum (r.m.s. per one-third octave, m/s^2).
freqs = np.array([1.0, 2.0, 4.0, 8.0, 16.0, 31.5, 63.0])
accel = np.array([0.20, 0.45, 0.42, 0.25, 0.12, 0.05, 0.02])

result = ph.weighted_acceleration(accel, freqs, "Wk")
print(round(result.overall, 3))          # 0.555  m/s^2 (a_w)
print(result.weighted.round(3))          # W_i * a_i per band
```

<picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/weighted_acceleration_dark.svg"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/weighted_acceleration.svg" alt="A measured vehicle-seat acceleration spectrum (grey) and its Wk-weighted contribution (blue) over the one-third octaves from 1 to 80 Hz: the weighting attenuates the low and high bands but leaves the 4 to 8 Hz range nearly unchanged, giving a weighted r.m.s. a_w of about 1.03 m/s^2" width="90%"></picture>

<details>
<summary>Show the code for this figure</summary>

```python
import numpy as np
import matplotlib.pyplot as plt
import phonometry as ph

freqs = np.array([1.0, 1.25, 1.6, 2.0, 2.5, 3.15, 4.0, 5.0, 6.3, 8.0, 10.0,
                  12.5, 16.0, 20.0, 25.0, 31.5, 40.0, 63.0, 80.0])
accel = np.array([0.18, 0.24, 0.33, 0.46, 0.52, 0.55, 0.48, 0.39, 0.31, 0.26,
                  0.21, 0.17, 0.13, 0.10, 0.078, 0.060, 0.045, 0.028, 0.020])
result = ph.weighted_acceleration(accel, freqs, "Wk")

# One line:
result.plot()
plt.show()

# By hand, mirroring what WeightedSpectrum.plot() draws:
pos = np.arange(freqs.size)
fig, ax = plt.subplots()
ax.bar(pos - 0.2, result.band_accelerations, 0.4, color="#bbbbbb",
       label="Unweighted $a_i$")
ax.bar(pos + 0.2, result.weighted, 0.4, color="#1f77b4",
       label="Weighted $W_i a_i$ (Wk)")
ax.set_xticks(pos)
ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right")
ax.set_xlabel("Frequency [Hz]")
ax.set_ylabel(r"r.m.s. acceleration [m/s$^2$]")
ax.set_title(f"Weighted acceleration ($a_w$ = {result.overall:.3f} m/s²)")
ax.legend()
plt.show()
```

</details>

When the r.m.s. value understates an intermittent or shock-laden exposure,
ISO 2631-1 adds dose measures computed on the weighted time signal: the
**running r.m.s.** and its maximum, the **maximum transient vibration value**
`MTVV` (Eq. (4), a 1 s running r.m.s.); the fourth-power **vibration dose
value** $\text{VDV} = \left(\int a_w^4\,dt\right)^{1/4}$ (Eq. (5)); the **motion
sickness dose value** $\text{MSDV} = \left(\int a_w^2\,dt\right)^{1/2}$; and the
**crest factor** (peak / r.m.s.), whose value above 9 signals that the basic
method is inadequate.

```python
import numpy as np
import phonometry as ph

fs = 1000.0
raw = np.random.default_rng(0).standard_normal(int(60 * fs))   # 60 s record
a_w = ph.apply_weighting(raw, fs, "Wk")                        # weighted signal

print(round(ph.vibration_dose_value(a_w, fs), 3))   # 0.744  VDV [m/s^1.75]
print(round(ph.mtvv(a_w, fs), 3))                   # 0.265  MTVV [m/s^2]
print(round(ph.crest_factor(a_w), 2))               # 3.74   crest factor
```

## 3. Vibration total value and daily exposure `A(8)`

Across the three axes the **vibration total value** combines the axis-weighted
r.m.s. accelerations with the posture multiplying factors $k_j$ (ISO 2631-1
Eq. (10); for hand-arm, ISO 5349-1 Eq. (1) with every $k = 1$):

$$
a_v = \sqrt{\sum_j k_j^2\,a_{wj}^2}.
$$

```python
import phonometry as ph

# Health, seated: k = 1.4 / 1.4 / 1.0 (ISO 2631-1, 7.2.3).
a_v = ph.vibration_total_value([0.35, 0.28, 0.62], k=[1.4, 1.4, 1.0])
print(round(a_v, 3))     # 0.882  m/s^2
```

The **daily exposure** normalises the total value to a reference 8-hour day
($T_0 = 28\,800$ s). For a single operation $A(8) = a_v\,\sqrt{T/T_0}$; several
operations combine through their partial exposures $A_i(8) = a_{vi}\,\sqrt{T_i/T_0}$
as $A(8) = \sqrt{\sum_i A_i(8)^2}$ (ISO 5349-1 Eqs. (2)/(3); ISO 5349-2
Eqs. (1)–(3)).

`daily_vibration_exposure` builds the partial exposures, combines them and
assesses the result against **Directive 2002/44/EC** — hand-arm action value
`A(8) = 2.5` and limit value `5` m/s²; whole-body action `0.5` and limit `1.15`
m/s² (or a VDV of `9.1` / `21` m/s¹·⁷⁵):

```python
import phonometry as ph

# ISO 5349-2 Annex E.3: a forestry worker's three chain-saw tasks.
result = ph.daily_vibration_exposure(
    total_values=[4.6, 6.0, 3.6],                 # a_hv per task, m/s^2
    durations_s=[2 * 3600, 1 * 3600, 2 * 3600],   # exposure time per task
    kind="hav",
    labels=["brush-saw", "felling", "stripping"],
)
print(result.partials.round(2))            # [2.3  2.12 1.8 ]  A_i(8)
print(round(result.a8, 2))                 # 3.61  m/s^2
print(result.assessment.zone)             # 'action'  (2.5 <= A(8) < 5.0)
```

<picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/daily_vibration_exposure_dark.svg"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/daily_vibration_exposure.svg" alt="A bar chart of the three partial hand-arm exposures (about 2.3, 2.1 and 1.8 m/s^2) and the combined A(8) of 3.61 m/s^2, with the Directive 2002/44/EC exposure action value at 2.5 and exposure limit value at 5.0 m/s^2 marked as horizontal lines; the daily exposure sits in the action zone between them" width="82%"></picture>

<details>
<summary>Show the code for this figure</summary>

```python
import numpy as np
import matplotlib.pyplot as plt
import phonometry as ph

result = ph.daily_vibration_exposure(
    [4.6, 6.0, 3.6], [2 * 3600, 1 * 3600, 2 * 3600], kind="hav",
    labels=["brush-saw", "felling", "stripping"],
)

# One line:
result.plot()
plt.show()

# By hand, mirroring what DailyVibrationExposure.plot() draws:
labels = [*result.labels, "A(8)"]
values = [*result.partials.tolist(), result.a8]
a = result.assessment
fig, ax = plt.subplots()
ax.bar(range(len(values)), values,
       color=["#bbbbbb"] * result.partials.size + ["#1f77b4"])
ax.axhline(a.action_value, color="#2ca02c", ls="--", label=f"EAV = {a.action_value:g}")
ax.axhline(a.limit_value, color="#d62728", ls="--", label=f"ELV = {a.limit_value:g}")
ax.set_xticks(range(len(values)))
ax.set_xticklabels(labels, rotation=30, ha="right")
ax.set_ylabel(r"Daily exposure A(8) [m/s$^2$]")
ax.legend()
plt.show()
```

</details>

## 4. Exposure–response guidance

For hand-transmitted vibration, ISO 5349-1 Annex C relates the daily exposure to
the group-mean lifetime $D_y$ (in years) that produces vibration-white-finger in
10 % of an exposed group, $D_y = 31.8\,A(8)^{-1.06}$ (Eq. (C.1)):

```python
import phonometry as ph

print(round(ph.hav_vwf_lifetime_years(7.0), 1))   # 4.0 years (Table C.1)
```

The standards deliberately define no safe limit — `A(8)` and the directive's
action and limit values are the basis for any exposure criterion. For whole-body
exposure, `energy_equivalent_acceleration` gives the ISO 2631-1 Eq. (B.3)
energy-equivalent magnitude across periods of different magnitude and duration.

---

**Standards.** ISO 8041-1:2017 (weighting definitions and tolerances);
ISO 2631-1:1997 (whole-body evaluation); ISO 2631-2:2003 (buildings, `Wm`);
ISO 2631-4:2001 (rail ride comfort, `Wb`); ISO 5349-1:2001 and ISO 5349-2:2001
(hand-transmitted vibration); Directive 2002/44/EC (exposure action and limit
values).
