diff --git a/CHANGELOG.md b/CHANGELOG.md index cdc342d8..84d38f96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/api-reference.md b/docs/api-reference.md index 57bf7922..4cb8ed7c 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -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: @@ -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 @@ -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) | diff --git a/llms-full.txt b/llms-full.txt index 56db3ab5..839c1c04 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -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: @@ -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 @@ -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) | diff --git a/site/src/content/docs/reference/api/noise_control/enclosures.md b/site/src/content/docs/reference/api/noise_control/enclosures.md index 986ce7e9..0f1c8b5b 100644 --- a/site/src/content/docs/reference/api/noise_control/enclosures.md +++ b/site/src/content/docs/reference/api/noise_control/enclosures.md @@ -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, @@ -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). | diff --git a/src/phonometry/noise_control/enclosures.py b/src/phonometry/noise_control/enclosures.py index 574926e3..4f9d1a47 100644 --- a/src/phonometry/noise_control/enclosures.py +++ b/src/phonometry/noise_control/enclosures.py @@ -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 @@ -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: @@ -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, @@ -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. @@ -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) diff --git a/tests/noise_control/test_enclosures.py b/tests/noise_control/test_enclosures.py index 439abaa3..4fd67556 100644 --- a/tests/noise_control/test_enclosures.py +++ b/tests/noise_control/test_enclosures.py @@ -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,