<!-- canonical: https://jmrplens.github.io/phonometry/reference/api/noise_control/duct-path/ -->
Source: https://jmrplens.github.io/phonometry/reference/api/noise_control/duct-path/

End-to-end duct-borne noise calculation: fan to room, element by element.

The classic consulting workflow of HVAC acoustics is a bookkeeping exercise.
Start from the sound power the fan puts into the duct, walk down the path, and
at every element subtract what it attenuates and add back what its own airflow
regenerates. What arrives at the terminal device is converted into a sound
pressure level by the room effect, the supply and return paths are combined,
and the result is compared with the room criterion. If it fails, the sheet
itself says which element to change.

This module implements that cascade as a result object. It follows the
worked sheet of Long, *Architectural Acoustics* 2nd ed., Table 14.9 (a
5000 cfm forward-curved fan feeding a room through a supply and a return path,
checked against NC 30) and the same row structure as the industry procedure of
AHRI Standard 885, *Procedure for Estimating Occupied Space Sound Levels in the
Application of Air Terminals and Air Outlets*, Table 8: one row per physical
element, octave bands across the columns, regenerated noise as its own row
combined logarithmically rather than subtracted, and a cumulative received-level
row at the foot.

Three arithmetic conventions matter and are worth stating once:

* **Attenuations are positive.** Every element model in
  [`phonometry.noise_control.hvac`](/phonometry/reference/api/noise_control/hvac/) returns a loss as a positive number of
  decibels, and the cascade subtracts it. Published worksheets print the same
  quantity as a negative level change; [`DuctPathResult.table`](/phonometry/reference/api/noise_control/duct-path/#ductpathresulttable) follows the
  worksheet sign so the printed sheet reads like the reference.
* **Regenerated noise adds on a power basis.** The self-noise of an element is
  a sound power level in its own right and is combined with the level arriving
  at that point as $10 \log_{10}(10^{L/10} + 10^{L_\mathrm{sn}/10})$.
* **There is a self-noise floor.** Long's worked sheet uses a 0 dB self-noise
  sound power level as the default and whenever a calculated level would be
  negative, which is why his received spectrum bottoms out near 0 dB rather
  than running to minus infinity. `self_noise_floor` reproduces that and can
  be switched off with `None`.

The elements themselves come from [`phonometry.noise_control.hvac`](/phonometry/reference/api/noise_control/hvac/) (fan
sound power, straight lined and unlined ducts, elbows, flexible duct, branch
splits, end reflection, silencer self-noise, plenums and the room effect) and
from manufacturer data, which is what a real calculation uses for silencers and
air terminal devices. [`phonometry.noise_control.duct_modes`](/phonometry/reference/api/noise_control/duct-modes/) supplies the
cut-on frequency above which the plane-wave elements stop being valid, and the
cascade raises a
[`PlaneWaveWarning`](/phonometry/reference/api/noise_control/duct-modes/#planewavewarning) when a duct
section is declared and the analysis runs past it.

> Auto-generated from the source docstrings by `scripts/generate_api_docs.py` (`make api-docs`). Do not edit by hand.

## combine_duct_paths

```python
combine_duct_paths(
    paths: list[DuctPathResult] | tuple[DuctPathResult, ...],
    *,
    criterion: str | None = None,
    target: float | None = None,
    label: str = 'Combined paths',
) -> DuctPathResult
```

Combine the received spectra of several duct paths into one result.

A room is usually fed by more than one path (Long's worked sheet combines a
supply and a return path before comparing with NC 30). The received levels
add on a power basis, and the combination is rated against the same design
criterion.

**Parameters**

| Name | Description |
| :--- | :--- |
| `paths` | Two or more [`DuctPathResult`](/phonometry/reference/api/noise_control/duct-path/#ductpathresult) objects sharing the same analysis bands. |
| `criterion` | Room-criterion family; `None` (default) takes it from the first path. |
| `target` | Design criterion value; `None` (default) takes it from the first path that declares one. |
| `label` | A short human label of the combination. |

**Returns:** A [`DuctPathResult`](/phonometry/reference/api/noise_control/duct-path/#ductpathresult) whose `contributions` hold the per-path received spectra and whose `stages` are empty.

**Raises**

| Exception | When |
| :--- | :--- |
| ValueError | If fewer than one path is given or the bands differ. |

## duct_path

```python
duct_path(
    frequencies: ArrayLike,
    source_level: ArrayLike,
    elements: list[DuctElement] | tuple[DuctElement, ...],
    *,
    room_effect: ArrayLike | None = None,
    source_label: str = 'Fan',
    criterion: str = 'NC',
    target: float | None = None,
    self_noise_floor: float | None = 0.0,
    section: dict[str, float] | None = None,
    flow_velocity: float = 0.0,
    speed_of_sound: float = 343.0,
    label: str = 'Duct path',
) -> DuctPathResult
```

Cascade a duct path from the source sound power to the received level.

Walks the elements in order, subtracting each attenuation and combining
each regenerated sound power level on a power basis, then applies the room
effect and rates the result against the design criterion. This is the
calculation of Long Table 14.9 and of AHRI 885 Table 8.

**Parameters**

| Name | Description |
| :--- | :--- |
| `frequencies` | Octave-band centre frequencies, Hz (1-D array). The published sheets use 63 Hz to 8 kHz. |
| `source_level` | Sound power level entering the path, dB re 1 pW; e.g. from [`phonometry.noise_control.hvac.fan_sound_power`](/phonometry/reference/api/noise_control/hvac/#fan_sound_power). |
| `elements` | The path elements in order, as [`DuctElement`](/phonometry/reference/api/noise_control/duct-path/#ductelement) entries. Their `attenuation` and `self_noise` accept a scalar, a per-band array or an [`HvacSpectrumResult`](/phonometry/reference/api/noise_control/hvac/#hvacspectrumresult). |
| `room_effect` | The room effect as a positive attenuation, dB (from [`phonometry.noise_control.hvac.room_effect`](/phonometry/reference/api/noise_control/hvac/#room_effect)). `None` leaves the received spectrum as a sound power level at the terminal device. |
| `source_label` | Description of the source for the printed sheet. |
| `criterion` | Room-criterion family, `"NC"` (default) or `"RC"`. |
| `target` | The design criterion value (e.g. `30`), or `None`. |
| `self_noise_floor` | Default regenerated sound power level, dB re 1 pW, applied where an element declares none and as a floor under every element's self-noise. Long's worked sheet uses 0 dB; pass `None` to disable the floor entirely. |
| `section` | Optional duct cross section for the plane-wave validity check, as `{"diameter": d}`, `{"width": a, "height": b}` or `{"area": s}`, in metres. When given, a [`PlaneWaveWarning`](/phonometry/reference/api/noise_control/duct-modes/#planewavewarning) is raised for analysis frequencies above the first cut-on. |
| `flow_velocity` | Mean axial flow speed in that section, m/s, used for the cut-on Mach correction. |
| `speed_of_sound` | Speed of sound, m/s, for the same check. |
| `label` | A short human label of the path. |

**Returns:** A [`DuctPathResult`](/phonometry/reference/api/noise_control/duct-path/#ductpathresult).

**Raises**

| Exception | When |
| :--- | :--- |
| ValueError | If the spectra do not share one value per band or the criterion family is unknown. |
| TypeError | If an entry of `elements` is not a [`DuctElement`](/phonometry/reference/api/noise_control/duct-path/#ductelement). |

## DuctElement

```python
DuctElement(
    label: str,
    attenuation: Any = None,
    self_noise: Any = None,
    code: str = '',
)
```

One element of a duct path: what it attenuates and what it regenerates.

**Attributes**

| Name | Description |
| :--- | :--- |
| `label` | Human description of the element, e.g. `"Silencer, 3 ft, standard pressure drop"`. |
| `attenuation` | Octave-band attenuation as a **positive** loss, dB. A scalar is broadcast over the bands; an [`HvacSpectrumResult`](/phonometry/reference/api/noise_control/hvac/#hvacspectrumresult) (or anything exposing `values`) is accepted directly. `None` means no attenuation. |
| `self_noise` | Octave-band regenerated sound power level of the element, dB re 1 pW, or `None` when the element regenerates nothing (the cascade then applies its self-noise floor). |
| `code` | A short worksheet code for the printed sheet (AHRI 885 style, e.g. `"D1"`, `"S"`); defaults to the row number. |

## DuctPathResult

```python
DuctPathResult(
    frequencies: np.ndarray,
    source_level: np.ndarray,
    source_label: str,
    stages: tuple[DuctPathStage, ...],
    room_effect: np.ndarray | None,
    received_level: np.ndarray,
    criterion: str,
    target: float | None,
    label: str,
    contributions: tuple[tuple[str, np.ndarray], ...] = ...,
)
```

The end-to-end duct-borne noise calculation of one path (or of a sum).

Built by [`duct_path`](/phonometry/reference/api/noise_control/duct-path/#duct_path) for a single fan-to-room path, and by
[`combine_duct_paths`](/phonometry/reference/api/noise_control/duct-path/#combine_duct_paths) for the logarithmic sum of several such paths
(whose `stages` are empty and whose `contributions` carry one entry per
path).

**Attributes**

| Name | Description |
| :--- | :--- |
| `frequencies` | Octave-band centre frequencies, Hz. |
| `source_level` | Sound power level entering the path, dB re 1 pW. |
| `source_label` | Description of the source, e.g. the fan. |
| `stages` | One [`DuctPathStage`](/phonometry/reference/api/noise_control/duct-path/#ductpathstage) per element, in path order. |
| `room_effect` | The room effect applied after the last element, as a positive attenuation in dB, or `None` when the path was not taken into a room (`received_level` is then still a sound power level). |
| `received_level` | The spectrum at the receiver: a sound pressure level in dB when `room_effect` was applied, otherwise the sound power level leaving the last element. |
| `criterion` | `"NC"` or `"RC"`, the room-criterion family used. |
| `target` | The design criterion value (e.g. `30` for NC 30), or `None` when no target was declared. |
| `contributions` | `(label, received_level)` per contributing path for a combination; empty for a single path. |
| `label` | A short human label of the path. |

### DuctPathResult.criterion_curve

*property*

The design criterion curve at `frequencies`, dB, or `None`.

The NC or RC curve of `target`, sampled at the analysis bands, so
it can be compared band by band with `received_level`.

### DuctPathResult.exceedance

*property*

Band-by-band excess of the received level over the criterion, dB.

Positive where the design criterion is exceeded. `None` when no
target was declared.

### DuctPathResult.meets_target

*property*

`True` when no band exceeds the design criterion curve.

`None` when no target was declared. This is the band-by-band test a
design sheet applies, not the NC *rating* of `rating`, which the
standard derives by its own two-step procedure.

### DuctPathResult.plot()

```python
DuctPathResult.plot(
    ax: Axes | None = None,
    *,
    language: str = 'en',
    **kwargs: Any,
) -> Axes
```

Plot the spectrum cascading down the path against the criterion curve.

One line per element shows where the spectrum stands after that
element, from the source at the top to the received level at the
bottom, with the design criterion curve overlaid. Requires matplotlib
(`pip install phonometry[plot]`).

**Parameters**

| Name | Description |
| :--- | :--- |
| `ax` | Existing axes, or `None` to create a figure. |
| `language` | Label language, `"en"` (default) or `"es"`. |
| `kwargs` | Forwarded to the received-level `Axes.plot`. |

**Returns:** The axes.

### DuctPathResult.rating

*property*

The room-criterion rating of `received_level`.

An [`NCResult`](/phonometry/reference/api/rooms/noise-criteria/#ncresult) when
`criterion` is `"NC"`, otherwise an
[`RCResult`](/phonometry/reference/api/rooms/noise-criteria/#rcresult) (ANSI/ASA S12.2-2019).

### DuctPathResult.report()

```python
DuctPathResult.report(
    path: str,
    *,
    metadata: ReportMetadata | None = None,
    engine: str = 'reportlab',
    verbose: bool = False,
    language: str = 'en',
) -> str
```

Render the duct-borne noise calculation sheet to a PDF at `path`.

Writes a one-page duct-path sheet in the layout of the published
procedures (AHRI Standard 885 Table 8; Long Table 14.9): the
method-basis line, an optional metadata header (client, system, room,
instrumentation, climate, date), the element table with the octave
bands across the columns and one row per element (attenuations printed
with the worksheet's negative sign, regenerated noise shaded as its own
row, the running level after each element and the room effect and
received spectrum at the foot), the cascade chart against the criterion
curve, the boxed room-criterion rating, the verdict against the design
criterion, and a method-basis strip.

**Parameters**

| Name | Description |
| :--- | :--- |
| `path` | Destination path of the PDF file. |
| `metadata` | Optional [`ReportMetadata`](/phonometry/reference/api/building/insulation/#reportmetadata) supplying the header (`client`, `specimen` the air system, `test_room` the served room, `instrumentation`, `temperature`, `relative_humidity`, `pressure`, `test_date`) and the footer identity. The design criterion comes from the result's own `target`; a `requirement` in the metadata overrides it. |
| `engine` | Rendering back end; only `"reportlab"` is supported. |
| `verbose` | When `True` the table adds the running level after every element; when `False` only the source, the element attenuations, the regenerated-noise rows and the received spectrum are printed, which keeps a long path on one page. |
| `language` | Fiche language: `"en"` (default) or `"es"`. |

**Returns:** The written `path` as a `str`.

**Raises**

| Exception | When |
| :--- | :--- |
| ValueError | If `engine` is not `"reportlab"` or `language` is unknown. |
| ImportError | If reportlab (or, for the figure, matplotlib) is not installed (`pip install phonometry[report]`). |

### DuctPathResult.table()

```python
DuctPathResult.table() -> list[dict[str, Any]]
```

The per-element sheet, one entry per printed row.

The rows follow the layout of a published duct-borne calculation sheet
(Long Table 14.9; AHRI 885 Table 8): the source, then for each element
its attenuation, the *Sum* after subtracting it, its regenerated noise
and the *Combined* level, then the room effect and the received
spectrum. Each entry has `code`, `label`, `kind` (one of
`"source"`, `"attenuation"`, `"sum"`, `"self_noise"`,
`"level"`, `"contribution"`, `"room_effect"`, `"received"`,
`"criterion"`) and `values` (a per-band array). An attenuation row
carries the worksheet sign, i.e. the **negative** of the positive loss
the models return; a `"contribution"` row is the received spectrum of
one path of a combination.

**Returns:** The list of row dictionaries, in printing order.

## DuctPathStage

```python
DuctPathStage(
    label: str,
    code: str,
    attenuation: np.ndarray,
    attenuated: np.ndarray,
    self_noise: np.ndarray,
    level: np.ndarray,
)
```

The computed rows of one element of a duct path.

The four spectra are exactly the four printed rows of a duct-borne
calculation sheet: what the element takes out, what the level becomes,
what the element puts back and where the path stands afterwards.

**Attributes**

| Name | Description |
| :--- | :--- |
| `label` | Human description of the element. |
| `code` | The worksheet code of the row. |
| `attenuation` | Attenuation of the element as a positive loss, dB. |
| `attenuated` | Level after subtracting the attenuation (the *Sum* row), dB. |
| `self_noise` | Regenerated sound power level of the element, dB re 1 pW, after the self-noise floor has been applied. |
| `level` | Level leaving the element, the energy sum of `attenuated` and `self_noise` (the *Combined* row), dB. |
