← [Documentation index](README.md)

# Rotorcraft noise: the hemisphere method (ECAC Doc 32 / NORAH2)

Helicopter noise is strongly directive, so the ECAC Doc 32 method describes the
source with a **noise hemisphere**: one-third-octave-band sound pressure levels on
a spherical grid of azimuth `φ` and polar angle `θ`, measured at a fixed **60 m**
reference distance under ICAO reference atmospheric conditions. Placing that
source at a receiver adds the propagation adjustment
`ΔLp = ΔLs + ΔLa + ΔLg (+ ΔLd)`.

This page covers the source and propagation primitives, the flight-condition
interpolation across a hemisphere database, the flight-path kinematics, and the
single-event integration to `SEL`/`LASmax`/`EPNL` and ground-grid contours;
terrain shielding follows in later work.

## 1. The noise hemisphere

A `RotorcraftHemisphere` holds the band levels on the azimuth/polar grid (with
missing bins marked `NaN`). `hemisphere_source_level` reads the source level at an
arbitrary emission direction: the grid is first gap-filled by nearest-bin
constant-value extrapolation (Eq. 14/15, equally-near bins energy-averaged;
computed once and cached), then the lookup is bilinear in the energy domain over
the four neighbouring bins (Doc 32 Eq. 13), so partially-measured cells stay
continuous with their measured corners.

```python
import numpy as np
from phonometry import aircraft

# A hemisphere: band levels of shape (azimuth, polar, frequency) at 60 m.
h = aircraft.RotorcraftHemisphere(frequencies=freqs, azimuth=phi, polar=theta, levels=levels)
h.plot()                                   # fore-aft directivity (needs matplotlib)
lv = aircraft.hemisphere_source_level(h, 0.0, 90.0)   # level per band abeam-below
```

## 2. Propagation adjustments

The hemisphere level at 60 m is carried to the receiver by three adjustments
(Doc 32 §A.4):

- **`spherical_spreading_adjustment(r)`**: `ΔLs = −20·log10(r/60)` (Eq. 24).
- **`atmospheric_adjustment(freqs, r)`**: `ΔLa = −α(f)·(r − 60)` with the
  ISO 9613-1 pure-tone coefficient at the exact band centre (Eq. 26/27), reusing
  the library's `air_attenuation`; this is what the NORAH2 reference
  implementation computes. The guidance's alternative SAE (Rickley) band mapping,
  tabulated in its Table 4, coincides below 3.15 kHz and deviates by up to
  2.2 dB/km at 8-10 kHz.
- **`ground_effect_adjustment(freqs, hs, hr, dp, flow_resistivity=…)`**: the
  interference between the direct and ground-reflected rays over an impedance
  plane (Chien-Soroka, Eq. 28-35), with the Delany-Bazley one-parameter impedance
  and the CNOSSOS flow-resistivity classes `"A"`-`"H"`.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/rotorcraft_ground_effect_dark.svg">
  <img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/rotorcraft_ground_effect.svg" alt="Rotorcraft ground-effect adjustment versus one-third-octave frequency for hard (asphalt) and soft (grass) ground: low-frequency reinforcement, a deep destructive interference dip, and the incoherent +3 dB high-frequency region, the dip deeper over hard ground" width="82%">
</picture>

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

```python
import matplotlib.pyplot as plt
import numpy as np
from phonometry import aircraft

freqs = 1000.0 * 10.0 ** (np.arange(-13, 11) / 10.0)   # 50 Hz-10 kHz thirds
hs, hr, dp = 150.0, 1.5, 500.0                          # overflight geometry
grass = aircraft.ground_effect_adjustment(freqs, hs, hr, dp, flow_resistivity="D")
asphalt = aircraft.ground_effect_adjustment(freqs, hs, hr, dp, flow_resistivity="G")

fig, ax = plt.subplots()
ax.axhline(0.0, color="0.5", linewidth=1.0)
ax.semilogx(freqs, asphalt, marker="o", markersize=3,
            label="Hard (asphalt/concrete, class G)")
ax.semilogx(freqs, grass, marker="s", markersize=3,
            label="Soft (grass/pasture, class D)")
ax.set(xlabel="One-third-octave-band centre frequency [Hz]",
       ylabel="Ground-effect adjustment ΔLg [dB]",
       title="Rotorcraft ground effect (ECAC Doc 32, Chien-Soroka)")
ax.grid(True, which="both", alpha=0.3)
ax.legend()
plt.show()
```

</details>

```python
import numpy as np
from phonometry import aircraft

freqs = 1000.0 * 10.0 ** (np.arange(-13, 11) / 10.0)   # 50 Hz-10 kHz thirds
r = 500.0
received = (lv
            + aircraft.spherical_spreading_adjustment(r)
            + aircraft.atmospheric_adjustment(freqs, r)
            + aircraft.ground_effect_adjustment(freqs, 150.0, 1.5, 500.0, flow_resistivity="D"))
```

The standard database is recorded at 60 m, the default. If a hemisphere uses a
different polar distance (`h.distance`, e.g. 70 m hover rings), pass it to both
distance-dependent adjustments:
`spherical_spreading_adjustment(r, reference_distance=h.distance)` and
`atmospheric_adjustment(freqs, r, reference_distance=h.distance)`.

## 3. Flight conditions: interpolating between hemispheres

A database records one hemisphere per flight condition (airspeed `V`, path
angle `γ`): approaches at several descent angles, level flyovers at several
speeds, take-off climbs. Real flight conditions rarely coincide with a measured
one, so the NORAH2 guidance interpolates (Eq. 3-10): both axes are normalised
by their database spans (with the empirical scaling factor `Ffc = 2` on the
path angle), a Delaunay triangulation covers the normalised conditions, and a
query inside the convex hull blends the three hemispheres of its enveloping
triangle with inverse-distance weights in the energy domain. Outside the hull
the nearest condition is adopted unblended, which is also the behaviour ECAC
Doc 32, 1st ed. prescribes for its whole envelope (it defines no interpolation
yet).

`flight_condition_weights` returns the `(index, weight)` pairs;
`interpolated_source_level` applies them to the hemisphere lookups:

```python
import numpy as np
from phonometry import aircraft

# One hemisphere per measured flight condition (speeds in kt, angles in deg).
speeds = [50.0, 70.0, 60.0]
angles = [0.0, 0.0, 10.0]
weights = aircraft.flight_condition_weights(speeds, angles, 60.0, 2.5)
lv = aircraft.interpolated_source_level(
    [h_50_level, h_70_level, h_60_climb], speeds, angles,
    60.0, 2.5, 0.0, 90.0)               # blended level per band, abeam-below
```

The airspeed, not the ground speed, selects the hemisphere. The weights are
unit-invariant (knots or m/s, as long as the query matches the database), and a
database lookup triangulation can be passed as `triangles` (the NORAH database
ships one per type; its shipped tables triangulate the raw `(V, γ)` plane, so
passing them reproduces the reference implementation bin for bin). Types whose
rotor configuration is mirrored with respect to their class reference substitute
`h.mirrored()` (Eq. 2, `φ → −φ`), and certification-level offsets enter as
`level_offset`.

## 4. Flight-path kinematics

`flight_path_kinematics` derives, from a time-stamped track by central finite
differences, everything the event needs (Eq. 16-21 / Doc 32 Eq. 8-10): ground
speed `Vg`, airspeed `VA` (zero wind), heading `Θ = atan2(ΔX, ΔY)`, curvature
`K = ΔΘ/ΔS`, bank angle `Φ = atan(K·Vg²/g)` and path angle `γ = atan(ΔZ/ΔS)`.
The guidance recommends smoothing radar tracks (e.g. spline resampling to a
0.5 s cadence) before differentiating.

```python
kin = aircraft.flight_path_kinematics(times, positions)   # positions (N, 3), m
kin.plot()                                # speed and angle profiles
kin.airspeed, kin.path_angle              # select the hemisphere per point
kin.bank_angle                            # tilts the hemisphere in turns
```

## 5. The single event: `SEL`, `LASmax` and `EPNL`

`rotorcraft_event_level` runs the whole chain for one flyover at one receiver.
Per track point, the flight condition selects (or blends) the hemispheres and
the emission angles address the source level; the hemisphere frame is oriented
by the heading and tilted by the bank angle in turns (pitch attitude is
implicit in the hemispheres). The received one-third-octave history is
expressed at recorded time `tr = te + r/c` (Eq. 22, `c = 346.1 m/s`) and
integrated: `LASmax`, `SEL` over the full history and over the certification
10 dB-down window (Doc 32 Eq. 27), and `EPNL` per ICAO Annex 16 (Doc 32
Eq. 28), reusing the library's `epnl_from_pnlt`.

```python
res = aircraft.rotorcraft_event_level(
    hemispheres, speeds, angles,          # the database
    times, positions,                     # the track (m, z up)
    receiver=(120.0, 0.0),                # ground position of the microphone
    flow_resistivity="D")                 # grass site
res.la_max, res.sel, res.epnl             # LASmax, SEL, EPNL
res.plot()                                # the LA(t) time history
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/rotorcraft_flyover_event_dark.svg">
  <img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/rotorcraft_flyover_event.svg" alt="A-weighted level versus recorded time for a level rotorcraft flyover: a smooth rise to LASmax over the 10 dB-down window and a slower decay, annotated with the SEL and EPNL of the event" width="82%">
</picture>

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

```python
import matplotlib.pyplot as plt
import numpy as np
from phonometry import aircraft

# A synthetic helicopter-like hemisphere on the standard 31-band, 10° grid.
freqs = 1000.0 * 10.0 ** (np.arange(-20, 11) / 10.0)   # 10 Hz-10 kHz thirds
az = np.arange(-90.0, 91.0, 10.0)
po = np.arange(0.0, 181.0, 10.0)
spectrum = 88.0 - 12.0 * np.log10(freqs / 100.0) ** 2   # broad low-mid hump
levels = (spectrum[None, None, :] - 0.045 * np.abs(po - 80.0)[None, :, None]
          - 0.02 * np.abs(az)[:, None, None])
h = aircraft.RotorcraftHemisphere(freqs, az, po, levels)

speed = 30.87                                           # 60 kt, in m/s
t = np.arange(0.0, 130.01, 0.5)
track = np.column_stack([np.zeros_like(t), speed * (t - 65.0),
                         np.full_like(t, 150.0)])
event = aircraft.rotorcraft_event_level(
    [h], [speed], [0.0], t, track, (120.0, 0.0), flow_resistivity="D")
event.plot()
plt.show()
```

</details>

Radar-track workflows can hand the smoothed per-point `airspeed`, `path_angle`,
`heading` and `bank_angle` directly instead of deriving them from the
positions; when they are derived, the track is in metres and seconds, so the
database airspeeds must then be in m/s.

## 6. Ground-grid contours

`rotorcraft_noise_contour` evaluates the same event over a whole grid in one
vectorised pass per emission step and reduces each receiver's history to the
`SEL` (`metric="exposure"`) or `LASmax` (`metric="maximum"`) footprint:

```python
res = aircraft.rotorcraft_noise_contour(
    hemispheres, speeds, angles, times, positions,
    x=np.linspace(-2000.0, 2000.0, 81),
    y=np.linspace(-3000.0, 3000.0, 121),
    metric="exposure", flow_resistivity="D")
res.plot()                                # filled SEL contours
```

The ground may vary across the receivers without a full elevation model:
`flow_resistivity` and `ground_elevation` accept one value per grid point
(shape `(len(y), len(x))`), and each receiver's two-ray model then uses its
local values.

## 7. Terrain: the mean ground plane and screening

Doc 32, 1st ed., assumes flat terrain; its guidance adds the machinery for
real sites. A varying vertical section is represented by its **mean ground
plane** (Eq. 36-40), the least-squares line through the terrain polyline
computed in closed form; source and receiver enter the flat-ground equations
with their **equivalent heights**, measured orthogonally to that plane and
floored at 0.1 m. Ground that changes type along the path averages its flow
resistivity by the logarithm, weighted by segment length (Eq. 41).

When terrain blocks the line of sight, the sound follows the shortest convex
path over it (the guidance's rubber band) and every touched vertex is a
**diffraction edge**. The attenuation combines the pure diffraction of the
path difference `δ` (Eq. 42-44, `10·Ch·log10(3 + (40/λ)·C″·δ)`, capped at
25 dB) with the source-side and receiver-side ground effects, each over its
own mean ground plane and weighted by its image-path diffraction (Eq. 45-47,
the CNOSSOS-EU scheme the guidance adopts). The ground effect is not
evaluated separately in that regime.

`mean_ground_plane`, `mean_flow_resistivity` and `diffraction_attenuation`
expose the pieces; `terrain_screening_adjustment` runs the whole section:

```python
import numpy as np
from phonometry import aircraft

d = [0.0, 150.0, 260.0, 300.0, 340.0, 420.0, 600.0]     # section distances
z = [0.0, 4.0, 48.0, 62.0, 40.0, 8.0, 2.0]              # terrain heights
freqs = 1000.0 * 10.0 ** (np.arange(-13, 11) / 10.0)
res = aircraft.terrain_screening_adjustment(
    freqs, source=(0.0, 90.0), receiver=(600.0, 3.2), distances=d, heights=z,
    flow_resistivity="D")
res.screened, res.path_difference      # True, the rubber-band delta
res.adjustment                         # per band, replaces the flat-ground ΔLg
res.plot()                             # the section geometry
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/rotorcraft_terrain_screening_dark.svg">
  <img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/rotorcraft_terrain_screening.svg" alt="Terrain screening section with a helicopter source, a hill blocking the line of sight to the microphone and the diffracted path over its crest, above the per-band ground-and-screening adjustment compared with the flat-ground comb" width="82%">
</picture>

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

```python
import matplotlib.pyplot as plt
import numpy as np
from phonometry import aircraft

freqs = 1000.0 * 10.0 ** (np.arange(-13, 11) / 10.0)   # 50 Hz-10 kHz thirds
d = np.array([0.0, 150.0, 260.0, 300.0, 340.0, 420.0, 600.0])
z = np.array([0.0, 4.0, 48.0, 62.0, 40.0, 8.0, 2.0])
res = aircraft.terrain_screening_adjustment(
    freqs, (0.0, 90.0), (600.0, 3.2), d, z, flow_resistivity="D")
flat = aircraft.ground_effect_adjustment(freqs, 90.0, 1.2, 600.0,
                                         flow_resistivity="D")

fig, (ax, ax2) = plt.subplots(2, 1, figsize=(9, 7))
res.plot(ax=ax)
ax2.axhline(0.0, color="0.5", linewidth=1.0)
ax2.semilogx(freqs, flat, ls="--", marker="s", markersize=3,
             label="Flat ground (no hill)")
ax2.semilogx(freqs, res.adjustment, marker="o", markersize=3,
             label="Screened by the hill (Eq. 45-47)")
ax2.set(xlabel="One-third-octave-band centre frequency [Hz]",
        ylabel="Ground and screening adjustment [dB]")
ax2.grid(True, which="both", alpha=0.3)
ax2.legend()
plt.show()
```

</details>

The event and contour run over real sites by passing a digital elevation
model: `terrain=(x, y, z)` on the track frame. Every emission-receiver pair
then samples its own vertical section at `terrain_resolution` (default: the
model's cell size) and evaluates it with the machinery above; the receiver
ground comes from the model. The cost grows with track points times grid
points, so keep contour grids modest with terrain.

```python
res = aircraft.rotorcraft_event_level(
    hemispheres, speeds, angles, times, positions, receiver=(1200.0, 300.0),
    terrain=(tx, ty, tz), flow_resistivity="D")
```

## Validation

Validated against the NORAH2 guidance Table 4 (all 31 bands, 10 Hz-10 kHz), the
closed-form inverse-square spreading, the analytic rigid-ground `+6 dB` and
grazing limits of the ground effect, off-node bilinear lookups on the reference
hemispheres of all eleven rotorcraft types, hand-checked interpolation
simplices, closed-form kinematics and the Lorentzian `SEL − LASmax` flyover
integral, and end to end against the NORAH2 prototype's ARP verification cases:
emission angles reproduce to 0.01°, retarded times to 0.02 s, every step level
of the hard-ground events to 0.08 dB(A) out to 18 km, `LASmax` to 0.03 dB and
`SEL` to 0.05 dB over hard ground (0.4 dB over soft ground), the 187-microphone
contour grid to 0.7 dB worst-case, `PNLTM` to 0.1 dB and `EPNL` to about
1.3 dB (the prototype's sub-noy-floor perceived-noisiness policy differs from
the published Annex 16 law). One documented
divergence remains: at far range over soft ground the prototype damps the
coherent two-ray interference of guidance Eq. 30 towards the incoherent sum
(up to 4.9 dB on individual low-level steps beyond 7 km); neither Doc 32 nor
the guidance contains such a term, and this implementation follows the
published equations.

The terrain machinery is anchored in closed form: the mean ground plane is
exact on linear and symmetric profiles, a flat section reproduces the
flat-ground model to machine precision and an inclined plane its analytic
rotation, the log-mean resistivity recovers the geometric mean, the grazing
diffraction gives the classical `10·log10(3)`, and a hand-checked hill fixes
the rubber-band path difference. Per-receiver ground handling validates end
to end against the prototype's ARP Case 3 (187 microphones, each on its own
ground elevation: every step level to 0.08 dB(A), `SEL`/`LASmax` to 0.05 dB,
the contour grid to 0.15 dB) and the mixed-ground Case 2 grid reproduces in
a single per-receiver-resistivity call. The prototype's public release does
not include a reconstructible screening case (the frame of its terrain model
could not be pinned to the published outputs), so the diffraction chain
itself rests on the closed-form anchors and its CNOSSOS-EU lineage.

## References

- European Civil Aviation Conference. (2026). *Report on standard method of
  computing rotorcraft noise contours* (ECAC.CEAC Doc 32, 1st ed.).
  [ECAC documents page](https://www.ecac-ceac.org/documents/ecac-documents-and-international-agreements),
  [free PDF](https://www.ecac-ceac.org/images/documents/ECAC-CEAC-DOC_32-REPORT_ON_STANDARD_METHOD_OF_COMPUTING_ROTORCRAFT_NOISE_CONTOURS.pdf).
  The standard rotorcraft contour method whose hemisphere source model and
  propagation adjustments this page implements.
- Olsen, H., Tuinstra, M., & van Oosten, N. (2024). *Rotorcraft noise
  modelling guidance* (Research Project NOISE SC01, deliverable D1.5d,
  contract EASA.2020.FC.06). European Union Aviation Safety Agency.
  [EASA project page](https://www.easa.europa.eu/en/research-projects/environmental-research-rotorcraft-noise),
  [free PDF](https://www.easa.europa.eu/en/downloads/132005/en).
  The equation-level guidance (Eq. 13-35) behind the implementation, with the
  Table 4 attenuation values and the reference hemispheres used as oracles.
- Chien, C. F., & Soroka, W. W. (1975). Sound propagation along an impedance
  plane. *Journal of Sound and Vibration*, 43(1), 9-20.
  [doi:10.1016/0022-460X(75)90200-X](https://doi.org/10.1016/0022-460X(75)90200-X).
  The two-ray interference solution over an impedance plane behind the
  section 2 ground-effect adjustment.
- Delany, M. E., & Bazley, E. N. (1970). Acoustical properties of fibrous
  absorbent materials. *Applied Acoustics*, 3(2), 105-116.
  [doi:10.1016/0003-682X(70)90031-9](https://doi.org/10.1016/0003-682X(70)90031-9).
  The one-parameter flow-resistivity impedance model the ground effect
  evaluates.
- Kephalopoulos, S., Paviotti, M., & Anfosso-Lédée, F. (2012). *Common noise
  assessment methods in Europe (CNOSSOS-EU)* (EUR 25379 EN). Publications
  Office of the European Union.
  [doi:10.2788/31776](https://doi.org/10.2788/31776),
  [JRC repository](https://publications.jrc.ec.europa.eu/repository/handle/JRC72550).
  The flow-resistivity ground classes `"A"`-`"H"` accepted by
  `ground_effect_adjustment`.

## Standards

ECAC Doc 32, 1st ed., *Report on Standard Method of Computing
Rotorcraft Noise Contours*; NORAH2 rotorcraft-noise modelling guidance
(EASA.2020.FC.06 SC01.D1.5d), §A.3-A.5: the noise hemisphere (§A.3.2), spherical
spreading (Eq. 24), atmospheric attenuation (Eq. 26/27, ISO 9613-1 coefficient,
Table 4), ground effect (Chien-Soroka, Eq. 28-35, Delany-Bazley impedance,
CNOSSOS flow resistivity), flight-condition interpolation (Eq. 3-10),
flight-path kinematics (Eq. 16-21 / Doc 32 Eq. 8-10), recorded time (Eq. 22),
the single-event metrics `SEL`/`LASmax` and `EPNL` (Doc 32 Eq. 27/28, ICAO
Annex 16 App. 2), the mean ground plane and equivalent heights (Eq. 36-40),
the log-mean flow resistivity (Eq. 41) and the terrain screening chain
(Eq. 42-47 with the guidance's noise-path appendices; CNOSSOS-EU lineage).
Hover, idle and taxi source handling (guidance §A.3.5) remains outside the
implementation.
