Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

### Added

- `enclosure_insertion_loss` now accepts a panel prediction result directly for
its `panel_transmission_loss` argument, in addition to a per-band array or a
callable. A `SoundReductionResult` or `ApertureTransmissionResult` (from the
panel and aperture models) is matched structurally, so its per-band `R` and
band centres feed the enclosure calculation without any dependency of
`noise_control` on `building`. The predicted panel `R` and a measured `R`
are therefore interchangeable at the enclosure input.
- Atmospheric refraction: ray tracing and the parabolic equation
(`phonometry.environmental.atmospheric_refraction`, also re-exported at the
top level). The module is the refracting-atmosphere counterpart of the ocean
Expand Down
9 changes: 5 additions & 4 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# API Reference

All core functionality lives in thirteen domain subpackages; every public name
All core functionality lives in fifteen domain subpackages; every public name
is also re-exported by the top-level `phonometry` package.

> **Note.** This page is the curated quick table for the GitHub/PyPI audience:
Expand All @@ -13,7 +13,7 @@ is also re-exported by the top-level `phonometry` package.

## Namespaces

The library is organized into thirteen domain subpackages, and importing the
The library is organized into fifteen domain subpackages, and importing the
domain namespace is the primary form used throughout the documentation:

```python
Expand All @@ -32,10 +32,11 @@ contour = aircraft.noise_contour(path, powers, distances, sel, lmax, x=gx, y=gy)
| `phonometry.materials` | Absorption (ISO 354/11654), impedance tube, airflow resistance, scattering/diffusion, road absorption, dynamic stiffness |
| `phonometry.building` | Sound insulation measurement and prediction (EN 12354, ISO 717/16283/10140/15186/10052/10848), structure-borne sound |
| `phonometry.vibration` | Human vibration (ISO 2631/5349/8041), multiple shocks, mobility (ISO 7626), transfer stiffness (ISO 10846) |
| `phonometry.environmental` | Rating levels, ISO 1996-2 measurement, outdoor propagation (ISO 9613), wind-turbine noise, impulsive prominence |
| `phonometry.environmental` | Rating levels, ISO 1996-2 measurement, outdoor propagation (ISO 9613), atmospheric refraction (ray tracing and the parabolic equation), wind-turbine noise, impulsive prominence |
| `phonometry.aircraft` | EPNL (ICAO Annex 16), SAE ARP 5534 absorption, airport contours (ECAC Doc 29), rotorcraft (ECAC Doc 32) |
| `phonometry.underwater` | ISO 18405/17208/18406 levels, propagation, sound speed, sonar equation, seabed, ambient and ship-traffic noise, numerical solvers |
| `phonometry.electroacoustics` | Distortion (IEC 60268-3 / AES17), transfer function and coherence |
| `phonometry.electroacoustics` | Distortion (IEC 60268-3 / AES17), transfer function and coherence, radiating piston |
| `phonometry.noise_control` | Reactive silencers (four-pole method), HVAC duct attenuation, machine-enclosure insertion loss |
| `phonometry.broadcast` | Programme loudness and true peak (ITU-R BS.1770-5, EBU R 128 with Tech 3341/3342) |
| `phonometry.simulation` | 2D acoustic FDTD wave simulation (staggered grid, sources, probes, impedance boundaries, obstacles) |

Expand Down
9 changes: 5 additions & 4 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9892,7 +9892,7 @@ vectorization only batches the computation across the channel axis.

# API Reference

All core functionality lives in thirteen domain subpackages; every public name
All core functionality lives in fifteen domain subpackages; every public name
is also re-exported by the top-level `phonometry` package.

> **Note.** This page is the curated quick table for the GitHub/PyPI audience:
Expand All @@ -9903,7 +9903,7 @@ is also re-exported by the top-level `phonometry` package.

## Namespaces

The library is organized into thirteen domain subpackages, and importing the
The library is organized into fifteen domain subpackages, and importing the
domain namespace is the primary form used throughout the documentation:

```python
Expand All @@ -9922,10 +9922,11 @@ contour = aircraft.noise_contour(path, powers, distances, sel, lmax, x=gx, y=gy)
| `phonometry.materials` | Absorption (ISO 354/11654), impedance tube, airflow resistance, scattering/diffusion, road absorption, dynamic stiffness |
| `phonometry.building` | Sound insulation measurement and prediction (EN 12354, ISO 717/16283/10140/15186/10052/10848), structure-borne sound |
| `phonometry.vibration` | Human vibration (ISO 2631/5349/8041), multiple shocks, mobility (ISO 7626), transfer stiffness (ISO 10846) |
| `phonometry.environmental` | Rating levels, ISO 1996-2 measurement, outdoor propagation (ISO 9613), wind-turbine noise, impulsive prominence |
| `phonometry.environmental` | Rating levels, ISO 1996-2 measurement, outdoor propagation (ISO 9613), atmospheric refraction (ray tracing and the parabolic equation), wind-turbine noise, impulsive prominence |
| `phonometry.aircraft` | EPNL (ICAO Annex 16), SAE ARP 5534 absorption, airport contours (ECAC Doc 29), rotorcraft (ECAC Doc 32) |
| `phonometry.underwater` | ISO 18405/17208/18406 levels, propagation, sound speed, sonar equation, seabed, ambient and ship-traffic noise, numerical solvers |
| `phonometry.electroacoustics` | Distortion (IEC 60268-3 / AES17), transfer function and coherence |
| `phonometry.electroacoustics` | Distortion (IEC 60268-3 / AES17), transfer function and coherence, radiating piston |
| `phonometry.noise_control` | Reactive silencers (four-pole method), HVAC duct attenuation, machine-enclosure insertion loss |
| `phonometry.broadcast` | Programme loudness and true peak (ITU-R BS.1770-5, EBU R 128 with Tech 3341/3342) |
| `phonometry.simulation` | 2D acoustic FDTD wave simulation (staggered grid, sources, probes, impedance boundaries, obstacles) |

Expand Down
14 changes: 8 additions & 6 deletions site/src/content/docs/reference/api/noise_control/enclosures.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,18 @@ Bies terms this net reduction the enclosure *noise reduction*; it is the
insertion loss of the enclosure.

**The panel transmission loss `R` is supplied by the caller** -- measured, or
predicted by a panel model -- as a per-band array or a callable of frequency.
This module never predicts `R` itself; it combines a given `R` with the
interior absorption. The interior room constant reuses
[`phonometry.room.room_constant`](/phonometry/reference/api/rooms/steady-field/#room_constant).
predicted by a panel model -- as a per-band array, a callable of frequency, or
a panel prediction result (a [`phonometry.building.SoundReductionResult`](/phonometry/reference/api/building/panel-transmission/#soundreductionresult)
or [`phonometry.building.ApertureTransmissionResult`](/phonometry/reference/api/building/aperture-transmission/#aperturetransmissionresult), matched structurally
so no dependency on `building` is introduced). This module never predicts
`R` itself; it combines a given `R` with the interior absorption. The
interior room constant reuses [`phonometry.room.room_constant`](/phonometry/reference/api/rooms/steady-field/#room_constant).

## enclosure_insertion_loss

```python
enclosure_insertion_loss(
panel_transmission_loss: ArrayLike | Callable[[NDArray[np.float64]], ArrayLike],
panel_transmission_loss: ArrayLike | Callable[[NDArray[np.float64]], ArrayLike] | PanelTransmissionResult,
external_area: float,
internal_area: float,
internal_absorption: ArrayLike,
Expand All @@ -58,7 +60,7 @@ constant `R_i = S_i alpha_i / (1 - alpha_i)`.

| Name | Description |
| :--- | :--- |
| `panel_transmission_loss` | Panel transmission loss `R` per band, dB. Either a per-band array (measured or predicted elsewhere) or a callable mapping a frequency array to per-band `R` (then `frequencies` is required). This function does not predict `R`. |
| `panel_transmission_loss` | Panel transmission loss `R` per band, dB. One of: a per-band array (measured); a callable mapping a frequency array to per-band `R` (then `frequencies` is required); or a panel prediction result carrying `transmission_loss` and `frequencies`, such as the [`SoundReductionResult`](/phonometry/reference/api/building/panel-transmission/#soundreductionresult) of [`phonometry.single_panel_transmission_loss`](/phonometry/reference/api/building/panel-transmission/#single_panel_transmission_loss) / [`phonometry.double_wall_transmission_loss`](/phonometry/reference/api/building/panel-transmission/#double_wall_transmission_loss) or the [`ApertureTransmissionResult`](/phonometry/reference/api/building/aperture-transmission/#aperturetransmissionresult) of [`phonometry.composite_transmission_loss`](/phonometry/reference/api/building/aperture-transmission/#composite_transmission_loss) (its `frequencies` are then used unless *frequencies* is given). This function does not predict `R` itself. |
| `external_area` | External enclosure surface area `S_E`, m2. |
| `internal_area` | Internal surface area `S_i` (including the machine), m2. |
| `internal_absorption` | Mean interior absorption `alpha_i` in `(0, 1)` (scalar or per-band). |
Expand Down
60 changes: 51 additions & 9 deletions src/phonometry/noise_control/enclosures.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,19 @@
insertion loss of the enclosure.

**The panel transmission loss ``R`` is supplied by the caller** -- measured, or
predicted by a panel model -- as a per-band array or a callable of frequency.
This module never predicts ``R`` itself; it combines a given ``R`` with the
interior absorption. The interior room constant reuses
:func:`phonometry.room.room_constant`.
predicted by a panel model -- as a per-band array, a callable of frequency, or
a panel prediction result (a :class:`phonometry.building.SoundReductionResult`
or :class:`phonometry.building.ApertureTransmissionResult`, matched structurally
so no dependency on ``building`` is introduced). This module never predicts
``R`` itself; it combines a given ``R`` with the interior absorption. The
interior room constant reuses :func:`phonometry.room.room_constant`.
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Protocol, cast

import numpy as np
from numpy.typing import ArrayLike, NDArray
Expand Down Expand Up @@ -80,6 +82,22 @@ def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes":
return plot_enclosure(self, ax=ax, **kwargs)


class PanelTransmissionResult(Protocol):
"""A panel prediction result exposing a per-band transmission loss.

A frozen result such as :class:`phonometry.building.SoundReductionResult`
or :class:`phonometry.building.ApertureTransmissionResult` carries the
predicted transmission loss in ``transmission_loss`` and its band centres
in ``frequencies``. Matching structurally (a :class:`typing.Protocol`)
lets :func:`enclosure_insertion_loss` accept such a result directly without
importing the ``building`` package, so ``noise_control`` keeps no
dependency on it.
"""

transmission_loss: NDArray[np.float64]
frequencies: NDArray[np.float64]


def _resolve_frequencies(
frequencies: ArrayLike | None,
) -> NDArray[np.float64] | None:
Expand Down Expand Up @@ -116,7 +134,11 @@ def _resolve_panel_r(


def enclosure_insertion_loss(
panel_transmission_loss: ArrayLike | Callable[[NDArray[np.float64]], ArrayLike],
panel_transmission_loss: (
ArrayLike
| Callable[[NDArray[np.float64]], ArrayLike]
| PanelTransmissionResult
),
external_area: float,
internal_area: float,
internal_absorption: ArrayLike,
Expand All @@ -129,9 +151,16 @@ def enclosure_insertion_loss(
constant ``R_i = S_i alpha_i / (1 - alpha_i)``.

:param panel_transmission_loss: Panel transmission loss ``R`` per band, dB.
Either a per-band array (measured or predicted elsewhere) or a callable
mapping a frequency array to per-band ``R`` (then ``frequencies`` is
required). This function does not predict ``R``.
One of: a per-band array (measured); a callable mapping a frequency
array to per-band ``R`` (then ``frequencies`` is required); or a panel
prediction result carrying ``transmission_loss`` and ``frequencies``,
such as the :class:`~phonometry.building.SoundReductionResult` of
:func:`phonometry.single_panel_transmission_loss` /
:func:`phonometry.double_wall_transmission_loss` or the
:class:`~phonometry.building.ApertureTransmissionResult` of
:func:`phonometry.composite_transmission_loss` (its ``frequencies`` are
then used unless *frequencies* is given). This function does not
predict ``R`` itself.
:param external_area: External enclosure surface area ``S_E``, m2.
:param internal_area: Internal surface area ``S_i`` (including the machine),
m2.
Expand All @@ -145,6 +174,19 @@ def enclosure_insertion_loss(
s_e = require_positive(external_area, "external_area")
s_i = require_positive(internal_area, "internal_area")

if (
not callable(panel_transmission_loss)
and not isinstance(panel_transmission_loss, (np.ndarray, list, tuple))
and hasattr(panel_transmission_loss, "transmission_loss")
and hasattr(panel_transmission_loss, "frequencies")
):
result = cast("PanelTransmissionResult", panel_transmission_loss)
if frequencies is None:
frequencies = result.frequencies
panel_transmission_loss = np.asarray(
result.transmission_loss, dtype=np.float64
)

freqs = _resolve_frequencies(frequencies)
r = _resolve_panel_r(panel_transmission_loss, freqs)

Expand Down
28 changes: 28 additions & 0 deletions tests/noise_control/test_enclosures.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,34 @@ def test_callable_requires_frequencies() -> None:
enclosure_insertion_loss(lambda f: f, 6.0, 5.0, 0.3)


def test_panel_result_bridge() -> None:
# A predicted SoundReductionResult can be fed straight in; its per-band R
# and its frequencies are read from the result, matching an explicit pass.
from phonometry import single_panel_transmission_loss

freqs = np.array([125.0, 250.0, 500.0, 1000.0, 2000.0])
panel = single_panel_transmission_loss(freqs, 15.0, critical_frequency=2000.0)

via_result = enclosure_insertion_loss(panel, 6.0, 5.0, 0.3)
via_arrays = enclosure_insertion_loss(
panel.transmission_loss, 6.0, 5.0, 0.3, frequencies=freqs
)
assert np.allclose(via_result.panel_transmission_loss, panel.transmission_loss)
assert np.allclose(via_result.frequencies, freqs)
assert np.allclose(via_result.insertion_loss, via_arrays.insertion_loss)


def test_panel_result_frequencies_override() -> None:
# An explicit 'frequencies' wins over the ones carried by the result.
from phonometry import single_panel_transmission_loss

freqs = np.array([125.0, 250.0, 500.0])
panel = single_panel_transmission_loss(freqs, 15.0, critical_frequency=2000.0)
other = np.array([100.0, 200.0, 400.0])
res = enclosure_insertion_loss(panel, 6.0, 5.0, 0.3, frequencies=other)
assert np.allclose(res.frequencies, other)


def test_result_type_and_plot() -> None:
res = enclosure_insertion_loss(
[20.0, 30.0, 40.0], 6.0, 5.0, 0.3,
Expand Down
Loading