[READ-ONLY] Mirror of https://github.com/jmrplens/PyOctaveBand. [Python3] Octave-Band and Fractional Octave-Band filter. For signal in time domain. jmrplens.github.io/PyOctaveBand/
acoustics audio filter frequency frequency-analysis frequency-domain octave python3 signal time-domain
0

Configure Feed

Select the types of activity you want to include in your feed.

Impedance tube: complete circular, rectangular and square cross-section support (#380)

* Impedance tube: complete circular, rectangular and square cross-section support

The ISO 10534-2 branch already distinguished circular from rectangular
tubes; the ASTM E2611 branch ignored the cross-section entirely. Both
branches now cover it end to end:

- Accept "square" as a cross-section alias (the rectangular bound with
d the side length) in plane_wave_frequency_range() and
two_microphone_impedance().
- New plane_wave_frequency_range_astm(): ASTM E2611-19 working range
with the unrounded circular constant (fu d < 0.586 c, 6.2.4.1), the
rectangular bound (0.500 c, 6.2.5), the spacing bound (fu s < 0.40 c,
6.5.4) and the 1 % of wavelength lower limit (6.2.3).
- wave_decomposition(), transfer_matrix_two_load() and
transfer_matrix_one_load() accept diameter/shape and emit an
ImpedanceTubeWarning outside the plane-wave range (wavenumber-space
check, no speed of sound needed).
- New hydraulic_diameter(width, height) helper (4A/P) for the
tube_attenuation_constant() rectangular case (ISO 10534-2 A.2.1.5).
- ImpedanceTubeResult retains spacing/x1/diameter/shape and
TransferMatrix retains l1/s1/l2/s2/thickness/diameter/shape plus the
frequency vector and air characteristic impedance, so
TransferMatrix.plot() now works without arguments on solver-built
matrices.
- ReportMetadata.tube_shape prints a translated cross-section row on
the ISO 10534-2 fiche.
- Tests for every new path; materials guide (EN/ES), api-reference and
generated API pages updated.

* Address review: field order, spacing guard, plot docstring, message constant

- Append ReportMetadata.tube_shape after the original fields so the
public positional constructor order is unchanged.
- Validate s1/s2 inside _warn_astm_plane_wave so a zero spacing raises
ValueError instead of ZeroDivisionError from the two transfer-matrix
solvers.
- Reword TransferMatrix.plot() docstring: solver-built matrices retain
frequency and rho c, only hand-built ones must pass them.
- Hoist the duplicated diameter validation message into a module
constant (SonarCloud S1192).

authored by

José M. Requena Plens and committed by
GitHub
(Jul 27, 2026, 6:21 AM +0200) 84fcdeb0 23b74608

+737 -63
+8 -6
docs/api-reference.md
··· 703 703 | `PrecisionFieldIndicators` | `dataclass` | **Annex B indicators (per band).**<br>• `ft`: temporal variability (= F1) or None<br>• `f_pi_unsigned`: F_pIn unsigned (= F2)<br>• `f_pi_signed`: F_pIn signed (= F3), ≥ unsigned<br>• `fs`: field non-uniformity FS (= F4) | `fi.f_pi_signed, fi.fs` | 704 704 | `precision_qualification` | `function` | **The five ISO 9614-3 Annex C acceptance criteria.**<br>• `indicators`: `PrecisionFieldIndicators`<br>• `scan_intensity_level_1`/`_2`: LIn per scan [dB] (criterion 1)<br>• `pressure_residual_index`: δpI0 [dB] (criterion 2, K = 10)<br>• `field_nonuniformity_1`/`_2`: FS per scan density (criterion 5)<br>• `frequencies` [Hz] or `repeatability_limit`: Table 1 limit s | `q = precision_qualification(fi, pressure_residual_index=18.0)`<br><br>• `PrecisionCriteria` | 705 705 | `PrecisionCriteria` | `dataclass` | **Annex C pass/fail per band.**<br>• `criterion_1`…`criterion_5`: bool arrays or None where not evaluable<br>• `qualified`: conjunction of criteria 1–4 or None | `q.qualified, q.criterion_2` | 706 - | `plane_wave_frequency_range` | `function` | **Working plane-wave range (f_l, f_u) (ISO 10534-2 §4.2–4.5).**<br>• `spacing` s [m]<br>• `speed_of_sound` c0 [m/s]<br>• `diameter` d [m] (Default: None → spacing bound only)<br>• `shape`: 'circular'/'rectangular' (Default: 'circular') | `plane_wave_frequency_range(0.05, 343.2, diameter=0.1) # (343.2, 1990.6)`<br><br>• [Hz] | 706 + | `plane_wave_frequency_range` | `function` | **Working plane-wave range (f_l, f_u) (ISO 10534-2 §4.2–4.5).**<br>• `spacing` s [m]<br>• `speed_of_sound` c0 [m/s]<br>• `diameter` d [m] (Default: None → spacing bound only)<br>• `shape`: 'circular'/'rectangular'/'square' (Default: 'circular') | `plane_wave_frequency_range(0.05, 343.2, diameter=0.1) # (343.2, 1990.6)`<br><br>• [Hz] | 707 + | `plane_wave_frequency_range_astm` | `function` | **Working plane-wave range (f_l, f_u) (ASTM E2611-19 6.2.3–6.2.5, 6.5.4).**<br>• `spacing` s [m]<br>• `speed_of_sound` c [m/s]<br>• `diameter` d [m] (Default: None → spacing bound only)<br>• `shape`: 'circular'/'rectangular'/'square' (Default: 'circular') | `plane_wave_frequency_range_astm(0.05, 343.2, diameter=0.1) # (68.6, 2011.2)`<br><br>• [Hz]; f_u s < 0.40 c, f_u d < 0.586 c (0.500 rectangular) | 707 708 | `speed_of_sound_iso` | `function` | **Speed of sound (ISO 10534-2 Eq. 5).**<br>• `temperature` T [K] | `speed_of_sound_iso(293.0) # 343.2`<br><br>• c0 = 343.2 √(T/293) [m/s] | 708 709 | `air_density_iso` | `function` | **Air density (ISO 10534-2 Eq. 7).**<br>• `temperature` T [K]<br>• `atmospheric_pressure` [kPa] (Default: 101.325) | `air_density_iso(293.0) # 1.186`<br><br>• ρ [kg/m³] | 709 710 | `speed_of_sound_astm` | `function` | **Speed of sound (ASTM E2611-19 Eq. 4).**<br>• `temperature` T [°C] | `speed_of_sound_astm(20.0) # 343.2`<br><br>• 20.047 √(273.15 + T) [m/s] | 710 711 | `air_density_astm` | `function` | **Air density (ASTM E2611-19 Eq. 5).**<br>• `temperature` T [°C]<br>• `atmospheric_pressure` P [kPa] (Default: 101.325) | `air_density_astm(20.0) # 1.202`<br><br>• ρ [kg/m³] | 711 712 | `characteristic_impedance` | `function` | **Characteristic impedance of air ρc.**<br>• `density` ρ [kg/m³]<br>• `speed_of_sound` c [m/s] | `characteristic_impedance(1.186, 343.2) # 407.0`<br><br>• [rayl] | 713 + | `hydraulic_diameter` | `function` | **Hydraulic diameter of a rectangular tube, 4A/P (ISO 10534-2 A.2.1.5).**<br>• `width` w [m]<br>• `height` h [m] | `hydraulic_diameter(0.08, 0.04) # 0.0533`<br><br>• d_h = 2wh/(w + h) [m]; feed to `tube_attenuation_constant` | 712 714 | `tube_attenuation_constant` | `function` | **Lower-bound tube attenuation k0″ (ISO 10534-2 Eq. A.18).**<br>• `frequency` f [Hz]<br>• `speed_of_sound` c0 [m/s]<br>• `diameter` d [m] (hydraulic for rectangular) | `tube_attenuation_constant(1000.0, 343.2, 0.1) # 0.0179`<br><br>• [Np/m] | 713 715 | `tube_wavenumber` | `function` | **Complex wavenumber k0 = k0′ − j k0″ (ISO 10534-2 §2.6).**<br>• `frequency` f [Hz]<br>• `speed_of_sound` c0 [m/s]<br>• `attenuation`: k0″ [Np/m] (Default: None → lossless) | `tube_wavenumber(1000.0, 343.2, attenuation=0.018)`<br><br>• 18.308 − 0.018j [1/m] | 714 716 | `mic_calibration_factor` | `function` | **Microphone-mismatch calibration factor Hc (ISO 10534-2 Eq. 10).**<br>• `h12_config1`: H12 in the standard configuration<br>• `h12_config2`: H12 with the microphones interchanged | `hc = mic_calibration_factor(h12_a, h12_b)`<br><br>• Hc = √(H12ᴵ/H12ᴵᴵ) (complex) | ··· 719 721 | `normalized_surface_impedance` | `function` | **Normalised surface impedance Z/(ρc0) (Eq. 19).**<br>• `reflection`: complex r | `normalized_surface_impedance(0.5) # 3.0` | 720 722 | `normalized_surface_admittance` | `function` | **Normalised surface admittance Gρc0 (Eq. 20).**<br>• `reflection`: complex r | `normalized_surface_admittance(0.5) # 0.333`<br><br>• (1 − r)/(1 + r) | 721 723 | `two_microphone_impedance` | `function` | **Full two-microphone reduction (ISO 10534-2 Clause 7).**<br>• `h12`: corrected transfer function<br>• `frequency` f [Hz]<br>• `spacing` s [m], `x1` [m]<br>• `speed_of_sound` c0 [m/s], `characteristic_impedance` ρc0 [rayl]<br>• `attenuation`: k0″ [Np/m] (Default: None)<br>• `diameter` + `shape`: activate the plane-wave range check | `res = two_microphone_impedance(h12, frequency=f, spacing=0.05, x1=0.15, speed_of_sound=343.2, characteristic_impedance=407.0)`<br><br>• `ImpedanceTubeResult` | 722 - | `ImpedanceTubeResult` | `dataclass` | **Two-microphone result per frequency.**<br>• `frequency` [Hz]<br>• `reflection`: complex r (Eq. 17)<br>• `surface_impedance` [rayl], `normalized_impedance` (Eq. 19)<br>• `absorption`: α = 1 − \|r\|² (Eq. 18) | `res.absorption, res.normalized_impedance` | 724 + | `ImpedanceTubeResult` | `dataclass` | **Two-microphone result per frequency.**<br>• `frequency` [Hz]<br>• `reflection`: complex r (Eq. 17)<br>• `surface_impedance` [rayl], `normalized_impedance` (Eq. 19)<br>• `absorption`: α = 1 − \|r\|² (Eq. 18)<br>• Retains `spacing`, `x1`, `diameter`, `shape` when supplied | `res.absorption, res.normalized_impedance` | 723 725 | `ImpedanceTubeWarning` | `warning class` | **ISO 10534-2 advisory.**<br>Emitted by `two_microphone_impedance` for frequencies outside the plane-wave working range (Eqs. 1–4); the results are still returned | `warnings.simplefilter('error', ImpedanceTubeWarning)` | 724 726 | `standing_wave_ratio_from_level` | `function` | **Standing-wave ratio from ΔL (ISO 10534-1 Eq. 15).**<br>• `level_difference`: Lmax − Lmin [dB] | `standing_wave_ratio_from_level(20.0) # 10.0`<br><br>• s = 10^(ΔL/20) | 725 727 | `standing_wave_reflection_magnitude` | `function` | **Reflection magnitude from the SWR (Eq. 14).**<br>• `swr`: s ≥ 1 | `standing_wave_reflection_magnitude(10.0) # 0.818`<br><br>• \|r\| = (s − 1)/(s + 1) | 726 728 | `standing_wave_reflection` | `function` | **Complex reflection factor from the standing wave (Eqs. 17–23).**<br>• `swr`: s<br>• `first_min_distance`: x_min1 [m]<br>• `wavelength`: λ0 [m] | `standing_wave_reflection(10.0, 0.05, 0.4) # -0.818j`<br><br>• phase φ = π(4x_min1/λ0 − 1) | 727 729 | `standing_wave_absorption` | `function` | **Absorption from the SWR (Eqs. 9 + 14).**<br>• `swr`: s ≥ 1 | `standing_wave_absorption(10.0) # 0.331`<br><br>• α = 4s/(s + 1)² | 728 730 | `standing_wave_normalized_impedance` | `function` | **Normalised impedance from the standing wave (Eqs. 24–26).**<br>• `swr`, `first_min_distance` [m], `wavelength` [m] | `standing_wave_normalized_impedance(10.0, 0.05, 0.4)`<br><br>• 0.198 − 0.98j | 729 - | `wave_decomposition` | `function` | **Decompose the field into (A, B, C, D) (ASTM E2611-19 Eqs. 17–20).**<br>• `h1`…`h4`: microphone transfer functions<br>• `l1`, `s1`, `l2`, `s2`: geometry [m]<br>• `wavenumber` k | `A, B, C, D = wave_decomposition(h1, h2, h3, h4, l1=0.2, s1=0.05, l2=0.2, s2=0.05, wavenumber=k)`<br><br>• Forward/backward amplitudes, both sides | 731 + | `wave_decomposition` | `function` | **Decompose the field into (A, B, C, D) (ASTM E2611-19 Eqs. 17–20).**<br>• `h1`…`h4`: microphone transfer functions<br>• `l1`, `s1`, `l2`, `s2`: geometry [m]<br>• `wavenumber` k<br>• `diameter` + `shape`: activate the plane-wave range check | `A, B, C, D = wave_decomposition(h1, h2, h3, h4, l1=0.2, s1=0.05, l2=0.2, s2=0.05, wavenumber=k)`<br><br>• Forward/backward amplitudes, both sides | 730 732 | `face_quantities` | `function` | **Face pressures and velocities (Eq. 21).**<br>• `a`, `b`, `c`, `d`: wave amplitudes<br>• `wavenumber` k, `thickness` d [m], `characteristic_impedance` ρc | `p0, pd, u0, ud = face_quantities(A, B, C, D, wavenumber=k, thickness=0.05, characteristic_impedance=407.0)` | 731 - | `transfer_matrix_two_load` | `function` | **Two-load transfer matrix (ASTM E2611-19 Eqs. 17–22).**<br>• `load_a`, `load_b`: (H1, H2, H3, H4) per termination<br>• `l1`, `s1`, `l2`, `s2`, `thickness` [m]<br>• `wavenumber`, `characteristic_impedance` | `T = transfer_matrix_two_load(load_a, load_b, l1=0.2, s1=0.05, l2=0.2, s2=0.05, thickness=0.05, wavenumber=k, characteristic_impedance=407.0)`<br><br>• `TransferMatrix` | 732 - | `transfer_matrix_one_load` | `function` | **One-load transfer matrix, symmetric specimen (Eqs. 23–24).**<br>• `load`: (H1, H2, H3, H4)<br>• Same geometry parameters as `transfer_matrix_two_load` | `T = transfer_matrix_one_load(load, l1=0.2, s1=0.05, l2=0.2, s2=0.05, thickness=0.05, wavenumber=k, characteristic_impedance=407.0)`<br><br>• Requires reciprocity + symmetry | 733 + | `transfer_matrix_two_load` | `function` | **Two-load transfer matrix (ASTM E2611-19 Eqs. 17–22).**<br>• `load_a`, `load_b`: (H1, H2, H3, H4) per termination<br>• `l1`, `s1`, `l2`, `s2`, `thickness` [m]<br>• `wavenumber`, `characteristic_impedance`<br>• `frequency` (retained for `.plot()`), `diameter` + `shape`: range check | `T = transfer_matrix_two_load(load_a, load_b, l1=0.2, s1=0.05, l2=0.2, s2=0.05, thickness=0.05, wavenumber=k, characteristic_impedance=407.0)`<br><br>• `TransferMatrix` | 734 + | `transfer_matrix_one_load` | `function` | **One-load transfer matrix, symmetric specimen (Eqs. 23–24).**<br>• `load`: (H1, H2, H3, H4)<br>• Same geometry/frequency/range-check parameters as `transfer_matrix_two_load` | `T = transfer_matrix_one_load(load, l1=0.2, s1=0.05, l2=0.2, s2=0.05, thickness=0.05, wavenumber=k, characteristic_impedance=407.0)`<br><br>• Requires reciprocity + symmetry | 733 735 | `air_layer_transfer_matrix` | `function` | **Analytic loss-free air-layer matrix (validation reference).**<br>• `wavenumber` k<br>• `thickness` d [m]<br>• `characteristic_impedance` ρc [rayl] | `T = air_layer_transfer_matrix(k, 0.05, 407.0)`<br><br>• det(T) = 1, T11 = T22 | 734 - | `TransferMatrix` | `dataclass` | **Acoustic transfer matrix [[T11, T12], [T21, T22]] (ASTM E2611-19 Eq. 16).**<br>• `t11`, `t12`, `t21`, `t22`: complex, scalar or per frequency | `T.t11, T.t12` | 736 + | `TransferMatrix` | `dataclass` | **Acoustic transfer matrix [[T11, T12], [T21, T22]] (ASTM E2611-19 Eq. 16).**<br>• `t11`, `t12`, `t21`, `t22`: complex, scalar or per frequency<br>• Solver-built matrices retain `l1`/`s1`/`l2`/`s2`, `thickness`, `diameter`, `shape`, `frequency`, `air_characteristic_impedance` (so `.plot()` needs no arguments) | `T.t11, T.t12` | 735 737 | `airflow_resistance` | `function` | **Airflow resistance R = Δp/qv (ISO 9053-1:2018 §3.1).**<br>• `pressure_drop` Δp [Pa]<br>• `volume_flow_rate` qv [m³/s] | `airflow_resistance(2.0, 0.0005) # 4000.0`<br><br>• [Pa·s/m³] | 736 738 | `specific_airflow_resistance` | `function` | **Specific airflow resistance Rs (§3.2).**<br>• `resistance` R + `area` A, **or** `pressure_drop` Δp + `velocity` u | `specific_airflow_resistance(4000.0, 0.01) # 40.0`<br><br>• Rs = R·A = Δp/u [Pa·s/m] | 737 739 | `airflow_resistivity` | `function` | **Airflow resistivity σ = Rs/d (§3.3).**<br>• `specific_resistance` Rs [Pa·s/m]<br>• `thickness` d [m] | `airflow_resistivity(40.0, 0.05) # 800.0`<br><br>• [Pa·s/m²] |
+18 -3
docs/materials.md
··· 501 501 print(round(f_l, 1), round(f_u, 1)) # 858.0 6864.0 502 502 ``` 503 503 504 + The same helper covers square and rectangular tubes: pass `shape="square"` or 505 + `shape="rectangular"` and the Eq. (3) factor replaces the circular one, with 506 + $d$ the maximum side length. The four-microphone branch keeps its own limits 507 + in `plane_wave_frequency_range_astm`: ASTM E2611 retains the unrounded 508 + circular constant ($f\,d < 0.586\,c$, 6.2.4.1), the same rectangular 509 + $f\,d < 0.500\,c$ (6.2.5), a slightly stricter spacing bound 510 + $f\,s < 0.40\,c$ (6.5.4) and a laxer low end at 1 % of the wavelength 511 + (6.2.3). Passing `diameter=` (and `shape=`) to `two_microphone_impedance`, 512 + `wave_decomposition` or the `transfer_matrix_*` solvers turns the matching 513 + check into an advisory warning, and for the Annex A attenuation estimate of a 514 + rectangular tube `hydraulic_diameter(width, height)` supplies the $4A/P$ 515 + diameter that `tube_attenuation_constant` expects. 516 + 504 517 **Standing-wave-ratio method (ISO 10534-1).** A probe traverses the standing wave 505 518 and reads the level difference $\Delta L = L_\text{max} - L_\text{min}$ between a 506 519 pressure maximum and the adjacent minimum. The standing-wave ratio, reflection ··· 654 667 It uses the same `ReportMetadata` container and rendering engine as the other 655 668 fiches. The measured frequency range is taken from the result; the applicable 656 669 descriptive and geometric `ReportMetadata` fields are `client`, `manufacturer`, 657 - `specimen`, `tube_diameter`, `mic_spacing`, `mounting`, `test_room`, 670 + `specimen`, `tube_diameter`, `tube_shape`, `mic_spacing`, `mounting`, `test_room`, 658 671 `test_date`, `temperature`, `pressure`, `measurement_standard`, `laboratory`, 659 672 `operator`, `report_id` and `notes` (`tube_diameter` and `mic_spacing` are given 660 673 in metres and printed in millimetres). The `requirement` field is ignored ··· 719 732 `(H1, H2, H3, H4)` of each load; its methods 720 733 (`transmission_loss`, `reflection_hard_backed`, `absorption_hard_backed`, 721 734 `characteristic_impedance_material`, `material_wavenumber`) then read off the 722 - ASTM E2611 quantities, and its `.plot(frequency, rho_c)` draws the 723 - transmission loss with the hard-backed absorption overlaid. The matrix does 735 + ASTM E2611 quantities, and its `.plot()` draws the transmission loss with 736 + the hard-backed absorption overlaid (a matrix built by the solvers retains 737 + $\rho c$ and, when supplied, the frequency vector, so only a hand-built 738 + matrix needs them as arguments). The matrix does 724 739 not have to come from a measurement: the 725 740 [multilayer solver](porous-absorbers.md) exposes the chain matrix of any 726 741 modelled stack in the same convention, so a predicted specimen can be read
+4
src/phonometry/__init__.py
··· 523 523 apply_mic_calibration, 524 524 characteristic_impedance, 525 525 face_quantities, 526 + hydraulic_diameter, 526 527 mic_calibration_factor, 527 528 normalized_surface_admittance, 528 529 normalized_surface_impedance, 529 530 plane_wave_frequency_range, 531 + plane_wave_frequency_range_astm, 530 532 reflection_factor, 531 533 speed_of_sound_astm, 532 534 speed_of_sound_iso, ··· 1596 1598 "helmholtz_resonator_impedance", 1597 1599 "hemisphere_source_level", 1598 1600 "htlan", 1601 + "hydraulic_diameter", 1599 1602 "hydrophone_depths", 1600 1603 "idle_channel_noise", 1601 1604 "image_source_rir", ··· 1764 1767 "piston_resistance", 1765 1768 "piston_volume_flow_rate", 1766 1769 "plane_wave_frequency_range", 1770 + "plane_wave_frequency_range_astm", 1767 1771 "plate_bending_stiffness", 1768 1772 "plate_loss_factor", 1769 1773 "plenum_attenuation",
+169
tests/materials/test_impedance_tube.py
··· 34 34 apply_mic_calibration, 35 35 characteristic_impedance, 36 36 face_quantities, 37 + hydraulic_diameter, 37 38 mic_calibration_factor, 38 39 normalized_surface_admittance, 39 40 normalized_surface_impedance, 40 41 plane_wave_frequency_range, 42 + plane_wave_frequency_range_astm, 41 43 reflection_factor, 42 44 speed_of_sound_astm, 43 45 speed_of_sound_iso, ··· 246 248 characteristic_impedance=RC, 247 249 diameter=0.05, 248 250 ) 251 + 252 + 253 + def test_plane_wave_range_rectangular_bound() -> None: 254 + # ISO 10534-2 Eq. (3): a rectangular tube cuts on at 0,50 c0 / d with d the 255 + # maximum side length (stricter than the 0,58 circular factor of Eq. (2)). 256 + f_lower, f_upper = plane_wave_frequency_range( 257 + 0.03, C0, diameter=0.05, shape="rectangular" 258 + ) 259 + assert f_upper == pytest.approx(min(0.45 * C0 / 0.03, 0.50 * C0 / 0.05)) 260 + assert f_lower == pytest.approx(C0 / (20.0 * 0.03)) 261 + 262 + 263 + def test_plane_wave_range_square_alias() -> None: 264 + # A square tube is the rectangular case with d the side length, in both 265 + # the ISO 10534-2 and the ASTM E2611-19 bounds. 266 + for fn in (plane_wave_frequency_range, plane_wave_frequency_range_astm): 267 + square = fn(0.03, C0, diameter=0.05, shape="square") 268 + rectangular = fn(0.03, C0, diameter=0.05, shape="rectangular") 269 + assert square == rectangular 270 + 271 + 272 + def test_plane_wave_range_invalid_shape() -> None: 273 + with pytest.raises(ValueError, match="shape"): 274 + plane_wave_frequency_range(0.03, C0, diameter=0.05, shape="oval") 275 + with pytest.raises(ValueError, match="shape"): 276 + plane_wave_frequency_range_astm(0.03, C0, diameter=0.05, shape="oval") 277 + 278 + 279 + def test_plane_wave_range_astm_bounds() -> None: 280 + # ASTM E2611-19: upper = min(0,40 c/s (6.5.4), 0,586 c/d circular 281 + # (6.2.4.1) or 0,500 c/d rectangular (6.2.5)); lower = c / (100 s), the 282 + # spacing greater than 1 % of the wavelength (6.2.3). 283 + f_lower, f_upper = plane_wave_frequency_range_astm( 284 + 0.03, C0, diameter=0.05, shape="circular" 285 + ) 286 + assert f_upper == pytest.approx(min(0.40 * C0 / 0.03, 0.586 * C0 / 0.05)) 287 + assert f_lower == pytest.approx(C0 / (100.0 * 0.03)) 288 + _, f_upper_rect = plane_wave_frequency_range_astm( 289 + 0.03, C0, diameter=0.05, shape="rectangular" 290 + ) 291 + assert f_upper_rect == pytest.approx(min(0.40 * C0 / 0.03, 0.50 * C0 / 0.05)) 292 + 293 + 294 + def test_hydraulic_diameter_values() -> None: 295 + # d_h = 4 A / P = 2 w h / (w + h) (ISO 10534-2 A.2.1.5); a square tube 296 + # gives the side length back. 297 + assert hydraulic_diameter(0.05, 0.05) == pytest.approx(0.05) 298 + assert hydraulic_diameter(0.08, 0.04) == pytest.approx( 299 + 4.0 * (0.08 * 0.04) / (2.0 * (0.08 + 0.04)) 300 + ) 301 + with pytest.raises(ValueError, match="positive"): 302 + hydraulic_diameter(0.0, 0.05) 303 + 304 + 305 + def test_result_retains_tube_geometry() -> None: 306 + f = np.array([800.0, 1200.0]) 307 + h12 = _synth_h12(0.3 + 0.1j, np.asarray(tube_wavenumber(f, C0)), 0.12, 0.03) 308 + res = two_microphone_impedance( 309 + h12, frequency=f, spacing=0.03, x1=0.12, speed_of_sound=C0, 310 + characteristic_impedance=RC, diameter=0.05, shape="square", 311 + ) 312 + assert res.spacing == pytest.approx(0.03) 313 + assert res.x1 == pytest.approx(0.12) 314 + assert res.diameter == pytest.approx(0.05) 315 + # "square" is stored canonically as the rectangular case. 316 + assert res.shape == "rectangular" 317 + # Without a diameter no cross-section claim is retained. 318 + res_no_d = two_microphone_impedance( 319 + h12, frequency=f, spacing=0.03, x1=0.12, speed_of_sound=C0, 320 + characteristic_impedance=RC, 321 + ) 322 + assert res_no_d.diameter is None 323 + assert res_no_d.shape is None 249 324 250 325 251 326 def test_frequency_range_no_warning_in_band() -> None: ··· 537 612 assert np.allclose(rec.t11, rec.t22) 538 613 539 614 615 + def test_astm_solvers_warn_outside_plane_wave_range() -> None: 616 + # With the tube diameter given, frequencies past the 6.2.4.1 cut-on 617 + # (0,586 c/d for this 10 cm circular tube ~ 2011 Hz) must be flagged. 618 + f = np.array([500.0, 2500.0]) 619 + k = np.asarray(tube_wavenumber(f, C0)).real.astype(np.complex128) 620 + tm = air_layer_transfer_matrix(k, THICKNESS, RC) 621 + load_a = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, 0.3 + 0.0j) 622 + load_b = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, -0.5 + 0.2j) 623 + with pytest.warns(ImpedanceTubeWarning, match="ASTM E2611-19"): 624 + transfer_matrix_two_load( 625 + load_a, load_b, thickness=THICKNESS, wavenumber=k, 626 + characteristic_impedance=RC, diameter=0.10, **GEOM, 627 + ) 628 + with pytest.warns(ImpedanceTubeWarning, match="ASTM E2611-19"): 629 + transfer_matrix_one_load( 630 + load_a, thickness=THICKNESS, wavenumber=k, 631 + characteristic_impedance=RC, diameter=0.10, **GEOM, 632 + ) 633 + with pytest.warns(ImpedanceTubeWarning, match="ASTM E2611-19"): 634 + wave_decomposition( 635 + *load_a, wavenumber=k, diameter=0.10, **GEOM, 636 + ) 637 + 638 + 639 + def test_astm_solvers_warn_below_lower_bound() -> None: 640 + # The 6.2.3 lower bound: spacing greater than 1 % of the wavelength. For 641 + # s = 3 cm that is ~114 Hz; 50 Hz must be flagged. 642 + f = np.array([50.0]) 643 + k = np.asarray(tube_wavenumber(f, C0)).real.astype(np.complex128) 644 + tm = air_layer_transfer_matrix(k, THICKNESS, RC) 645 + load = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, 0.3 + 0.0j) 646 + with pytest.warns(ImpedanceTubeWarning, match="ASTM E2611-19"): 647 + wave_decomposition(*load, wavenumber=k, diameter=0.10, **GEOM) 648 + 649 + 650 + def test_astm_solvers_silent_in_band_and_without_diameter() -> None: 651 + import warnings as _w 652 + 653 + f = np.array([500.0, 1000.0, 1800.0]) 654 + k = np.asarray(tube_wavenumber(f, C0)).real.astype(np.complex128) 655 + tm = air_layer_transfer_matrix(k, THICKNESS, RC) 656 + load_a = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, 0.3 + 0.0j) 657 + load_b = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, -0.5 + 0.2j) 658 + with _w.catch_warnings(): 659 + _w.simplefilter("error", ImpedanceTubeWarning) 660 + # In band with the diameter given (10 cm tube: 114 Hz .. 2011 Hz). 661 + transfer_matrix_two_load( 662 + load_a, load_b, thickness=THICKNESS, wavenumber=k, 663 + characteristic_impedance=RC, diameter=0.10, **GEOM, 664 + ) 665 + # No diameter -> no range check, as in the ISO branch. 666 + k_wide = np.asarray( 667 + tube_wavenumber(np.array([50.0, 2500.0]), C0) 668 + ).real.astype(np.complex128) 669 + tm_wide = air_layer_transfer_matrix(k_wide, THICKNESS, RC) 670 + wide_a = _synth_four_mics(tm_wide, np.asarray(k_wide), 1.0 + 0.0j, 0.3 + 0.0j) 671 + wide_b = _synth_four_mics(tm_wide, np.asarray(k_wide), 0.4 + 0.5j, -0.6 + 0.1j) 672 + transfer_matrix_two_load( 673 + wide_a, wide_b, thickness=THICKNESS, wavenumber=k_wide, 674 + characteristic_impedance=RC, **GEOM, 675 + ) 676 + 677 + 678 + def test_transfer_matrix_retains_measurement_context() -> None: 679 + # 500..1500 Hz sits inside the square-tube working range (114..1716 Hz). 680 + f = np.array([500.0, 1000.0, 1500.0]) 681 + k = np.asarray(tube_wavenumber(f, C0)).real.astype(np.complex128) 682 + tm = air_layer_transfer_matrix(k, THICKNESS, RC) 683 + load_a = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, 0.3 + 0.0j) 684 + load_b = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, -0.5 + 0.2j) 685 + rec = transfer_matrix_two_load( 686 + load_a, load_b, thickness=THICKNESS, wavenumber=k, 687 + characteristic_impedance=RC, frequency=f, diameter=0.10, 688 + shape="square", **GEOM, 689 + ) 690 + assert rec.l1 == pytest.approx(GEOM["l1"]) 691 + assert rec.s1 == pytest.approx(GEOM["s1"]) 692 + assert rec.l2 == pytest.approx(GEOM["l2"]) 693 + assert rec.s2 == pytest.approx(GEOM["s2"]) 694 + assert rec.thickness == pytest.approx(THICKNESS) 695 + assert rec.diameter == pytest.approx(0.10) 696 + assert rec.shape == "rectangular" 697 + assert rec.frequency is not None 698 + assert np.allclose(rec.frequency, f) 699 + assert rec.air_characteristic_impedance == pytest.approx(RC) 700 + # A hand-built matrix retains nothing and plot() demands the arguments. 701 + bare = air_layer_transfer_matrix(k, THICKNESS, RC) 702 + assert bare.frequency is None 703 + assert bare.air_characteristic_impedance is None 704 + with pytest.raises(ValueError, match="hand-built"): 705 + bare.plot() 706 + 707 + 540 708 def test_public_exports() -> None: 541 709 import phonometry 542 710 ··· 548 716 "air_density_iso", "air_density_astm", "tube_wavenumber", 549 717 "tube_attenuation_constant", "mic_calibration_factor", "apply_mic_calibration", 550 718 "two_microphone_impedance", "plane_wave_frequency_range", 719 + "plane_wave_frequency_range_astm", "hydraulic_diameter", 551 720 "standing_wave_absorption", "standing_wave_reflection", 552 721 "standing_wave_reflection_magnitude", "standing_wave_ratio_from_level", 553 722 "standing_wave_normalized_impedance", "wave_decomposition", "face_quantities",
+4
src/phonometry/_report/_i18n.py
··· 192 192 "{standard} normal-incidence measurement of sound absorption and surface impedance in an impedance tube by the two-microphone transfer-function method per ISO 10534-2:2001.": "{standard} medición a incidencia normal de la absorción acústica y la impedancia superficial en tubo de impedancia por el método de la función de transferencia con dos micrófonos según ISO 10534-2:2001.", 193 193 "Normal-incidence measurement of sound absorption and surface impedance in an impedance tube by the two-microphone transfer-function method per ISO 10534-2:2001.": "Medición a incidencia normal de la absorción acústica y la impedancia superficial en tubo de impedancia por el método de la función de transferencia con dos micrófonos según ISO 10534-2:2001.", 194 194 "Tube diameter d [mm]": "Diámetro del tubo d [mm]", 195 + "Tube cross-section": "Sección transversal del tubo", 196 + "Circular": "Circular", 197 + "Rectangular": "Rectangular", 198 + "Square": "Cuadrada", 195 199 "Microphone spacing s [mm]": "Separación de micrófonos s [mm]", 196 200 "Frequency range [Hz]": "Rango de frecuencias [Hz]", 197 201 "Normal-incidence &#945; and normalised surface impedance z": "&#945; a incidencia normal e impedancia superficial normalizada z",
+11
src/phonometry/_report/iso10534.py
··· 59 59 if TYPE_CHECKING: 60 60 from ..materials.impedance_tube import ImpedanceTubeResult 61 61 62 + #: Printable label for each accepted ``ReportMetadata.tube_shape`` value. 63 + _SHAPE_LABELS = { 64 + "circular": "Circular", 65 + "rectangular": "Rectangular", 66 + "square": "Square", 67 + } 68 + 62 69 63 70 def _a2(value: float, language: str = "en") -> str: 64 71 """Two decimals (``alpha``, ``|r|`` and the normalised impedance parts).""" ··· 88 95 specimen = _md("specimen") 89 96 tube_diameter = _md("tube_diameter") 90 97 mic_spacing = _md("mic_spacing") 98 + tube_shape = _md("tube_shape") 91 99 mounting = _md("mounting") 92 100 test_room = _md("test_room") 93 101 test_date = _md("test_date") ··· 108 116 (t("Tube diameter d [mm]", language), 109 117 fmt_meta(tube_diameter * 1e3, language) 110 118 if tube_diameter is not None else None), 119 + (t("Tube cross-section", language), 120 + t(_SHAPE_LABELS[tube_shape], language) 121 + if tube_shape is not None else None), 111 122 (t("Microphone spacing s [mm]", language), 112 123 fmt_meta(mic_spacing * 1e3, language) 113 124 if mic_spacing is not None else None),
+18 -3
src/phonometry/_report/metadata.py
··· 81 81 positions of the impedance tube, in metres. Printed by the 82 82 impedance-tube fiche (ISO 10534-2), where it bounds the working 83 83 frequency range. 84 + :ivar tube_shape: Cross-section of the impedance tube: ``"circular"``, 85 + ``"rectangular"`` or ``"square"``. Printed by the impedance-tube fiche 86 + (ISO 10534-2) next to the tube diameter, which it qualifies (inner 87 + diameter for a circular tube, maximum lateral dimension otherwise). 84 88 :ivar thickness: Specimen thickness under the applied static load, in 85 89 metres. Printed by the dynamic-stiffness fiche (EN 29052-1 / 86 90 ISO 9052-1), where EN 29052-1:1992 Clause 9 b) requires reporting the ··· 113 117 :ivar notes: Free-form remarks printed in the footer. 114 118 :raises ValueError: If a supplied dimension/mass/volume/pressure is not 115 119 finite and strictly positive, a temperature or requirement is not 116 - finite, a relative humidity is outside 0..100 %, or a required class is 117 - not one of 0, 1, 2, or a position count is not a finite, positive 118 - integer. 120 + finite, a relative humidity is outside 0..100 %, a required class is 121 + not one of 0, 1, 2, a position count is not a finite, positive 122 + integer, or a tube shape is not one of ``"circular"``, 123 + ``"rectangular"``, ``"square"``. 119 124 """ 120 125 121 126 specimen: str | None = None ··· 151 156 requirement: float | None = None 152 157 required_class: int | None = None 153 158 notes: str | None = None 159 + # Appended after the original fields so the positional constructor order 160 + # of this public dataclass is preserved. 161 + tube_shape: str | None = None 154 162 155 163 #: Numeric fields that must be finite and strictly positive. 156 164 _POSITIVE_FIELDS = ( ··· 231 239 raise ValueError( 232 240 "ReportMetadata.required_class must be 0, 1 or 2 when given; " 233 241 f"got {self.required_class!r}." 242 + ) 243 + if self.tube_shape is not None and self.tube_shape not in ( 244 + "circular", "rectangular", "square" 245 + ): 246 + raise ValueError( 247 + "ReportMetadata.tube_shape must be 'circular', 'rectangular' " 248 + f"or 'square' when given; got {self.tube_shape!r}." 234 249 ) 235 250 236 251 def is_empty(self) -> bool:
+4
src/phonometry/materials/__init__.py
··· 63 63 apply_mic_calibration, 64 64 characteristic_impedance, 65 65 face_quantities, 66 + hydraulic_diameter, 66 67 mic_calibration_factor, 67 68 normalized_surface_admittance, 68 69 normalized_surface_impedance, 69 70 plane_wave_frequency_range, 71 + plane_wave_frequency_range_astm, 70 72 reflection_factor, 71 73 speed_of_sound_astm, 72 74 speed_of_sound_iso, ··· 270 272 "geometric_spreading_factor_angle", 271 273 "helmholtz_resonance_frequency", 272 274 "helmholtz_resonator_impedance", 275 + "hydraulic_diameter", 273 276 "insitu_absorption_coefficient", 274 277 "insitu_absorption_from_reflection", 275 278 "insitu_absorption_spectrum", ··· 295 298 "perforation_end_correction", 296 299 "piston_volume_flow_rate", 297 300 "plane_wave_frequency_range", 301 + "plane_wave_frequency_range_astm", 298 302 "power_reflection_coefficient", 299 303 "practical_absorption_coefficient", 300 304 "practical_coefficient_uncertainty",
+325 -27
src/phonometry/materials/impedance_tube.py
··· 85 85 #: i.e. ``f_l = c0 / (20 s)``. 86 86 _ISO_LOWER_WAVELENGTH_FRACTION = 20.0 87 87 88 + #: Upper-frequency plane-wave factor ``K``, circular tube (ASTM E2611-19, 89 + #: 6.2.4.1, Eq. (2)). 90 + _ASTM_KU_CIRCULAR = 0.586 91 + #: Upper-frequency plane-wave factor ``K``, rectangular tube with ``d`` the 92 + #: largest section dimension (ASTM E2611-19, 6.2.5). 93 + _ASTM_KU_RECTANGULAR = 0.500 94 + #: Microphone-spacing factor for the upper limit: ``s`` no larger than 80 % of 95 + #: ``c / (2 f_u)``, i.e. ``f_u s < 0,40 c`` (ASTM E2611-19, 6.5.4). 96 + _ASTM_KU_SPACING = 0.40 97 + #: Lower-limit factor: spacing shall be greater than 1 % of the wavelength 98 + #: (ASTM E2611-19, 6.2.3), i.e. ``f_l = c / (100 s)``. 99 + _ASTM_LOWER_WAVELENGTH_FRACTION = 100.0 100 + 101 + #: Shared validation message for the tube diameter arguments. 102 + _DIAMETER_POSITIVE = "'diameter' must be positive." 103 + 104 + #: Aliases accepted for the tube cross-section ``shape`` arguments. A square 105 + #: tube is the rectangular case with equal sides (ISO 10534-2, 4.1; 106 + #: ASTM E2611-19, 6.2.5). 107 + _SHAPE_ALIASES = { 108 + "circular": "circular", 109 + "rectangular": "rectangular", 110 + "square": "rectangular", 111 + } 112 + 88 113 __all__ = [ 89 114 "ImpedanceTubeResult", 90 115 "ImpedanceTubeWarning", ··· 96 121 "apply_mic_calibration", 97 122 "characteristic_impedance", 98 123 "face_quantities", 124 + "hydraulic_diameter", 99 125 "mic_calibration_factor", 100 126 "normalized_surface_admittance", 101 127 "normalized_surface_impedance", 102 128 "plane_wave_frequency_range", 129 + "plane_wave_frequency_range_astm", 103 130 "reflection_factor", 104 131 "speed_of_sound_astm", 105 132 "speed_of_sound_iso", ··· 202 229 203 230 204 231 # --------------------------------------------------------------------------- 205 - # Wavenumber and tube attenuation (ISO 10534-2). 232 + # Cross-section helpers, wavenumber and tube attenuation (ISO 10534-2). 206 233 # --------------------------------------------------------------------------- 234 + def _canonical_shape(shape: str) -> str: 235 + """Normalise a cross-section name to ``"circular"``/``"rectangular"``.""" 236 + try: 237 + return _SHAPE_ALIASES[shape] 238 + except KeyError: 239 + raise ValueError( 240 + "'shape' must be 'circular', 'rectangular' or 'square'." 241 + ) from None 242 + 243 + 244 + def hydraulic_diameter(width: float, height: float) -> float: 245 + """Hydraulic diameter of a rectangular tube, ``4 A / P`` (ISO 10534-2, A.2.1.5). 246 + 247 + For a rectangular cross-section of side lengths ``w`` and ``h`` the ratio 248 + of four times the area to the perimeter reduces to 249 + ``d_h = 2 w h / (w + h)``; a square tube gives ``d_h`` equal to the side 250 + length. This is the ``d`` the Eq. (A.18) attenuation estimate expects for 251 + rectangular tubes (see :func:`tube_attenuation_constant`). 252 + 253 + :param width: Inner side length ``w``, in metres. 254 + :param height: Inner side length ``h``, in metres. 255 + :return: Hydraulic diameter ``d_h = 4 A / P``, in metres. 256 + """ 257 + if width <= 0.0 or height <= 0.0: 258 + raise ValueError("'width' and 'height' must be positive.") 259 + return float(2.0 * width * height / (width + height)) 260 + 261 + 207 262 def tube_attenuation_constant( 208 263 frequency: ArrayLike, speed_of_sound: float, diameter: float 209 264 ) -> Real: ··· 215 270 :param frequency: Frequency ``f``, in hertz (scalar or per band). 216 271 :param speed_of_sound: Speed of sound ``c0``, in metres per second. 217 272 :param diameter: Circular-tube diameter ``d``, in metres, or the hydraulic 218 - diameter ``4 * area / perimeter`` for a rectangular tube. 273 + diameter ``4 * area / perimeter`` for a rectangular tube (see 274 + :func:`hydraulic_diameter`). 219 275 :return: Attenuation constant ``k0''``, in nepers per metre. 220 276 """ 221 277 if speed_of_sound <= 0.0: 222 278 raise ValueError("'speed_of_sound' must be positive.") 223 279 if diameter <= 0.0: 224 - raise ValueError("'diameter' must be positive.") 280 + raise ValueError(_DIAMETER_POSITIVE) 225 281 f = np.asarray(frequency, dtype=np.float64) 226 282 if np.any(f < 0.0): 227 283 raise ValueError("'frequency' must be non-negative.") ··· 414 470 :param spacing: Microphone spacing ``s``, in metres. 415 471 :param speed_of_sound: Speed of sound ``c0``, in metres per second. 416 472 :param diameter: Tube diameter (circular) or maximum lateral dimension 417 - (rectangular) ``d``, in metres; ``None`` applies only the spacing bound. 418 - :param shape: ``"circular"`` or ``"rectangular"``. 473 + (rectangular/square) ``d``, in metres; ``None`` applies only the 474 + spacing bound. 475 + :param shape: ``"circular"``, ``"rectangular"`` or ``"square"`` (a square 476 + tube is the rectangular bound with ``d`` the side length). 419 477 :return: Tuple ``(f_l, f_u)`` of the lower and upper frequency limits, in Hz. 420 478 """ 479 + return _frequency_range( 480 + spacing, 481 + speed_of_sound, 482 + diameter=diameter, 483 + shape=shape, 484 + ku_circular=_ISO_KU_CIRCULAR, 485 + ku_rectangular=_ISO_KU_RECTANGULAR, 486 + ku_spacing=_ISO_KU_SPACING, 487 + lower_fraction=_ISO_LOWER_WAVELENGTH_FRACTION, 488 + ) 489 + 490 + 491 + def plane_wave_frequency_range_astm( 492 + spacing: float, 493 + speed_of_sound: float, 494 + *, 495 + diameter: float | None = None, 496 + shape: str = "circular", 497 + ) -> tuple[float, float]: 498 + """Working plane-wave frequency range ``(f_l, f_u)`` (ASTM E2611-19). 499 + 500 + The upper limit is the smaller of the microphone-spacing bound 501 + ``s <= 0,8 c / (2 f_u)``, i.e. ``f_u s < 0,40 c`` (6.5.4), and, when the 502 + tube ``diameter`` is given, the cut-on bound ``f_u < K c / d`` with 503 + ``K = 0,586`` for a circular tube (6.2.4.1, Eq. (2)) or ``K = 0,500`` for 504 + a rectangular tube with ``d`` the largest section dimension (6.2.5). The 505 + lower limit follows 6.2.3: the spacing shall be greater than 1 % of the 506 + wavelength, i.e. ``f_l = c / (100 s)``. 507 + 508 + With two different spacings ``s1``/``s2``, call with the larger one for 509 + the upper bound and the smaller one for the lower bound (each bound is 510 + binding for every microphone pair). 511 + 512 + :param spacing: Microphone spacing ``s``, in metres. 513 + :param speed_of_sound: Speed of sound ``c``, in metres per second. 514 + :param diameter: Tube diameter (circular) or largest section dimension 515 + (rectangular/square) ``d``, in metres; ``None`` applies only the 516 + spacing bound. 517 + :param shape: ``"circular"``, ``"rectangular"`` or ``"square"``. 518 + :return: Tuple ``(f_l, f_u)`` of the lower and upper frequency limits, in Hz. 519 + """ 520 + return _frequency_range( 521 + spacing, 522 + speed_of_sound, 523 + diameter=diameter, 524 + shape=shape, 525 + ku_circular=_ASTM_KU_CIRCULAR, 526 + ku_rectangular=_ASTM_KU_RECTANGULAR, 527 + ku_spacing=_ASTM_KU_SPACING, 528 + lower_fraction=_ASTM_LOWER_WAVELENGTH_FRACTION, 529 + ) 530 + 531 + 532 + def _frequency_range( 533 + spacing: float, 534 + speed_of_sound: float, 535 + *, 536 + diameter: float | None, 537 + shape: str, 538 + ku_circular: float, 539 + ku_rectangular: float, 540 + ku_spacing: float, 541 + lower_fraction: float, 542 + ) -> tuple[float, float]: 543 + """Shared ISO/ASTM plane-wave frequency-range arithmetic.""" 421 544 if spacing <= 0.0: 422 545 raise ValueError("'spacing' must be positive.") 423 546 if speed_of_sound <= 0.0: 424 547 raise ValueError("'speed_of_sound' must be positive.") 425 - if shape not in ("circular", "rectangular"): 426 - raise ValueError("'shape' must be 'circular' or 'rectangular'.") 427 - f_upper = _ISO_KU_SPACING * speed_of_sound / spacing 548 + canonical = _canonical_shape(shape) 549 + f_upper = ku_spacing * speed_of_sound / spacing 428 550 if diameter is not None: 429 551 if diameter <= 0.0: 430 - raise ValueError("'diameter' must be positive.") 431 - factor = ( 432 - _ISO_KU_CIRCULAR if shape == "circular" else _ISO_KU_RECTANGULAR 433 - ) 552 + raise ValueError(_DIAMETER_POSITIVE) 553 + factor = ku_circular if canonical == "circular" else ku_rectangular 434 554 f_upper = min(f_upper, factor * speed_of_sound / diameter) 435 - f_lower = speed_of_sound / (_ISO_LOWER_WAVELENGTH_FRACTION * spacing) 555 + f_lower = speed_of_sound / (lower_fraction * spacing) 436 556 return f_lower, f_upper 437 557 438 558 ··· 464 584 (Eq. (19)), ``normalized_impedance`` the ratio ``Z / (rho c0)`` (Eq. (19)) 465 585 and ``absorption`` the normal-incidence coefficient ``alpha = 1 - |r|^2`` 466 586 (Eq. (18)). 587 + 588 + The trailing fields retain the tube geometry the reduction was run with 589 + (microphone ``spacing`` ``s``, distance ``x1`` from the sample to the 590 + farther microphone, tube ``diameter`` and cross-section ``shape``, stored 591 + canonically as ``"circular"``/``"rectangular"`` - a ``"square"`` input is 592 + kept as ``"rectangular"``); they default to ``None`` when not supplied to 593 + :func:`two_microphone_impedance`. 467 594 """ 468 595 469 596 frequency: Real ··· 471 598 surface_impedance: Complex 472 599 normalized_impedance: Complex 473 600 absorption: Real 601 + spacing: float | None = None 602 + x1: float | None = None 603 + diameter: float | None = None 604 + shape: str | None = None 474 605 475 606 def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes: 476 607 """Plot the absorption spectrum ``alpha(f)`` with ``|r|`` overlaid. ··· 513 644 produces a body-and-disclaimer fiche whose header shows only the 514 645 measured frequency range. The applicable descriptive/geometric 515 646 fields are ``client``, ``manufacturer``, ``specimen``, 516 - ``tube_diameter``, ``mic_spacing``, ``mounting``, ``test_room``, 647 + ``tube_diameter``, ``tube_shape``, ``mic_spacing``, ``mounting``, 648 + ``test_room``, 517 649 ``test_date``, ``temperature``, ``pressure``, 518 650 ``measurement_standard``, ``laboratory``, ``operator``, 519 651 ``report_id`` and ``notes``. The ``requirement`` field is ignored ··· 574 706 nepers/m (see :func:`tube_attenuation_constant`). 575 707 :param diameter: Optional tube diameter/lateral dimension, in metres, that 576 708 activates the plane-wave range check. 577 - :param shape: Tube cross-section, ``"circular"`` or ``"rectangular"``. 578 - :return: An :class:`ImpedanceTubeResult`. 709 + :param shape: Tube cross-section, ``"circular"``, ``"rectangular"`` or 710 + ``"square"``. 711 + :return: An :class:`ImpedanceTubeResult` (the tube geometry is retained on 712 + the result). 579 713 """ 580 714 f = np.asarray(frequency, dtype=np.float64) 581 715 k0 = tube_wavenumber(f, speed_of_sound, attenuation=attenuation) 582 716 r = reflection_factor(h12, spacing=spacing, x1=x1, wavenumber=k0) 717 + canonical = _canonical_shape(shape) 583 718 if diameter is not None: 584 719 f_lower, f_upper = plane_wave_frequency_range( 585 - spacing, speed_of_sound, diameter=diameter, shape=shape 720 + spacing, speed_of_sound, diameter=diameter, shape=canonical 586 721 ) 587 722 _warn_frequency_range(f, f_lower, f_upper, stacklevel=2) 588 723 return ImpedanceTubeResult( ··· 591 726 surface_impedance=surface_impedance(r, characteristic_impedance), 592 727 normalized_impedance=normalized_surface_impedance(r), 593 728 absorption=absorption_from_reflection(r), 729 + spacing=spacing, 730 + x1=x1, 731 + diameter=diameter, 732 + shape=canonical if diameter is not None else None, 594 733 ) 595 734 596 735 ··· 698 837 # --------------------------------------------------------------------------- 699 838 # ASTM E2611-19: four-microphone transfer-matrix method. 700 839 # --------------------------------------------------------------------------- 840 + def _warn_astm_plane_wave( 841 + wavenumber: ArrayLike, 842 + *, 843 + s1: float, 844 + s2: float, 845 + diameter: float, 846 + shape: str, 847 + stacklevel: int, 848 + ) -> None: 849 + """Advise when wavenumbers leave the ASTM E2611-19 plane-wave range. 850 + 851 + The check runs on the real part of ``k`` so no speed of sound is needed: 852 + ``f s < 0,40 c`` maps to ``k s < 0,80 pi`` (6.5.4), ``f d < K c`` to 853 + ``k d < 2 pi K`` (6.2.4.1/6.2.5) and the greater-than-1 %-of-wavelength 854 + spacing bound to ``k s > 0,02 pi`` (6.2.3). The upper spacing bound binds 855 + every microphone pair (largest spacing), the lower one the smallest. 856 + """ 857 + if s1 <= 0.0 or s2 <= 0.0: 858 + raise ValueError("'s1' and 's2' must be positive.") 859 + if diameter <= 0.0: 860 + raise ValueError(_DIAMETER_POSITIVE) 861 + k = np.real(np.asarray(wavenumber, dtype=np.complex128)) 862 + ku = _ASTM_KU_CIRCULAR if shape == "circular" else _ASTM_KU_RECTANGULAR 863 + two_pi = 2.0 * np.pi 864 + k_upper = min( 865 + two_pi * _ASTM_KU_SPACING / max(s1, s2), 866 + two_pi * ku / diameter, 867 + ) 868 + k_lower = two_pi / (_ASTM_LOWER_WAVELENGTH_FRACTION * min(s1, s2)) 869 + if np.any(k < k_lower) or np.any(k > k_upper): 870 + warnings.warn( 871 + "Wavenumbers outside the ASTM E2611-19 plane-wave working range " 872 + "(6.2.3-6.2.5, 6.5.4) for the given microphone spacings and tube " 873 + "cross-section; results there are advisory. See " 874 + "plane_wave_frequency_range_astm() for the limits in hertz.", 875 + ImpedanceTubeWarning, 876 + stacklevel=stacklevel + 1, 877 + ) 878 + 879 + 701 880 def wave_decomposition( 702 881 h1: ArrayLike, 703 882 h2: ArrayLike, ··· 709 888 l2: float, 710 889 s2: float, 711 890 wavenumber: ArrayLike, 891 + diameter: float | None = None, 892 + shape: str = "circular", 712 893 ) -> tuple[Complex, Complex, Complex, Complex]: 713 894 """Decompose the wave field into ``(A, B, C, D)`` (ASTM E2611-19, Eqs. (17)-(20)). 714 895 ··· 738 919 :param l2: Distance ``l2`` from the front reference plane, in metres. 739 920 :param s2: Downstream microphone spacing ``s2``, in metres. 740 921 :param wavenumber: Air wavenumber ``k`` (real or complex), scalar or per band. 922 + :param diameter: Optional tube diameter (circular) or largest section 923 + dimension (rectangular/square), in metres, that activates the 924 + plane-wave working-range check (6.2.3-6.2.5, 6.5.4). 925 + :param shape: Tube cross-section, ``"circular"``, ``"rectangular"`` or 926 + ``"square"``. 741 927 :return: Tuple ``(A, B, C, D)`` of complex amplitudes. 742 928 """ 743 929 if s1 <= 0.0 or s2 <= 0.0: 744 930 raise ValueError("'s1' and 's2' must be positive.") 931 + canonical = _canonical_shape(shape) 932 + if diameter is not None: 933 + _warn_astm_plane_wave( 934 + wavenumber, s1=s1, s2=s2, diameter=diameter, shape=canonical, 935 + stacklevel=2, 936 + ) 745 937 ha = np.asarray(h1, dtype=np.complex128) 746 938 hb = np.asarray(h2, dtype=np.complex128) 747 939 hc = np.asarray(h3, dtype=np.complex128) ··· 813 1005 Relates the pressure and normal particle velocity across a specimen, 814 1006 ``[p; u]_{x=0} = T [p; u]_{x=d}`` (Eq. (16)). Each entry is complex and 815 1007 may be scalar or a per-frequency array of matching shape. 1008 + 1009 + The trailing fields retain the measurement context when the matrix comes 1010 + out of :func:`transfer_matrix_two_load` / :func:`transfer_matrix_one_load` 1011 + (tube geometry ``l1``/``s1``/``l2``/``s2``, specimen ``thickness``, tube 1012 + ``diameter`` and canonical cross-section ``shape``, the ``frequency`` 1013 + vector when supplied to the solver, and the air 1014 + ``air_characteristic_impedance`` ``rho c``); all default to ``None`` so a 1015 + hand-built matrix (for example :func:`air_layer_transfer_matrix`) is 1016 + unchanged. 816 1017 """ 817 1018 818 1019 t11: Complex 819 1020 t12: Complex 820 1021 t21: Complex 821 1022 t22: Complex 1023 + l1: float | None = None 1024 + s1: float | None = None 1025 + l2: float | None = None 1026 + s2: float | None = None 1027 + thickness: float | None = None 1028 + diameter: float | None = None 1029 + shape: str | None = None 1030 + frequency: Real | None = None 1031 + air_characteristic_impedance: float | None = None 822 1032 823 1033 def determinant(self) -> Complex: 824 1034 """Determinant ``T11 T22 - T12 T21`` (unity for a reciprocal specimen).""" ··· 896 1106 897 1107 def plot( 898 1108 self, 899 - frequency: ArrayLike, 900 - characteristic_impedance: float, 1109 + frequency: ArrayLike | None = None, 1110 + characteristic_impedance: float | None = None, 901 1111 ax: Axes | None = None, 902 1112 *, 903 1113 language: str = "en", ··· 909 1119 laboratory quotes: the normal-incidence transmission loss ``TLn(f)`` 910 1120 (Eq. (26), the primary curve, left axis) and the hard-backed 911 1121 absorption coefficient ``alpha(f)`` (Eq. (28), a muted companion on a 912 - 0..1 right axis). The matrix itself carries no frequency axis, so the 913 - ``frequency`` vector of the measurement (matching the shape of the 914 - entries) and the air characteristic impedance ``rho c`` must be 915 - supplied. 1122 + 0..1 right axis). The four-pole entries carry no frequency axis of 1123 + their own, so the plot needs the measurement's ``frequency`` vector 1124 + (matching the shape of the entries) and the air characteristic 1125 + impedance ``rho c``. A matrix built by the solvers retains both 1126 + (``self.frequency`` / ``self.air_characteristic_impedance``), so 1127 + ``plot()`` takes no arguments there; only a hand-built matrix (for 1128 + example :func:`air_layer_transfer_matrix`) must supply them. 916 1129 917 1130 Requires matplotlib (``pip install phonometry[plot]``); returns the 918 1131 :class:`~matplotlib.axes.Axes` of the transmission-loss curve. 919 1132 920 1133 :param frequency: Frequency vector ``f``, in hertz, matching the shape 921 - of the matrix entries. 1134 + of the matrix entries; ``None`` uses the stored ``frequency``. 922 1135 :param characteristic_impedance: Characteristic impedance ``rho c`` of 923 - the air in the tube, in rayls. 1136 + the air in the tube, in rayls; ``None`` uses the stored 1137 + ``air_characteristic_impedance``. 924 1138 :param ax: Existing axes, or ``None`` to create a figure. 925 1139 :param language: Plot language: ``"en"`` (default) or ``"es"``. 926 1140 :param kwargs: Forwarded to the transmission-loss ``plot`` call. 927 1141 :return: The axes. 1142 + :raises ValueError: If ``frequency`` or ``characteristic_impedance`` 1143 + is neither supplied nor stored on the matrix. 928 1144 """ 1145 + if frequency is None: 1146 + frequency = self.frequency 1147 + if characteristic_impedance is None: 1148 + characteristic_impedance = self.air_characteristic_impedance 1149 + if frequency is None or characteristic_impedance is None: 1150 + raise ValueError( 1151 + "'frequency' and 'characteristic_impedance' must be supplied " 1152 + "when the matrix does not retain them (hand-built matrices)." 1153 + ) 929 1154 from .._i18n import check_language 930 1155 from .._plot.materials import plot_transfer_matrix 931 1156 ··· 1017 1242 ) 1018 1243 1019 1244 1245 + def _measurement_context( 1246 + *, 1247 + l1: float, 1248 + s1: float, 1249 + l2: float, 1250 + s2: float, 1251 + thickness: float, 1252 + diameter: float | None, 1253 + shape: str, 1254 + frequency: ArrayLike | None, 1255 + characteristic_impedance: float, 1256 + ) -> dict[str, Any]: 1257 + """Context fields a solver retains on the :class:`TransferMatrix`.""" 1258 + return { 1259 + "l1": l1, 1260 + "s1": s1, 1261 + "l2": l2, 1262 + "s2": s2, 1263 + "thickness": thickness, 1264 + "diameter": diameter, 1265 + "shape": shape if diameter is not None else None, 1266 + "frequency": ( 1267 + np.asarray(frequency, dtype=np.float64) 1268 + if frequency is not None else None 1269 + ), 1270 + "air_characteristic_impedance": characteristic_impedance, 1271 + } 1272 + 1273 + 1020 1274 def transfer_matrix_two_load( 1021 1275 load_a: tuple[ArrayLike, ArrayLike, ArrayLike, ArrayLike], 1022 1276 load_b: tuple[ArrayLike, ArrayLike, ArrayLike, ArrayLike], ··· 1028 1282 thickness: float, 1029 1283 wavenumber: ArrayLike, 1030 1284 characteristic_impedance: float, 1285 + frequency: ArrayLike | None = None, 1286 + diameter: float | None = None, 1287 + shape: str = "circular", 1031 1288 ) -> TransferMatrix: 1032 1289 """Two-load transfer matrix (ASTM E2611-19, Eqs. (17)-(22)). 1033 1290 ··· 1050 1307 :param thickness: Specimen thickness ``d``, in metres. 1051 1308 :param wavenumber: Air wavenumber ``k``. 1052 1309 :param characteristic_impedance: Characteristic impedance ``rho c``. 1053 - :return: The specimen :class:`TransferMatrix`. 1310 + :param frequency: Optional frequency vector ``f``, in hertz, retained on 1311 + the result so :meth:`TransferMatrix.plot` needs no arguments. 1312 + :param diameter: Optional tube diameter (circular) or largest section 1313 + dimension (rectangular/square), in metres, that activates the 1314 + plane-wave working-range check (6.2.3-6.2.5, 6.5.4). 1315 + :param shape: Tube cross-section, ``"circular"``, ``"rectangular"`` or 1316 + ``"square"``. 1317 + :return: The specimen :class:`TransferMatrix` (measurement context 1318 + retained on the result). 1054 1319 """ 1320 + canonical = _canonical_shape(shape) 1321 + if diameter is not None: 1322 + _warn_astm_plane_wave( 1323 + wavenumber, s1=s1, s2=s2, diameter=diameter, shape=canonical, 1324 + stacklevel=2, 1325 + ) 1055 1326 p0a, pda, u0a, uda = _face_from_loads( 1056 1327 load_a, l1=l1, s1=s1, l2=l2, s2=s2, thickness=thickness, 1057 1328 wavenumber=wavenumber, characteristic_impedance=characteristic_impedance, ··· 1071 1342 t12=np.asarray((p0b * pda - p0a * pdb) / den, dtype=np.complex128), 1072 1343 t21=np.asarray((u0a * udb - u0b * uda) / den, dtype=np.complex128), 1073 1344 t22=np.asarray((pda * u0b - pdb * u0a) / den, dtype=np.complex128), 1345 + **_measurement_context( 1346 + l1=l1, s1=s1, l2=l2, s2=s2, thickness=thickness, 1347 + diameter=diameter, shape=canonical, frequency=frequency, 1348 + characteristic_impedance=characteristic_impedance, 1349 + ), 1074 1350 ) 1075 1351 1076 1352 ··· 1084 1360 thickness: float, 1085 1361 wavenumber: ArrayLike, 1086 1362 characteristic_impedance: float, 1363 + frequency: ArrayLike | None = None, 1364 + diameter: float | None = None, 1365 + shape: str = "circular", 1087 1366 ) -> TransferMatrix: 1088 1367 """One-load transfer matrix, symmetric specimen (ASTM E2611-19, Eqs. (23)-(24)). 1089 1368 ··· 1103 1382 :param thickness: Specimen thickness ``d``, in metres. 1104 1383 :param wavenumber: Air wavenumber ``k``. 1105 1384 :param characteristic_impedance: Characteristic impedance ``rho c``. 1106 - :return: The specimen :class:`TransferMatrix`. 1385 + :param frequency: Optional frequency vector ``f``, in hertz, retained on 1386 + the result so :meth:`TransferMatrix.plot` needs no arguments. 1387 + :param diameter: Optional tube diameter (circular) or largest section 1388 + dimension (rectangular/square), in metres, that activates the 1389 + plane-wave working-range check (6.2.3-6.2.5, 6.5.4). 1390 + :param shape: Tube cross-section, ``"circular"``, ``"rectangular"`` or 1391 + ``"square"``. 1392 + :return: The specimen :class:`TransferMatrix` (measurement context 1393 + retained on the result). 1107 1394 """ 1395 + canonical = _canonical_shape(shape) 1396 + if diameter is not None: 1397 + _warn_astm_plane_wave( 1398 + wavenumber, s1=s1, s2=s2, diameter=diameter, shape=canonical, 1399 + stacklevel=2, 1400 + ) 1108 1401 p0, pd, u0, ud = _face_from_loads( 1109 1402 load, l1=l1, s1=s1, l2=l2, s2=s2, thickness=thickness, 1110 1403 wavenumber=wavenumber, characteristic_impedance=characteristic_impedance, ··· 1121 1414 t12=np.asarray((p0**2 - pd**2) / den, dtype=np.complex128), 1122 1415 t21=np.asarray((u0**2 - ud**2) / den, dtype=np.complex128), 1123 1416 t22=t_diag, 1417 + **_measurement_context( 1418 + l1=l1, s1=s1, l2=l2, s2=s2, thickness=thickness, 1419 + diameter=diameter, shape=canonical, frequency=frequency, 1420 + characteristic_impedance=characteristic_impedance, 1421 + ), 1124 1422 ) 1125 1423 1126 1424
+18 -3
site/src/content/docs/guides/materials.mdx
··· 582 582 print(round(f_l, 1), round(f_u, 1)) # 858.0 6864.0 583 583 ``` 584 584 585 + The same helper covers square and rectangular tubes: pass `shape="square"` or 586 + `shape="rectangular"` and the Eq. (3) factor replaces the circular one, with 587 + $d$ the maximum side length. The four-microphone branch keeps its own limits 588 + in `plane_wave_frequency_range_astm`: ASTM E2611 retains the unrounded 589 + circular constant ($f\,d < 0.586\,c$, 6.2.4.1), the same rectangular 590 + $f\,d < 0.500\,c$ (6.2.5), a slightly stricter spacing bound 591 + $f\,s < 0.40\,c$ (6.5.4) and a laxer low end at 1 % of the wavelength 592 + (6.2.3). Passing `diameter=` (and `shape=`) to `two_microphone_impedance`, 593 + `wave_decomposition` or the `transfer_matrix_*` solvers turns the matching 594 + check into an advisory warning, and for the Annex A attenuation estimate of a 595 + rectangular tube `hydraulic_diameter(width, height)` supplies the $4A/P$ 596 + diameter that `tube_attenuation_constant` expects. 597 + 585 598 **Standing-wave-ratio method (ISO 10534-1).** A probe traverses the standing wave 586 599 and reads the level difference $\Delta L = L_\text{max} - L_\text{min}$ between a 587 600 pressure maximum and the adjacent minimum. The standing-wave ratio, reflection ··· 739 752 It uses the same `ReportMetadata` container and rendering engine as the other 740 753 fiches. The measured frequency range is taken from the result; the applicable 741 754 descriptive and geometric `ReportMetadata` fields are `client`, `manufacturer`, 742 - `specimen`, `tube_diameter`, `mic_spacing`, `mounting`, `test_room`, 755 + `specimen`, `tube_diameter`, `tube_shape`, `mic_spacing`, `mounting`, `test_room`, 743 756 `test_date`, `temperature`, `pressure`, `measurement_standard`, `laboratory`, 744 757 `operator`, `report_id` and `notes` (`tube_diameter` and `mic_spacing` are given 745 758 in metres and printed in millimetres). The `requirement` field is ignored ··· 804 817 `(H1, H2, H3, H4)` of each load; its methods 805 818 (`transmission_loss`, `reflection_hard_backed`, `absorption_hard_backed`, 806 819 `characteristic_impedance_material`, `material_wavenumber`) then read off the 807 - ASTM E2611 quantities, and its `.plot(frequency, rho_c)` draws the 808 - transmission loss with the hard-backed absorption overlaid. The matrix does 820 + ASTM E2611 quantities, and its `.plot()` draws the transmission loss with 821 + the hard-backed absorption overlaid (a matrix built by the solvers retains 822 + $\rho c$ and, when supplied, the frequency vector, so only a hand-built 823 + matrix needs them as arguments). The matrix does 809 824 not have to come from a measurement: the 810 825 [multilayer solver](/phonometry/guides/porous-absorbers/) exposes the chain 811 826 matrix of any modelled stack in the same convention, so a predicted specimen
+19 -3
site/src/content/docs/es/guides/materials.mdx
··· 596 596 print(round(f_l, 1), round(f_u, 1)) # 858.0 6864.0 597 597 ``` 598 598 599 + El mismo ayudante cubre tubos cuadrados y rectangulares: con `shape="square"` 600 + o `shape="rectangular"` el factor de la Ec. (3) sustituye al circular, con 601 + $d$ el lado mayor. La rama de cuatro micrófonos mantiene sus propios límites 602 + en `plane_wave_frequency_range_astm`: ASTM E2611 conserva la constante 603 + circular sin redondear ($f\,d < 0{,}586\,c$, 6.2.4.1), el mismo 604 + $f\,d < 0{,}500\,c$ rectangular (6.2.5), una cota de separación algo más 605 + estricta $f\,s < 0{,}40\,c$ (6.5.4) y un extremo bajo más laxo en el 1 % de 606 + la longitud de onda (6.2.3). Pasar `diameter=` (y `shape=`) a 607 + `two_microphone_impedance`, `wave_decomposition` o los solucionadores 608 + `transfer_matrix_*` convierte la comprobación correspondiente en un aviso, y 609 + para la estimación de atenuación del anexo A en tubo rectangular 610 + `hydraulic_diameter(width, height)` proporciona el diámetro $4A/P$ que espera 611 + `tube_attenuation_constant`. 612 + 599 613 **Método de la razón de onda estacionaria (ISO 10534-1).** Una sonda recorre la 600 614 onda estacionaria y lee la diferencia de nivel $\Delta L = L_\text{max} - 601 615 L_\text{min}$ entre un máximo de presión y el mínimo adyacente. La razón de onda ··· 758 772 Usa el mismo contenedor `ReportMetadata` y el mismo motor que las demás fichas. 759 773 El rango de frecuencias medido se toma del resultado; los campos descriptivos y 760 774 geométricos de `ReportMetadata` que aplican aquí son `client`, `manufacturer`, 761 - `specimen`, `tube_diameter`, `mic_spacing`, `mounting`, `test_room`, 775 + `specimen`, `tube_diameter`, `tube_shape`, `mic_spacing`, `mounting`, `test_room`, 762 776 `test_date`, `temperature`, `pressure`, `measurement_standard`, `laboratory`, 763 777 `operator`, `report_id` y `notes` (`tube_diameter` y `mic_spacing` se dan en 764 778 metros y se imprimen en milímetros). El campo `requirement` se ignora (la ··· 824 838 (`transmission_loss`, `reflection_hard_backed`, 825 839 `absorption_hard_backed`, `characteristic_impedance_material`, 826 840 `material_wavenumber`) leen entonces las magnitudes de ASTM E2611, y su 827 - `.plot(frequency, rho_c)` dibuja la pérdida de transmisión con la absorción 828 - con respaldo rígido superpuesta. La matriz no tiene por qué venir de una 841 + `.plot()` dibuja la pérdida de transmisión con la absorción con respaldo 842 + rígido superpuesta (una matriz construida por los solucionadores retiene 843 + $\rho c$ y, si se aporta, el vector de frecuencias, de modo que solo una 844 + matriz construida a mano los necesita como argumentos). La matriz no tiene por qué venir de una 829 845 medición: el 830 846 [solucionador multicapa](/phonometry/es/guides/porous-absorbers/) expone la 831 847 matriz de cadena de cualquier apilado modelado en el mismo convenio, de modo
+3 -1
site/src/content/docs/reference/api/building/insulation.md
··· 820 820 requirement: float | None = None, 821 821 required_class: int | None = None, 822 822 notes: str | None = None, 823 + tube_shape: str | None = None, 823 824 ) 824 825 ``` 825 826 ··· 861 862 | `calibration` | Calibration traceability, as free text (calibrator, date and result of the most recent verification, the before/after field checks). Printed by the occupational noise-exposure fiche (ISO 9612:2009 Clause 15 c). | 862 863 | `tube_diameter` | Impedance-tube inner diameter `d` (circular tube) or maximum lateral dimension (rectangular tube), in metres. Printed by the impedance-tube fiche (ISO 10534-2), where it fixes the upper plane-wave cut-on frequency. | 863 864 | `mic_spacing` | Microphone spacing `s` between the two measurement positions of the impedance tube, in metres. Printed by the impedance-tube fiche (ISO 10534-2), where it bounds the working frequency range. | 865 + | `tube_shape` | Cross-section of the impedance tube: `"circular"`, `"rectangular"` or `"square"`. Printed by the impedance-tube fiche (ISO 10534-2) next to the tube diameter, which it qualifies (inner diameter for a circular tube, maximum lateral dimension otherwise). | 864 866 | `thickness` | Specimen thickness under the applied static load, in metres. Printed by the dynamic-stiffness fiche (EN 29052-1 / ISO 9052-1), where EN 29052-1:1992 Clause 9 b) requires reporting the thickness of the resilient layer under load; it is shown in millimetres. | 865 867 | `mounting` | Mounting condition of the specimen (e.g. the ISO 10140-1 mounting code or a short description). | 866 868 | `measurement_standard` | Measurement standard the spectrum was obtained under (e.g. `"ISO 10140-2"` or `"ISO 16283-1"`); it forms the report's standard-basis line together with the ISO 717 rating part. | ··· 876 878 877 879 | Exception | When | 878 880 | :--- | :--- | 879 - | ValueError | If a supplied dimension/mass/volume/pressure is not finite and strictly positive, a temperature or requirement is not finite, a relative humidity is outside 0..100 %, or a required class is not one of 0, 1, 2, or a position count is not a finite, positive integer. | 881 + | ValueError | If a supplied dimension/mass/volume/pressure is not finite and strictly positive, a temperature or requirement is not finite, a relative humidity is outside 0..100 %, a required class is not one of 0, 1, 2, a position count is not a finite, positive integer, or a tube shape is not one of `"circular"`, `"rectangular"`, `"square"`. | 880 882 881 883 ### ReportMetadata.is_empty() 882 884
+136 -17
site/src/content/docs/reference/api/materials/impedance-tube.md
··· 208 208 209 209 **Returns:** Tuple `(p0, pd, u0, ud)` of face pressures and velocities. 210 210 211 + ## hydraulic_diameter 212 + 213 + ```python 214 + hydraulic_diameter(width: float, height: float) -> float 215 + ``` 216 + 217 + Hydraulic diameter of a rectangular tube, `4 A / P` (ISO 10534-2, A.2.1.5). 218 + 219 + For a rectangular cross-section of side lengths `w` and `h` the ratio 220 + of four times the area to the perimeter reduces to 221 + `d_h = 2 w h / (w + h)`; a square tube gives `d_h` equal to the side 222 + length. This is the `d` the Eq. (A.18) attenuation estimate expects for 223 + rectangular tubes (see [`tube_attenuation_constant`](/phonometry/reference/api/materials/impedance-tube/#tube_attenuation_constant)). 224 + 225 + **Parameters** 226 + 227 + | Name | Description | 228 + | :--- | :--- | 229 + | `width` | Inner side length `w`, in metres. | 230 + | `height` | Inner side length `h`, in metres. | 231 + 232 + **Returns:** Hydraulic diameter `d_h = 4 A / P`, in metres. 233 + 211 234 ## ImpedanceTubeResult 212 235 213 236 ```python ··· 217 240 surface_impedance: Complex, 218 241 normalized_impedance: Complex, 219 242 absorption: Real, 243 + spacing: float | None = None, 244 + x1: float | None = None, 245 + diameter: float | None = None, 246 + shape: str | None = None, 220 247 ) 221 248 ``` 222 249 ··· 228 255 (Eq. (19)), `normalized_impedance` the ratio `Z / (rho c0)` (Eq. (19)) 229 256 and `absorption` the normal-incidence coefficient `alpha = 1 - |r|^2` 230 257 (Eq. (18)). 258 + 259 + The trailing fields retain the tube geometry the reduction was run with 260 + (microphone `spacing` `s`, distance `x1` from the sample to the 261 + farther microphone, tube `diameter` and cross-section `shape`, stored 262 + canonically as `"circular"`/`"rectangular"` - a `"square"` input is 263 + kept as `"rectangular"`); they default to `None` when not supplied to 264 + [`two_microphone_impedance`](/phonometry/reference/api/materials/impedance-tube/#two_microphone_impedance). 231 265 232 266 ### ImpedanceTubeResult.plot() 233 267 ··· 278 312 | Name | Description | 279 313 | :--- | :--- | 280 314 | `path` | Destination path of the PDF file. | 281 - | `metadata` | Optional [`ReportMetadata`](/phonometry/reference/api/building/insulation/#reportmetadata); `None` produces a body-and-disclaimer fiche whose header shows only the measured frequency range. The applicable descriptive/geometric fields are `client`, `manufacturer`, `specimen`, `tube_diameter`, `mic_spacing`, `mounting`, `test_room`, `test_date`, `temperature`, `pressure`, `measurement_standard`, `laboratory`, `operator`, `report_id` and `notes`. The `requirement` field is ignored (ISO 10534-2 has no verdict). | 315 + | `metadata` | Optional [`ReportMetadata`](/phonometry/reference/api/building/insulation/#reportmetadata); `None` produces a body-and-disclaimer fiche whose header shows only the measured frequency range. The applicable descriptive/geometric fields are `client`, `manufacturer`, `specimen`, `tube_diameter`, `tube_shape`, `mic_spacing`, `mounting`, `test_room`, `test_date`, `temperature`, `pressure`, `measurement_standard`, `laboratory`, `operator`, `report_id` and `notes`. The `requirement` field is ignored (ISO 10534-2 has no verdict). | 282 316 | `engine` | Rendering back end; only `"reportlab"` is supported. | 283 317 | `verbose` | When `True`, the value table inserts the reflection-factor magnitude `\|r\|` column. | 284 318 | `language` | Fiche language: `"en"` (default, English, decimal point) or `"es"` (Spanish, decimal comma). | ··· 384 418 | :--- | :--- | 385 419 | `spacing` | Microphone spacing `s`, in metres. | 386 420 | `speed_of_sound` | Speed of sound `c0`, in metres per second. | 387 - | `diameter` | Tube diameter (circular) or maximum lateral dimension (rectangular) `d`, in metres; `None` applies only the spacing bound. | 388 - | `shape` | `"circular"` or `"rectangular"`. | 421 + | `diameter` | Tube diameter (circular) or maximum lateral dimension (rectangular/square) `d`, in metres; `None` applies only the spacing bound. | 422 + | `shape` | `"circular"`, `"rectangular"` or `"square"` (a square tube is the rectangular bound with `d` the side length). | 423 + 424 + **Returns:** Tuple `(f_l, f_u)` of the lower and upper frequency limits, in Hz. 425 + 426 + ## plane_wave_frequency_range_astm 427 + 428 + ```python 429 + plane_wave_frequency_range_astm( 430 + spacing: float, 431 + speed_of_sound: float, 432 + *, 433 + diameter: float | None = None, 434 + shape: str = 'circular', 435 + ) -> tuple[float, float] 436 + ``` 437 + 438 + Working plane-wave frequency range `(f_l, f_u)` (ASTM E2611-19). 439 + 440 + The upper limit is the smaller of the microphone-spacing bound 441 + `s <= 0,8 c / (2 f_u)`, i.e. `f_u s < 0,40 c` (6.5.4), and, when the 442 + tube `diameter` is given, the cut-on bound `f_u < K c / d` with 443 + `K = 0,586` for a circular tube (6.2.4.1, Eq. (2)) or `K = 0,500` for 444 + a rectangular tube with `d` the largest section dimension (6.2.5). The 445 + lower limit follows 6.2.3: the spacing shall be greater than 1 % of the 446 + wavelength, i.e. `f_l = c / (100 s)`. 447 + 448 + With two different spacings `s1`/`s2`, call with the larger one for 449 + the upper bound and the smaller one for the lower bound (each bound is 450 + binding for every microphone pair). 451 + 452 + **Parameters** 453 + 454 + | Name | Description | 455 + | :--- | :--- | 456 + | `spacing` | Microphone spacing `s`, in metres. | 457 + | `speed_of_sound` | Speed of sound `c`, in metres per second. | 458 + | `diameter` | Tube diameter (circular) or largest section dimension (rectangular/square) `d`, in metres; `None` applies only the spacing bound. | 459 + | `shape` | `"circular"`, `"rectangular"` or `"square"`. | 389 460 390 461 **Returns:** Tuple `(f_l, f_u)` of the lower and upper frequency limits, in Hz. 391 462 ··· 595 666 thickness: float, 596 667 wavenumber: ArrayLike, 597 668 characteristic_impedance: float, 669 + frequency: ArrayLike | None = None, 670 + diameter: float | None = None, 671 + shape: str = 'circular', 598 672 ) -> TransferMatrix 599 673 ``` 600 674 ··· 622 696 | `thickness` | Specimen thickness `d`, in metres. | 623 697 | `wavenumber` | Air wavenumber `k`. | 624 698 | `characteristic_impedance` | Characteristic impedance `rho c`. | 699 + | `frequency` | Optional frequency vector `f`, in hertz, retained on the result so [`TransferMatrix.plot`](/phonometry/reference/api/materials/impedance-tube/#transfermatrixplot) needs no arguments. | 700 + | `diameter` | Optional tube diameter (circular) or largest section dimension (rectangular/square), in metres, that activates the plane-wave working-range check (6.2.3-6.2.5, 6.5.4). | 701 + | `shape` | Tube cross-section, `"circular"`, `"rectangular"` or `"square"`. | 625 702 626 - **Returns:** The specimen [`TransferMatrix`](/phonometry/reference/api/materials/impedance-tube/#transfermatrix). 703 + **Returns:** The specimen [`TransferMatrix`](/phonometry/reference/api/materials/impedance-tube/#transfermatrix) (measurement context retained on the result). 627 704 628 705 ## transfer_matrix_two_load 629 706 ··· 639 716 thickness: float, 640 717 wavenumber: ArrayLike, 641 718 characteristic_impedance: float, 719 + frequency: ArrayLike | None = None, 720 + diameter: float | None = None, 721 + shape: str = 'circular', 642 722 ) -> TransferMatrix 643 723 ``` 644 724 ··· 669 749 | `thickness` | Specimen thickness `d`, in metres. | 670 750 | `wavenumber` | Air wavenumber `k`. | 671 751 | `characteristic_impedance` | Characteristic impedance `rho c`. | 752 + | `frequency` | Optional frequency vector `f`, in hertz, retained on the result so [`TransferMatrix.plot`](/phonometry/reference/api/materials/impedance-tube/#transfermatrixplot) needs no arguments. | 753 + | `diameter` | Optional tube diameter (circular) or largest section dimension (rectangular/square), in metres, that activates the plane-wave working-range check (6.2.3-6.2.5, 6.5.4). | 754 + | `shape` | Tube cross-section, `"circular"`, `"rectangular"` or `"square"`. | 672 755 673 - **Returns:** The specimen [`TransferMatrix`](/phonometry/reference/api/materials/impedance-tube/#transfermatrix). 756 + **Returns:** The specimen [`TransferMatrix`](/phonometry/reference/api/materials/impedance-tube/#transfermatrix) (measurement context retained on the result). 674 757 675 758 ## TransferMatrix 676 759 677 760 ```python 678 - TransferMatrix(t11: Complex, t12: Complex, t21: Complex, t22: Complex) 761 + TransferMatrix( 762 + t11: Complex, 763 + t12: Complex, 764 + t21: Complex, 765 + t22: Complex, 766 + l1: float | None = None, 767 + s1: float | None = None, 768 + l2: float | None = None, 769 + s2: float | None = None, 770 + thickness: float | None = None, 771 + diameter: float | None = None, 772 + shape: str | None = None, 773 + frequency: Real | None = None, 774 + air_characteristic_impedance: float | None = None, 775 + ) 679 776 ``` 680 777 681 778 Acoustic transfer matrix `[[T11, T12], [T21, T22]]` (ASTM E2611-19). ··· 683 780 Relates the pressure and normal particle velocity across a specimen, 684 781 `[p; u]_{x=0} = T [p; u]_{x=d}` (Eq. (16)). Each entry is complex and 685 782 may be scalar or a per-frequency array of matching shape. 783 + 784 + The trailing fields retain the measurement context when the matrix comes 785 + out of [`transfer_matrix_two_load`](/phonometry/reference/api/materials/impedance-tube/#transfer_matrix_two_load) / [`transfer_matrix_one_load`](/phonometry/reference/api/materials/impedance-tube/#transfer_matrix_one_load) 786 + (tube geometry `l1`/`s1`/`l2`/`s2`, specimen `thickness`, tube 787 + `diameter` and canonical cross-section `shape`, the `frequency` 788 + vector when supplied to the solver, and the air 789 + `air_characteristic_impedance` `rho c`); all default to `None` so a 790 + hand-built matrix (for example [`air_layer_transfer_matrix`](/phonometry/reference/api/materials/impedance-tube/#air_layer_transfer_matrix)) is 791 + unchanged. 686 792 687 793 ### TransferMatrix.absorption_hard_backed() 688 794 ··· 746 852 747 853 ```python 748 854 TransferMatrix.plot( 749 - frequency: ArrayLike, 750 - characteristic_impedance: float, 855 + frequency: ArrayLike | None = None, 856 + characteristic_impedance: float | None = None, 751 857 ax: Axes | None = None, 752 858 *, 753 859 language: str = 'en', ··· 761 867 laboratory quotes: the normal-incidence transmission loss `TLn(f)` 762 868 (Eq. (26), the primary curve, left axis) and the hard-backed 763 869 absorption coefficient `alpha(f)` (Eq. (28), a muted companion on a 764 - 0..1 right axis). The matrix itself carries no frequency axis, so the 765 - `frequency` vector of the measurement (matching the shape of the 766 - entries) and the air characteristic impedance `rho c` must be 767 - supplied. 870 + 0..1 right axis). The four-pole entries carry no frequency axis of 871 + their own, so the plot needs the measurement's `frequency` vector 872 + (matching the shape of the entries) and the air characteristic 873 + impedance `rho c`. A matrix built by the solvers retains both 874 + (`self.frequency` / `self.air_characteristic_impedance`), so 875 + `plot()` takes no arguments there; only a hand-built matrix (for 876 + example [`air_layer_transfer_matrix`](/phonometry/reference/api/materials/impedance-tube/#air_layer_transfer_matrix)) must supply them. 768 877 769 878 Requires matplotlib (`pip install phonometry[plot]`); returns the 770 879 `Axes` of the transmission-loss curve. ··· 773 882 774 883 | Name | Description | 775 884 | :--- | :--- | 776 - | `frequency` | Frequency vector `f`, in hertz, matching the shape of the matrix entries. | 777 - | `characteristic_impedance` | Characteristic impedance `rho c` of the air in the tube, in rayls. | 885 + | `frequency` | Frequency vector `f`, in hertz, matching the shape of the matrix entries; `None` uses the stored `frequency`. | 886 + | `characteristic_impedance` | Characteristic impedance `rho c` of the air in the tube, in rayls; `None` uses the stored `air_characteristic_impedance`. | 778 887 | `ax` | Existing axes, or `None` to create a figure. | 779 888 | `language` | Plot language: `"en"` (default) or `"es"`. | 780 889 | `kwargs` | Forwarded to the transmission-loss `plot` call. | 781 890 782 891 **Returns:** The axes. 892 + 893 + **Raises** 894 + 895 + | Exception | When | 896 + | :--- | :--- | 897 + | ValueError | If `frequency` or `characteristic_impedance` is neither supplied nor stored on the matrix. | 783 898 784 899 ### TransferMatrix.reflection_hard_backed() 785 900 ··· 843 958 | :--- | :--- | 844 959 | `frequency` | Frequency `f`, in hertz (scalar or per band). | 845 960 | `speed_of_sound` | Speed of sound `c0`, in metres per second. | 846 - | `diameter` | Circular-tube diameter `d`, in metres, or the hydraulic diameter `4 * area / perimeter` for a rectangular tube. | 961 + | `diameter` | Circular-tube diameter `d`, in metres, or the hydraulic diameter `4 * area / perimeter` for a rectangular tube (see [`hydraulic_diameter`](/phonometry/reference/api/materials/impedance-tube/#hydraulic_diameter)). | 847 962 848 963 **Returns:** Attenuation constant `k0''`, in nepers per metre. 849 964 ··· 911 1026 | `characteristic_impedance` | Characteristic impedance `rho c0`, in rayls. | 912 1027 | `attenuation` | Optional tube attenuation constant `k0''`, in nepers/m (see [`tube_attenuation_constant`](/phonometry/reference/api/materials/impedance-tube/#tube_attenuation_constant)). | 913 1028 | `diameter` | Optional tube diameter/lateral dimension, in metres, that activates the plane-wave range check. | 914 - | `shape` | Tube cross-section, `"circular"` or `"rectangular"`. | 1029 + | `shape` | Tube cross-section, `"circular"`, `"rectangular"` or `"square"`. | 915 1030 916 - **Returns:** An [`ImpedanceTubeResult`](/phonometry/reference/api/materials/impedance-tube/#impedancetuberesult). 1031 + **Returns:** An [`ImpedanceTubeResult`](/phonometry/reference/api/materials/impedance-tube/#impedancetuberesult) (the tube geometry is retained on the result). 917 1032 918 1033 ## wave_decomposition 919 1034 ··· 929 1044 l2: float, 930 1045 s2: float, 931 1046 wavenumber: ArrayLike, 1047 + diameter: float | None = None, 1048 + shape: str = 'circular', 932 1049 ) -> tuple[Complex, Complex, Complex, Complex] 933 1050 ``` 934 1051 ··· 966 1083 | `l2` | Distance `l2` from the front reference plane, in metres. | 967 1084 | `s2` | Downstream microphone spacing `s2`, in metres. | 968 1085 | `wavenumber` | Air wavenumber `k` (real or complex), scalar or per band. | 1086 + | `diameter` | Optional tube diameter (circular) or largest section dimension (rectangular/square), in metres, that activates the plane-wave working-range check (6.2.3-6.2.5, 6.5.4). | 1087 + | `shape` | Tube cross-section, `"circular"`, `"rectangular"` or `"square"`. | 969 1088 970 1089 **Returns:** Tuple `(A, B, C, D)` of complex amplitudes.