diff --git a/docs/api-reference.md b/docs/api-reference.md
index b71082956..328bc7231 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -703,12 +703,14 @@ cycle and warn on use; they are removed in 4.0.
| `PrecisionFieldIndicators` | `dataclass` | **Annex B indicators (per band).**
• `ft`: temporal variability (= F1) or None
• `f_pi_unsigned`: F_pIn unsigned (= F2)
• `f_pi_signed`: F_pIn signed (= F3), ≥ unsigned
• `fs`: field non-uniformity FS (= F4) | `fi.f_pi_signed, fi.fs` |
| `precision_qualification` | `function` | **The five ISO 9614-3 Annex C acceptance criteria.**
• `indicators`: `PrecisionFieldIndicators`
• `scan_intensity_level_1`/`_2`: LIn per scan [dB] (criterion 1)
• `pressure_residual_index`: δpI0 [dB] (criterion 2, K = 10)
• `field_nonuniformity_1`/`_2`: FS per scan density (criterion 5)
• `frequencies` [Hz] or `repeatability_limit`: Table 1 limit s | `q = precision_qualification(fi, pressure_residual_index=18.0)`
• `PrecisionCriteria` |
| `PrecisionCriteria` | `dataclass` | **Annex C pass/fail per band.**
• `criterion_1`…`criterion_5`: bool arrays or None where not evaluable
• `qualified`: conjunction of criteria 1–4 or None | `q.qualified, q.criterion_2` |
-| `plane_wave_frequency_range` | `function` | **Working plane-wave range (f_l, f_u) (ISO 10534-2 §4.2–4.5).**
• `spacing` s [m]
• `speed_of_sound` c0 [m/s]
• `diameter` d [m] (Default: None → spacing bound only)
• `shape`: 'circular'/'rectangular' (Default: 'circular') | `plane_wave_frequency_range(0.05, 343.2, diameter=0.1) # (343.2, 1990.6)`
• [Hz] |
+| `plane_wave_frequency_range` | `function` | **Working plane-wave range (f_l, f_u) (ISO 10534-2 §4.2–4.5).**
• `spacing` s [m]
• `speed_of_sound` c0 [m/s]
• `diameter` d [m] (Default: None → spacing bound only)
• `shape`: 'circular'/'rectangular'/'square' (Default: 'circular') | `plane_wave_frequency_range(0.05, 343.2, diameter=0.1) # (343.2, 1990.6)`
• [Hz] |
+| `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).**
• `spacing` s [m]
• `speed_of_sound` c [m/s]
• `diameter` d [m] (Default: None → spacing bound only)
• `shape`: 'circular'/'rectangular'/'square' (Default: 'circular') | `plane_wave_frequency_range_astm(0.05, 343.2, diameter=0.1) # (68.6, 2011.2)`
• [Hz]; f_u s < 0.40 c, f_u d < 0.586 c (0.500 rectangular) |
| `speed_of_sound_iso` | `function` | **Speed of sound (ISO 10534-2 Eq. 5).**
• `temperature` T [K] | `speed_of_sound_iso(293.0) # 343.2`
• c0 = 343.2 √(T/293) [m/s] |
| `air_density_iso` | `function` | **Air density (ISO 10534-2 Eq. 7).**
• `temperature` T [K]
• `atmospheric_pressure` [kPa] (Default: 101.325) | `air_density_iso(293.0) # 1.186`
• ρ [kg/m³] |
| `speed_of_sound_astm` | `function` | **Speed of sound (ASTM E2611-19 Eq. 4).**
• `temperature` T [°C] | `speed_of_sound_astm(20.0) # 343.2`
• 20.047 √(273.15 + T) [m/s] |
| `air_density_astm` | `function` | **Air density (ASTM E2611-19 Eq. 5).**
• `temperature` T [°C]
• `atmospheric_pressure` P [kPa] (Default: 101.325) | `air_density_astm(20.0) # 1.202`
• ρ [kg/m³] |
| `characteristic_impedance` | `function` | **Characteristic impedance of air ρc.**
• `density` ρ [kg/m³]
• `speed_of_sound` c [m/s] | `characteristic_impedance(1.186, 343.2) # 407.0`
• [rayl] |
+| `hydraulic_diameter` | `function` | **Hydraulic diameter of a rectangular tube, 4A/P (ISO 10534-2 A.2.1.5).**
• `width` w [m]
• `height` h [m] | `hydraulic_diameter(0.08, 0.04) # 0.0533`
• d_h = 2wh/(w + h) [m]; feed to `tube_attenuation_constant` |
| `tube_attenuation_constant` | `function` | **Lower-bound tube attenuation k0″ (ISO 10534-2 Eq. A.18).**
• `frequency` f [Hz]
• `speed_of_sound` c0 [m/s]
• `diameter` d [m] (hydraulic for rectangular) | `tube_attenuation_constant(1000.0, 343.2, 0.1) # 0.0179`
• [Np/m] |
| `tube_wavenumber` | `function` | **Complex wavenumber k0 = k0′ − j k0″ (ISO 10534-2 §2.6).**
• `frequency` f [Hz]
• `speed_of_sound` c0 [m/s]
• `attenuation`: k0″ [Np/m] (Default: None → lossless) | `tube_wavenumber(1000.0, 343.2, attenuation=0.018)`
• 18.308 − 0.018j [1/m] |
| `mic_calibration_factor` | `function` | **Microphone-mismatch calibration factor Hc (ISO 10534-2 Eq. 10).**
• `h12_config1`: H12 in the standard configuration
• `h12_config2`: H12 with the microphones interchanged | `hc = mic_calibration_factor(h12_a, h12_b)`
• Hc = √(H12ᴵ/H12ᴵᴵ) (complex) |
@@ -719,19 +721,19 @@ cycle and warn on use; they are removed in 4.0.
| `normalized_surface_impedance` | `function` | **Normalised surface impedance Z/(ρc0) (Eq. 19).**
• `reflection`: complex r | `normalized_surface_impedance(0.5) # 3.0` |
| `normalized_surface_admittance` | `function` | **Normalised surface admittance Gρc0 (Eq. 20).**
• `reflection`: complex r | `normalized_surface_admittance(0.5) # 0.333`
• (1 − r)/(1 + r) |
| `two_microphone_impedance` | `function` | **Full two-microphone reduction (ISO 10534-2 Clause 7).**
• `h12`: corrected transfer function
• `frequency` f [Hz]
• `spacing` s [m], `x1` [m]
• `speed_of_sound` c0 [m/s], `characteristic_impedance` ρc0 [rayl]
• `attenuation`: k0″ [Np/m] (Default: None)
• `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)`
• `ImpedanceTubeResult` |
-| `ImpedanceTubeResult` | `dataclass` | **Two-microphone result per frequency.**
• `frequency` [Hz]
• `reflection`: complex r (Eq. 17)
• `surface_impedance` [rayl], `normalized_impedance` (Eq. 19)
• `absorption`: α = 1 − \|r\|² (Eq. 18) | `res.absorption, res.normalized_impedance` |
+| `ImpedanceTubeResult` | `dataclass` | **Two-microphone result per frequency.**
• `frequency` [Hz]
• `reflection`: complex r (Eq. 17)
• `surface_impedance` [rayl], `normalized_impedance` (Eq. 19)
• `absorption`: α = 1 − \|r\|² (Eq. 18)
• Retains `spacing`, `x1`, `diameter`, `shape` when supplied | `res.absorption, res.normalized_impedance` |
| `ImpedanceTubeWarning` | `warning class` | **ISO 10534-2 advisory.**
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)` |
| `standing_wave_ratio_from_level` | `function` | **Standing-wave ratio from ΔL (ISO 10534-1 Eq. 15).**
• `level_difference`: Lmax − Lmin [dB] | `standing_wave_ratio_from_level(20.0) # 10.0`
• s = 10^(ΔL/20) |
| `standing_wave_reflection_magnitude` | `function` | **Reflection magnitude from the SWR (Eq. 14).**
• `swr`: s ≥ 1 | `standing_wave_reflection_magnitude(10.0) # 0.818`
• \|r\| = (s − 1)/(s + 1) |
| `standing_wave_reflection` | `function` | **Complex reflection factor from the standing wave (Eqs. 17–23).**
• `swr`: s
• `first_min_distance`: x_min1 [m]
• `wavelength`: λ0 [m] | `standing_wave_reflection(10.0, 0.05, 0.4) # -0.818j`
• phase φ = π(4x_min1/λ0 − 1) |
| `standing_wave_absorption` | `function` | **Absorption from the SWR (Eqs. 9 + 14).**
• `swr`: s ≥ 1 | `standing_wave_absorption(10.0) # 0.331`
• α = 4s/(s + 1)² |
| `standing_wave_normalized_impedance` | `function` | **Normalised impedance from the standing wave (Eqs. 24–26).**
• `swr`, `first_min_distance` [m], `wavelength` [m] | `standing_wave_normalized_impedance(10.0, 0.05, 0.4)`
• 0.198 − 0.98j |
-| `wave_decomposition` | `function` | **Decompose the field into (A, B, C, D) (ASTM E2611-19 Eqs. 17–20).**
• `h1`…`h4`: microphone transfer functions
• `l1`, `s1`, `l2`, `s2`: geometry [m]
• `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)`
• Forward/backward amplitudes, both sides |
+| `wave_decomposition` | `function` | **Decompose the field into (A, B, C, D) (ASTM E2611-19 Eqs. 17–20).**
• `h1`…`h4`: microphone transfer functions
• `l1`, `s1`, `l2`, `s2`: geometry [m]
• `wavenumber` k
• `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)`
• Forward/backward amplitudes, both sides |
| `face_quantities` | `function` | **Face pressures and velocities (Eq. 21).**
• `a`, `b`, `c`, `d`: wave amplitudes
• `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)` |
-| `transfer_matrix_two_load` | `function` | **Two-load transfer matrix (ASTM E2611-19 Eqs. 17–22).**
• `load_a`, `load_b`: (H1, H2, H3, H4) per termination
• `l1`, `s1`, `l2`, `s2`, `thickness` [m]
• `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)`
• `TransferMatrix` |
-| `transfer_matrix_one_load` | `function` | **One-load transfer matrix, symmetric specimen (Eqs. 23–24).**
• `load`: (H1, H2, H3, H4)
• 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)`
• Requires reciprocity + symmetry |
+| `transfer_matrix_two_load` | `function` | **Two-load transfer matrix (ASTM E2611-19 Eqs. 17–22).**
• `load_a`, `load_b`: (H1, H2, H3, H4) per termination
• `l1`, `s1`, `l2`, `s2`, `thickness` [m]
• `wavenumber`, `characteristic_impedance`
• `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)`
• `TransferMatrix` |
+| `transfer_matrix_one_load` | `function` | **One-load transfer matrix, symmetric specimen (Eqs. 23–24).**
• `load`: (H1, H2, H3, H4)
• 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)`
• Requires reciprocity + symmetry |
| `air_layer_transfer_matrix` | `function` | **Analytic loss-free air-layer matrix (validation reference).**
• `wavenumber` k
• `thickness` d [m]
• `characteristic_impedance` ρc [rayl] | `T = air_layer_transfer_matrix(k, 0.05, 407.0)`
• det(T) = 1, T11 = T22 |
-| `TransferMatrix` | `dataclass` | **Acoustic transfer matrix [[T11, T12], [T21, T22]] (ASTM E2611-19 Eq. 16).**
• `t11`, `t12`, `t21`, `t22`: complex, scalar or per frequency | `T.t11, T.t12` |
+| `TransferMatrix` | `dataclass` | **Acoustic transfer matrix [[T11, T12], [T21, T22]] (ASTM E2611-19 Eq. 16).**
• `t11`, `t12`, `t21`, `t22`: complex, scalar or per frequency
• Solver-built matrices retain `l1`/`s1`/`l2`/`s2`, `thickness`, `diameter`, `shape`, `frequency`, `air_characteristic_impedance` (so `.plot()` needs no arguments) | `T.t11, T.t12` |
| `airflow_resistance` | `function` | **Airflow resistance R = Δp/qv (ISO 9053-1:2018 §3.1).**
• `pressure_drop` Δp [Pa]
• `volume_flow_rate` qv [m³/s] | `airflow_resistance(2.0, 0.0005) # 4000.0`
• [Pa·s/m³] |
| `specific_airflow_resistance` | `function` | **Specific airflow resistance Rs (§3.2).**
• `resistance` R + `area` A, **or** `pressure_drop` Δp + `velocity` u | `specific_airflow_resistance(4000.0, 0.01) # 40.0`
• Rs = R·A = Δp/u [Pa·s/m] |
| `airflow_resistivity` | `function` | **Airflow resistivity σ = Rs/d (§3.3).**
• `specific_resistance` Rs [Pa·s/m]
• `thickness` d [m] | `airflow_resistivity(40.0, 0.05) # 800.0`
• [Pa·s/m²] |
diff --git a/docs/materials.md b/docs/materials.md
index e8b369c50..0684e379f 100644
--- a/docs/materials.md
+++ b/docs/materials.md
@@ -501,6 +501,19 @@ f_l, f_u = materials.plane_wave_frequency_range(0.020, 343.2, diameter=0.029)
print(round(f_l, 1), round(f_u, 1)) # 858.0 6864.0
```
+The same helper covers square and rectangular tubes: pass `shape="square"` or
+`shape="rectangular"` and the Eq. (3) factor replaces the circular one, with
+$d$ the maximum side length. The four-microphone branch keeps its own limits
+in `plane_wave_frequency_range_astm`: ASTM E2611 retains the unrounded
+circular constant ($f\,d < 0.586\,c$, 6.2.4.1), the same rectangular
+$f\,d < 0.500\,c$ (6.2.5), a slightly stricter spacing bound
+$f\,s < 0.40\,c$ (6.5.4) and a laxer low end at 1 % of the wavelength
+(6.2.3). Passing `diameter=` (and `shape=`) to `two_microphone_impedance`,
+`wave_decomposition` or the `transfer_matrix_*` solvers turns the matching
+check into an advisory warning, and for the Annex A attenuation estimate of a
+rectangular tube `hydraulic_diameter(width, height)` supplies the $4A/P$
+diameter that `tube_attenuation_constant` expects.
+
**Standing-wave-ratio method (ISO 10534-1).** A probe traverses the standing wave
and reads the level difference $\Delta L = L_\text{max} - L_\text{min}$ between a
pressure maximum and the adjacent minimum. The standing-wave ratio, reflection
@@ -654,7 +667,7 @@ random-incidence $\alpha_s$/$\alpha_w$ of ISO 354 / ISO 11654. Setting
It uses the same `ReportMetadata` container and rendering engine as the other
fiches. The measured frequency range is taken from the result; the applicable
descriptive and geometric `ReportMetadata` fields are `client`, `manufacturer`,
-`specimen`, `tube_diameter`, `mic_spacing`, `mounting`, `test_room`,
+`specimen`, `tube_diameter`, `tube_shape`, `mic_spacing`, `mounting`, `test_room`,
`test_date`, `temperature`, `pressure`, `measurement_standard`, `laboratory`,
`operator`, `report_id` and `notes` (`tube_diameter` and `mic_spacing` are given
in metres and printed in millimetres). The `requirement` field is ignored
@@ -719,8 +732,10 @@ print(np.round(tm.transmission_loss(rho_c), 6)) # [0. 0. 0.]
`(H1, H2, H3, H4)` of each load; its methods
(`transmission_loss`, `reflection_hard_backed`, `absorption_hard_backed`,
`characteristic_impedance_material`, `material_wavenumber`) then read off the
-ASTM E2611 quantities, and its `.plot(frequency, rho_c)` draws the
-transmission loss with the hard-backed absorption overlaid. The matrix does
+ASTM E2611 quantities, and its `.plot()` draws the transmission loss with
+the hard-backed absorption overlaid (a matrix built by the solvers retains
+$\rho c$ and, when supplied, the frequency vector, so only a hand-built
+matrix needs them as arguments). The matrix does
not have to come from a measurement: the
[multilayer solver](porous-absorbers.md) exposes the chain matrix of any
modelled stack in the same convention, so a predicted specimen can be read
diff --git a/site/src/content/docs/es/guides/materials.mdx b/site/src/content/docs/es/guides/materials.mdx
index 1973965b3..bd1510fac 100644
--- a/site/src/content/docs/es/guides/materials.mdx
+++ b/site/src/content/docs/es/guides/materials.mdx
@@ -596,6 +596,20 @@ f_l, f_u = materials.plane_wave_frequency_range(0.020, 343.2, diameter=0.029)
print(round(f_l, 1), round(f_u, 1)) # 858.0 6864.0
```
+El mismo ayudante cubre tubos cuadrados y rectangulares: con `shape="square"`
+o `shape="rectangular"` el factor de la Ec. (3) sustituye al circular, con
+$d$ el lado mayor. La rama de cuatro micrófonos mantiene sus propios límites
+en `plane_wave_frequency_range_astm`: ASTM E2611 conserva la constante
+circular sin redondear ($f\,d < 0{,}586\,c$, 6.2.4.1), el mismo
+$f\,d < 0{,}500\,c$ rectangular (6.2.5), una cota de separación algo más
+estricta $f\,s < 0{,}40\,c$ (6.5.4) y un extremo bajo más laxo en el 1 % de
+la longitud de onda (6.2.3). Pasar `diameter=` (y `shape=`) a
+`two_microphone_impedance`, `wave_decomposition` o los solucionadores
+`transfer_matrix_*` convierte la comprobación correspondiente en un aviso, y
+para la estimación de atenuación del anexo A en tubo rectangular
+`hydraulic_diameter(width, height)` proporciona el diámetro $4A/P$ que espera
+`tube_attenuation_constant`.
+
**Método de la razón de onda estacionaria (ISO 10534-1).** Una sonda recorre la
onda estacionaria y lee la diferencia de nivel $\Delta L = L_\text{max} -
L_\text{min}$ entre un máximo de presión y el mínimo adyacente. La razón de onda
@@ -758,7 +772,7 @@ se añade la columna del módulo del factor de reflexión $|r|$.
Usa el mismo contenedor `ReportMetadata` y el mismo motor que las demás fichas.
El rango de frecuencias medido se toma del resultado; los campos descriptivos y
geométricos de `ReportMetadata` que aplican aquí son `client`, `manufacturer`,
-`specimen`, `tube_diameter`, `mic_spacing`, `mounting`, `test_room`,
+`specimen`, `tube_diameter`, `tube_shape`, `mic_spacing`, `mounting`, `test_room`,
`test_date`, `temperature`, `pressure`, `measurement_standard`, `laboratory`,
`operator`, `report_id` y `notes` (`tube_diameter` y `mic_spacing` se dan en
metros y se imprimen en milímetros). El campo `requirement` se ignora (la
@@ -824,8 +838,10 @@ micrófonos `(H1, H2, H3, H4)` medidas en cada carga; sus métodos
(`transmission_loss`, `reflection_hard_backed`,
`absorption_hard_backed`, `characteristic_impedance_material`,
`material_wavenumber`) leen entonces las magnitudes de ASTM E2611, y su
-`.plot(frequency, rho_c)` dibuja la pérdida de transmisión con la absorción
-con respaldo rígido superpuesta. La matriz no tiene por qué venir de una
+`.plot()` dibuja la pérdida de transmisión con la absorción con respaldo
+rígido superpuesta (una matriz construida por los solucionadores retiene
+$\rho c$ y, si se aporta, el vector de frecuencias, de modo que solo una
+matriz construida a mano los necesita como argumentos). La matriz no tiene por qué venir de una
medición: el
[solucionador multicapa](/phonometry/es/guides/porous-absorbers/) expone la
matriz de cadena de cualquier apilado modelado en el mismo convenio, de modo
diff --git a/site/src/content/docs/guides/materials.mdx b/site/src/content/docs/guides/materials.mdx
index 9567fc02a..b82aba623 100644
--- a/site/src/content/docs/guides/materials.mdx
+++ b/site/src/content/docs/guides/materials.mdx
@@ -582,6 +582,19 @@ f_l, f_u = materials.plane_wave_frequency_range(0.020, 343.2, diameter=0.029)
print(round(f_l, 1), round(f_u, 1)) # 858.0 6864.0
```
+The same helper covers square and rectangular tubes: pass `shape="square"` or
+`shape="rectangular"` and the Eq. (3) factor replaces the circular one, with
+$d$ the maximum side length. The four-microphone branch keeps its own limits
+in `plane_wave_frequency_range_astm`: ASTM E2611 retains the unrounded
+circular constant ($f\,d < 0.586\,c$, 6.2.4.1), the same rectangular
+$f\,d < 0.500\,c$ (6.2.5), a slightly stricter spacing bound
+$f\,s < 0.40\,c$ (6.5.4) and a laxer low end at 1 % of the wavelength
+(6.2.3). Passing `diameter=` (and `shape=`) to `two_microphone_impedance`,
+`wave_decomposition` or the `transfer_matrix_*` solvers turns the matching
+check into an advisory warning, and for the Annex A attenuation estimate of a
+rectangular tube `hydraulic_diameter(width, height)` supplies the $4A/P$
+diameter that `tube_attenuation_constant` expects.
+
**Standing-wave-ratio method (ISO 10534-1).** A probe traverses the standing wave
and reads the level difference $\Delta L = L_\text{max} - L_\text{min}$ between a
pressure maximum and the adjacent minimum. The standing-wave ratio, reflection
@@ -739,7 +752,7 @@ random-incidence $\alpha_s$/$\alpha_w$ of ISO 354 / ISO 11654. Setting
It uses the same `ReportMetadata` container and rendering engine as the other
fiches. The measured frequency range is taken from the result; the applicable
descriptive and geometric `ReportMetadata` fields are `client`, `manufacturer`,
-`specimen`, `tube_diameter`, `mic_spacing`, `mounting`, `test_room`,
+`specimen`, `tube_diameter`, `tube_shape`, `mic_spacing`, `mounting`, `test_room`,
`test_date`, `temperature`, `pressure`, `measurement_standard`, `laboratory`,
`operator`, `report_id` and `notes` (`tube_diameter` and `mic_spacing` are given
in metres and printed in millimetres). The `requirement` field is ignored
@@ -804,8 +817,10 @@ print(np.round(tm.transmission_loss(rho_c), 6)) # [0. 0. 0.]
`(H1, H2, H3, H4)` of each load; its methods
(`transmission_loss`, `reflection_hard_backed`, `absorption_hard_backed`,
`characteristic_impedance_material`, `material_wavenumber`) then read off the
-ASTM E2611 quantities, and its `.plot(frequency, rho_c)` draws the
-transmission loss with the hard-backed absorption overlaid. The matrix does
+ASTM E2611 quantities, and its `.plot()` draws the transmission loss with
+the hard-backed absorption overlaid (a matrix built by the solvers retains
+$\rho c$ and, when supplied, the frequency vector, so only a hand-built
+matrix needs them as arguments). The matrix does
not have to come from a measurement: the
[multilayer solver](/phonometry/guides/porous-absorbers/) exposes the chain
matrix of any modelled stack in the same convention, so a predicted specimen
diff --git a/site/src/content/docs/reference/api/building/insulation.md b/site/src/content/docs/reference/api/building/insulation.md
index 5cfb1496b..baaa0aaad 100644
--- a/site/src/content/docs/reference/api/building/insulation.md
+++ b/site/src/content/docs/reference/api/building/insulation.md
@@ -820,6 +820,7 @@ ReportMetadata(
requirement: float | None = None,
required_class: int | None = None,
notes: str | None = None,
+ tube_shape: str | None = None,
)
```
@@ -861,6 +862,7 @@ within 0..100 %. A violation raises `ValueError`.
| `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). |
| `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. |
| `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. |
+| `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). |
| `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. |
| `mounting` | Mounting condition of the specimen (e.g. the ISO 10140-1 mounting code or a short description). |
| `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,7 +878,7 @@ within 0..100 %. A violation raises `ValueError`.
| Exception | When |
| :--- | :--- |
-| 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. |
+| 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"`. |
### ReportMetadata.is_empty()
diff --git a/site/src/content/docs/reference/api/materials/impedance-tube.md b/site/src/content/docs/reference/api/materials/impedance-tube.md
index 0a78b2f7b..d8080dfe1 100644
--- a/site/src/content/docs/reference/api/materials/impedance-tube.md
+++ b/site/src/content/docs/reference/api/materials/impedance-tube.md
@@ -208,6 +208,29 @@ Face pressures and particle velocities (ASTM E2611-19, Eq. (21)).
**Returns:** Tuple `(p0, pd, u0, ud)` of face pressures and velocities.
+## hydraulic_diameter
+
+```python
+hydraulic_diameter(width: float, height: float) -> float
+```
+
+Hydraulic diameter of a rectangular tube, `4 A / P` (ISO 10534-2, A.2.1.5).
+
+For a rectangular cross-section of side lengths `w` and `h` the ratio
+of four times the area to the perimeter reduces to
+`d_h = 2 w h / (w + h)`; a square tube gives `d_h` equal to the side
+length. This is the `d` the Eq. (A.18) attenuation estimate expects for
+rectangular tubes (see [`tube_attenuation_constant`](/phonometry/reference/api/materials/impedance-tube/#tube_attenuation_constant)).
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `width` | Inner side length `w`, in metres. |
+| `height` | Inner side length `h`, in metres. |
+
+**Returns:** Hydraulic diameter `d_h = 4 A / P`, in metres.
+
## ImpedanceTubeResult
```python
@@ -217,6 +240,10 @@ ImpedanceTubeResult(
surface_impedance: Complex,
normalized_impedance: Complex,
absorption: Real,
+ spacing: float | None = None,
+ x1: float | None = None,
+ diameter: float | None = None,
+ shape: str | None = None,
)
```
@@ -229,6 +256,13 @@ reflection factor `r` at the sample surface (Eq. (17)),
and `absorption` the normal-incidence coefficient `alpha = 1 - |r|^2`
(Eq. (18)).
+The trailing fields retain the tube geometry the reduction was run with
+(microphone `spacing` `s`, distance `x1` from the sample to the
+farther microphone, tube `diameter` and cross-section `shape`, stored
+canonically as `"circular"`/`"rectangular"` - a `"square"` input is
+kept as `"rectangular"`); they default to `None` when not supplied to
+[`two_microphone_impedance`](/phonometry/reference/api/materials/impedance-tube/#two_microphone_impedance).
+
### ImpedanceTubeResult.plot()
```python
@@ -278,7 +312,7 @@ to the normal-incidence coefficient reported here).
| Name | Description |
| :--- | :--- |
| `path` | Destination path of the PDF file. |
-| `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). |
+| `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). |
| `engine` | Rendering back end; only `"reportlab"` is supported. |
| `verbose` | When `True`, the value table inserts the reflection-factor magnitude `\|r\|` column. |
| `language` | Fiche language: `"en"` (default, English, decimal point) or `"es"` (Spanish, decimal comma). |
@@ -384,8 +418,45 @@ Clause 4.2 guideline that the spacing exceed 5 % of the wavelength, i.e.
| :--- | :--- |
| `spacing` | Microphone spacing `s`, in metres. |
| `speed_of_sound` | Speed of sound `c0`, in metres per second. |
-| `diameter` | Tube diameter (circular) or maximum lateral dimension (rectangular) `d`, in metres; `None` applies only the spacing bound. |
-| `shape` | `"circular"` or `"rectangular"`. |
+| `diameter` | Tube diameter (circular) or maximum lateral dimension (rectangular/square) `d`, in metres; `None` applies only the spacing bound. |
+| `shape` | `"circular"`, `"rectangular"` or `"square"` (a square tube is the rectangular bound with `d` the side length). |
+
+**Returns:** Tuple `(f_l, f_u)` of the lower and upper frequency limits, in Hz.
+
+## plane_wave_frequency_range_astm
+
+```python
+plane_wave_frequency_range_astm(
+ spacing: float,
+ speed_of_sound: float,
+ *,
+ diameter: float | None = None,
+ shape: str = 'circular',
+) -> tuple[float, float]
+```
+
+Working plane-wave frequency range `(f_l, f_u)` (ASTM E2611-19).
+
+The upper limit is the smaller of the microphone-spacing bound
+`s <= 0,8 c / (2 f_u)`, i.e. `f_u s < 0,40 c` (6.5.4), and, when the
+tube `diameter` is given, the cut-on bound `f_u < K c / d` with
+`K = 0,586` for a circular tube (6.2.4.1, Eq. (2)) or `K = 0,500` for
+a rectangular tube with `d` the largest section dimension (6.2.5). The
+lower limit follows 6.2.3: the spacing shall be greater than 1 % of the
+wavelength, i.e. `f_l = c / (100 s)`.
+
+With two different spacings `s1`/`s2`, call with the larger one for
+the upper bound and the smaller one for the lower bound (each bound is
+binding for every microphone pair).
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `spacing` | Microphone spacing `s`, in metres. |
+| `speed_of_sound` | Speed of sound `c`, in metres per second. |
+| `diameter` | Tube diameter (circular) or largest section dimension (rectangular/square) `d`, in metres; `None` applies only the spacing bound. |
+| `shape` | `"circular"`, `"rectangular"` or `"square"`. |
**Returns:** Tuple `(f_l, f_u)` of the lower and upper frequency limits, in Hz.
@@ -595,6 +666,9 @@ transfer_matrix_one_load(
thickness: float,
wavenumber: ArrayLike,
characteristic_impedance: float,
+ frequency: ArrayLike | None = None,
+ diameter: float | None = None,
+ shape: str = 'circular',
) -> TransferMatrix
```
@@ -622,8 +696,11 @@ T21 = (u0^2 - ud^2) / DEN
| `thickness` | Specimen thickness `d`, in metres. |
| `wavenumber` | Air wavenumber `k`. |
| `characteristic_impedance` | Characteristic impedance `rho c`. |
+| `frequency` | Optional frequency vector `f`, in hertz, retained on the result so [`TransferMatrix.plot`](/phonometry/reference/api/materials/impedance-tube/#transfermatrixplot) needs no arguments. |
+| `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). |
+| `shape` | Tube cross-section, `"circular"`, `"rectangular"` or `"square"`. |
-**Returns:** The specimen [`TransferMatrix`](/phonometry/reference/api/materials/impedance-tube/#transfermatrix).
+**Returns:** The specimen [`TransferMatrix`](/phonometry/reference/api/materials/impedance-tube/#transfermatrix) (measurement context retained on the result).
## transfer_matrix_two_load
@@ -639,6 +716,9 @@ transfer_matrix_two_load(
thickness: float,
wavenumber: ArrayLike,
characteristic_impedance: float,
+ frequency: ArrayLike | None = None,
+ diameter: float | None = None,
+ shape: str = 'circular',
) -> TransferMatrix
```
@@ -669,13 +749,30 @@ T22 = (p_da u0b - p_db u0a) / DEN
| `thickness` | Specimen thickness `d`, in metres. |
| `wavenumber` | Air wavenumber `k`. |
| `characteristic_impedance` | Characteristic impedance `rho c`. |
+| `frequency` | Optional frequency vector `f`, in hertz, retained on the result so [`TransferMatrix.plot`](/phonometry/reference/api/materials/impedance-tube/#transfermatrixplot) needs no arguments. |
+| `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). |
+| `shape` | Tube cross-section, `"circular"`, `"rectangular"` or `"square"`. |
-**Returns:** The specimen [`TransferMatrix`](/phonometry/reference/api/materials/impedance-tube/#transfermatrix).
+**Returns:** The specimen [`TransferMatrix`](/phonometry/reference/api/materials/impedance-tube/#transfermatrix) (measurement context retained on the result).
## TransferMatrix
```python
-TransferMatrix(t11: Complex, t12: Complex, t21: Complex, t22: Complex)
+TransferMatrix(
+ t11: Complex,
+ t12: Complex,
+ t21: Complex,
+ t22: Complex,
+ l1: float | None = None,
+ s1: float | None = None,
+ l2: float | None = None,
+ s2: float | None = None,
+ thickness: float | None = None,
+ diameter: float | None = None,
+ shape: str | None = None,
+ frequency: Real | None = None,
+ air_characteristic_impedance: float | None = None,
+)
```
Acoustic transfer matrix `[[T11, T12], [T21, T22]]` (ASTM E2611-19).
@@ -684,6 +781,15 @@ Relates the pressure and normal particle velocity across a specimen,
`[p; u]_{x=0} = T [p; u]_{x=d}` (Eq. (16)). Each entry is complex and
may be scalar or a per-frequency array of matching shape.
+The trailing fields retain the measurement context when the matrix comes
+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)
+(tube geometry `l1`/`s1`/`l2`/`s2`, specimen `thickness`, tube
+`diameter` and canonical cross-section `shape`, the `frequency`
+vector when supplied to the solver, and the air
+`air_characteristic_impedance` `rho c`); all default to `None` so a
+hand-built matrix (for example [`air_layer_transfer_matrix`](/phonometry/reference/api/materials/impedance-tube/#air_layer_transfer_matrix)) is
+unchanged.
+
### TransferMatrix.absorption_hard_backed()
```python
@@ -746,8 +852,8 @@ Propagation wavenumber inside the material (ASTM E2611-19, Eq. (29)).
```python
TransferMatrix.plot(
- frequency: ArrayLike,
- characteristic_impedance: float,
+ frequency: ArrayLike | None = None,
+ characteristic_impedance: float | None = None,
ax: Axes | None = None,
*,
language: str = 'en',
@@ -761,10 +867,13 @@ Reads the four-pole entries out as the two ASTM E2611-19 spectra a
laboratory quotes: the normal-incidence transmission loss `TLn(f)`
(Eq. (26), the primary curve, left axis) and the hard-backed
absorption coefficient `alpha(f)` (Eq. (28), a muted companion on a
-0..1 right axis). The matrix itself carries no frequency axis, so the
-`frequency` vector of the measurement (matching the shape of the
-entries) and the air characteristic impedance `rho c` must be
-supplied.
+0..1 right axis). The four-pole entries carry no frequency axis of
+their own, so the plot needs the measurement's `frequency` vector
+(matching the shape of the entries) and the air characteristic
+impedance `rho c`. A matrix built by the solvers retains both
+(`self.frequency` / `self.air_characteristic_impedance`), so
+`plot()` takes no arguments there; only a hand-built matrix (for
+example [`air_layer_transfer_matrix`](/phonometry/reference/api/materials/impedance-tube/#air_layer_transfer_matrix)) must supply them.
Requires matplotlib (`pip install phonometry[plot]`); returns the
`Axes` of the transmission-loss curve.
@@ -773,14 +882,20 @@ Requires matplotlib (`pip install phonometry[plot]`); returns the
| Name | Description |
| :--- | :--- |
-| `frequency` | Frequency vector `f`, in hertz, matching the shape of the matrix entries. |
-| `characteristic_impedance` | Characteristic impedance `rho c` of the air in the tube, in rayls. |
+| `frequency` | Frequency vector `f`, in hertz, matching the shape of the matrix entries; `None` uses the stored `frequency`. |
+| `characteristic_impedance` | Characteristic impedance `rho c` of the air in the tube, in rayls; `None` uses the stored `air_characteristic_impedance`. |
| `ax` | Existing axes, or `None` to create a figure. |
| `language` | Plot language: `"en"` (default) or `"es"`. |
| `kwargs` | Forwarded to the transmission-loss `plot` call. |
**Returns:** The axes.
+**Raises**
+
+| Exception | When |
+| :--- | :--- |
+| ValueError | If `frequency` or `characteristic_impedance` is neither supplied nor stored on the matrix. |
+
### TransferMatrix.reflection_hard_backed()
```python
@@ -843,7 +958,7 @@ porous-wall and object losses and is therefore a lower limit (Clause A.2.1.5).
| :--- | :--- |
| `frequency` | Frequency `f`, in hertz (scalar or per band). |
| `speed_of_sound` | Speed of sound `c0`, in metres per second. |
-| `diameter` | Circular-tube diameter `d`, in metres, or the hydraulic diameter `4 * area / perimeter` for a rectangular tube. |
+| `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)). |
**Returns:** Attenuation constant `k0''`, in nepers per metre.
@@ -911,9 +1026,9 @@ supplied, frequencies outside the plane-wave range (Eqs. (1)-(4)) raise an
| `characteristic_impedance` | Characteristic impedance `rho c0`, in rayls. |
| `attenuation` | Optional tube attenuation constant `k0''`, in nepers/m (see [`tube_attenuation_constant`](/phonometry/reference/api/materials/impedance-tube/#tube_attenuation_constant)). |
| `diameter` | Optional tube diameter/lateral dimension, in metres, that activates the plane-wave range check. |
-| `shape` | Tube cross-section, `"circular"` or `"rectangular"`. |
+| `shape` | Tube cross-section, `"circular"`, `"rectangular"` or `"square"`. |
-**Returns:** An [`ImpedanceTubeResult`](/phonometry/reference/api/materials/impedance-tube/#impedancetuberesult).
+**Returns:** An [`ImpedanceTubeResult`](/phonometry/reference/api/materials/impedance-tube/#impedancetuberesult) (the tube geometry is retained on the result).
## wave_decomposition
@@ -929,6 +1044,8 @@ wave_decomposition(
l2: float,
s2: float,
wavenumber: ArrayLike,
+ diameter: float | None = None,
+ shape: str = 'circular',
) -> tuple[Complex, Complex, Complex, Complex]
```
@@ -966,5 +1083,7 @@ air-layer transfer matrix (see [`air_layer_transfer_matrix`](/phonometry/referen
| `l2` | Distance `l2` from the front reference plane, in metres. |
| `s2` | Downstream microphone spacing `s2`, in metres. |
| `wavenumber` | Air wavenumber `k` (real or complex), scalar or per band. |
+| `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). |
+| `shape` | Tube cross-section, `"circular"`, `"rectangular"` or `"square"`. |
**Returns:** Tuple `(A, B, C, D)` of complex amplitudes.
diff --git a/src/phonometry/__init__.py b/src/phonometry/__init__.py
index 7a7714d6d..c34ca7c3a 100644
--- a/src/phonometry/__init__.py
+++ b/src/phonometry/__init__.py
@@ -523,10 +523,12 @@
apply_mic_calibration,
characteristic_impedance,
face_quantities,
+ hydraulic_diameter,
mic_calibration_factor,
normalized_surface_admittance,
normalized_surface_impedance,
plane_wave_frequency_range,
+ plane_wave_frequency_range_astm,
reflection_factor,
speed_of_sound_astm,
speed_of_sound_iso,
@@ -1596,6 +1598,7 @@
"helmholtz_resonator_impedance",
"hemisphere_source_level",
"htlan",
+ "hydraulic_diameter",
"hydrophone_depths",
"idle_channel_noise",
"image_source_rir",
@@ -1764,6 +1767,7 @@
"piston_resistance",
"piston_volume_flow_rate",
"plane_wave_frequency_range",
+ "plane_wave_frequency_range_astm",
"plate_bending_stiffness",
"plate_loss_factor",
"plenum_attenuation",
diff --git a/src/phonometry/_report/_i18n.py b/src/phonometry/_report/_i18n.py
index 07f374c0a..bf91f61d5 100644
--- a/src/phonometry/_report/_i18n.py
+++ b/src/phonometry/_report/_i18n.py
@@ -192,6 +192,10 @@
"{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.",
"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.",
"Tube diameter d [mm]": "Diámetro del tubo d [mm]",
+ "Tube cross-section": "Sección transversal del tubo",
+ "Circular": "Circular",
+ "Rectangular": "Rectangular",
+ "Square": "Cuadrada",
"Microphone spacing s [mm]": "Separación de micrófonos s [mm]",
"Frequency range [Hz]": "Rango de frecuencias [Hz]",
"Normal-incidence α and normalised surface impedance z": "α a incidencia normal e impedancia superficial normalizada z",
diff --git a/src/phonometry/_report/iso10534.py b/src/phonometry/_report/iso10534.py
index a521826ad..a79f4d415 100644
--- a/src/phonometry/_report/iso10534.py
+++ b/src/phonometry/_report/iso10534.py
@@ -59,6 +59,13 @@
if TYPE_CHECKING:
from ..materials.impedance_tube import ImpedanceTubeResult
+#: Printable label for each accepted ``ReportMetadata.tube_shape`` value.
+_SHAPE_LABELS = {
+ "circular": "Circular",
+ "rectangular": "Rectangular",
+ "square": "Square",
+}
+
def _a2(value: float, language: str = "en") -> str:
"""Two decimals (``alpha``, ``|r|`` and the normalised impedance parts)."""
@@ -88,6 +95,7 @@ def _md(name: str) -> Any:
specimen = _md("specimen")
tube_diameter = _md("tube_diameter")
mic_spacing = _md("mic_spacing")
+ tube_shape = _md("tube_shape")
mounting = _md("mounting")
test_room = _md("test_room")
test_date = _md("test_date")
@@ -108,6 +116,9 @@ def _md(name: str) -> Any:
(t("Tube diameter d [mm]", language),
fmt_meta(tube_diameter * 1e3, language)
if tube_diameter is not None else None),
+ (t("Tube cross-section", language),
+ t(_SHAPE_LABELS[tube_shape], language)
+ if tube_shape is not None else None),
(t("Microphone spacing s [mm]", language),
fmt_meta(mic_spacing * 1e3, language)
if mic_spacing is not None else None),
diff --git a/src/phonometry/_report/metadata.py b/src/phonometry/_report/metadata.py
index 4d10668e8..b91d74b09 100644
--- a/src/phonometry/_report/metadata.py
+++ b/src/phonometry/_report/metadata.py
@@ -81,6 +81,10 @@ class ReportMetadata:
positions of the impedance tube, in metres. Printed by the
impedance-tube fiche (ISO 10534-2), where it bounds the working
frequency range.
+ :ivar 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).
:ivar 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
@@ -113,9 +117,10 @@ class 0 is the strictest. When supplied, the fiche's verdict passes if
:ivar notes: Free-form remarks printed in the footer.
:raises 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.
+ 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"``.
"""
specimen: str | None = None
@@ -151,6 +156,9 @@ class 0 is the strictest. When supplied, the fiche's verdict passes if
requirement: float | None = None
required_class: int | None = None
notes: str | None = None
+ # Appended after the original fields so the positional constructor order
+ # of this public dataclass is preserved.
+ tube_shape: str | None = None
#: Numeric fields that must be finite and strictly positive.
_POSITIVE_FIELDS = (
@@ -232,6 +240,13 @@ def __post_init__(self) -> None:
"ReportMetadata.required_class must be 0, 1 or 2 when given; "
f"got {self.required_class!r}."
)
+ if self.tube_shape is not None and self.tube_shape not in (
+ "circular", "rectangular", "square"
+ ):
+ raise ValueError(
+ "ReportMetadata.tube_shape must be 'circular', 'rectangular' "
+ f"or 'square' when given; got {self.tube_shape!r}."
+ )
def is_empty(self) -> bool:
"""Return ``True`` when no field is set (an all-``None`` instance)."""
diff --git a/src/phonometry/materials/__init__.py b/src/phonometry/materials/__init__.py
index e5c695d95..3fd697c2e 100644
--- a/src/phonometry/materials/__init__.py
+++ b/src/phonometry/materials/__init__.py
@@ -63,10 +63,12 @@
apply_mic_calibration,
characteristic_impedance,
face_quantities,
+ hydraulic_diameter,
mic_calibration_factor,
normalized_surface_admittance,
normalized_surface_impedance,
plane_wave_frequency_range,
+ plane_wave_frequency_range_astm,
reflection_factor,
speed_of_sound_astm,
speed_of_sound_iso,
@@ -270,6 +272,7 @@
"geometric_spreading_factor_angle",
"helmholtz_resonance_frequency",
"helmholtz_resonator_impedance",
+ "hydraulic_diameter",
"insitu_absorption_coefficient",
"insitu_absorption_from_reflection",
"insitu_absorption_spectrum",
@@ -295,6 +298,7 @@
"perforation_end_correction",
"piston_volume_flow_rate",
"plane_wave_frequency_range",
+ "plane_wave_frequency_range_astm",
"power_reflection_coefficient",
"practical_absorption_coefficient",
"practical_coefficient_uncertainty",
diff --git a/src/phonometry/materials/impedance_tube.py b/src/phonometry/materials/impedance_tube.py
index f6bd9761e..1e4f47189 100644
--- a/src/phonometry/materials/impedance_tube.py
+++ b/src/phonometry/materials/impedance_tube.py
@@ -85,6 +85,31 @@
#: i.e. ``f_l = c0 / (20 s)``.
_ISO_LOWER_WAVELENGTH_FRACTION = 20.0
+#: Upper-frequency plane-wave factor ``K``, circular tube (ASTM E2611-19,
+#: 6.2.4.1, Eq. (2)).
+_ASTM_KU_CIRCULAR = 0.586
+#: Upper-frequency plane-wave factor ``K``, rectangular tube with ``d`` the
+#: largest section dimension (ASTM E2611-19, 6.2.5).
+_ASTM_KU_RECTANGULAR = 0.500
+#: Microphone-spacing factor for the upper limit: ``s`` no larger than 80 % of
+#: ``c / (2 f_u)``, i.e. ``f_u s < 0,40 c`` (ASTM E2611-19, 6.5.4).
+_ASTM_KU_SPACING = 0.40
+#: Lower-limit factor: spacing shall be greater than 1 % of the wavelength
+#: (ASTM E2611-19, 6.2.3), i.e. ``f_l = c / (100 s)``.
+_ASTM_LOWER_WAVELENGTH_FRACTION = 100.0
+
+#: Shared validation message for the tube diameter arguments.
+_DIAMETER_POSITIVE = "'diameter' must be positive."
+
+#: Aliases accepted for the tube cross-section ``shape`` arguments. A square
+#: tube is the rectangular case with equal sides (ISO 10534-2, 4.1;
+#: ASTM E2611-19, 6.2.5).
+_SHAPE_ALIASES = {
+ "circular": "circular",
+ "rectangular": "rectangular",
+ "square": "rectangular",
+}
+
__all__ = [
"ImpedanceTubeResult",
"ImpedanceTubeWarning",
@@ -96,10 +121,12 @@
"apply_mic_calibration",
"characteristic_impedance",
"face_quantities",
+ "hydraulic_diameter",
"mic_calibration_factor",
"normalized_surface_admittance",
"normalized_surface_impedance",
"plane_wave_frequency_range",
+ "plane_wave_frequency_range_astm",
"reflection_factor",
"speed_of_sound_astm",
"speed_of_sound_iso",
@@ -202,8 +229,36 @@ def air_density_astm(
# ---------------------------------------------------------------------------
-# Wavenumber and tube attenuation (ISO 10534-2).
+# Cross-section helpers, wavenumber and tube attenuation (ISO 10534-2).
# ---------------------------------------------------------------------------
+def _canonical_shape(shape: str) -> str:
+ """Normalise a cross-section name to ``"circular"``/``"rectangular"``."""
+ try:
+ return _SHAPE_ALIASES[shape]
+ except KeyError:
+ raise ValueError(
+ "'shape' must be 'circular', 'rectangular' or 'square'."
+ ) from None
+
+
+def hydraulic_diameter(width: float, height: float) -> float:
+ """Hydraulic diameter of a rectangular tube, ``4 A / P`` (ISO 10534-2, A.2.1.5).
+
+ For a rectangular cross-section of side lengths ``w`` and ``h`` the ratio
+ of four times the area to the perimeter reduces to
+ ``d_h = 2 w h / (w + h)``; a square tube gives ``d_h`` equal to the side
+ length. This is the ``d`` the Eq. (A.18) attenuation estimate expects for
+ rectangular tubes (see :func:`tube_attenuation_constant`).
+
+ :param width: Inner side length ``w``, in metres.
+ :param height: Inner side length ``h``, in metres.
+ :return: Hydraulic diameter ``d_h = 4 A / P``, in metres.
+ """
+ if width <= 0.0 or height <= 0.0:
+ raise ValueError("'width' and 'height' must be positive.")
+ return float(2.0 * width * height / (width + height))
+
+
def tube_attenuation_constant(
frequency: ArrayLike, speed_of_sound: float, diameter: float
) -> Real:
@@ -215,13 +270,14 @@ def tube_attenuation_constant(
:param frequency: Frequency ``f``, in hertz (scalar or per band).
:param speed_of_sound: Speed of sound ``c0``, in metres per second.
:param diameter: Circular-tube diameter ``d``, in metres, or the hydraulic
- diameter ``4 * area / perimeter`` for a rectangular tube.
+ diameter ``4 * area / perimeter`` for a rectangular tube (see
+ :func:`hydraulic_diameter`).
:return: Attenuation constant ``k0''``, in nepers per metre.
"""
if speed_of_sound <= 0.0:
raise ValueError("'speed_of_sound' must be positive.")
if diameter <= 0.0:
- raise ValueError("'diameter' must be positive.")
+ raise ValueError(_DIAMETER_POSITIVE)
f = np.asarray(frequency, dtype=np.float64)
if np.any(f < 0.0):
raise ValueError("'frequency' must be non-negative.")
@@ -414,25 +470,89 @@ def plane_wave_frequency_range(
:param spacing: Microphone spacing ``s``, in metres.
:param speed_of_sound: Speed of sound ``c0``, in metres per second.
:param diameter: Tube diameter (circular) or maximum lateral dimension
- (rectangular) ``d``, in metres; ``None`` applies only the spacing bound.
- :param shape: ``"circular"`` or ``"rectangular"``.
+ (rectangular/square) ``d``, in metres; ``None`` applies only the
+ spacing bound.
+ :param shape: ``"circular"``, ``"rectangular"`` or ``"square"`` (a square
+ tube is the rectangular bound with ``d`` the side length).
:return: Tuple ``(f_l, f_u)`` of the lower and upper frequency limits, in Hz.
"""
+ return _frequency_range(
+ spacing,
+ speed_of_sound,
+ diameter=diameter,
+ shape=shape,
+ ku_circular=_ISO_KU_CIRCULAR,
+ ku_rectangular=_ISO_KU_RECTANGULAR,
+ ku_spacing=_ISO_KU_SPACING,
+ lower_fraction=_ISO_LOWER_WAVELENGTH_FRACTION,
+ )
+
+
+def plane_wave_frequency_range_astm(
+ spacing: float,
+ speed_of_sound: float,
+ *,
+ diameter: float | None = None,
+ shape: str = "circular",
+) -> tuple[float, float]:
+ """Working plane-wave frequency range ``(f_l, f_u)`` (ASTM E2611-19).
+
+ The upper limit is the smaller of the microphone-spacing bound
+ ``s <= 0,8 c / (2 f_u)``, i.e. ``f_u s < 0,40 c`` (6.5.4), and, when the
+ tube ``diameter`` is given, the cut-on bound ``f_u < K c / d`` with
+ ``K = 0,586`` for a circular tube (6.2.4.1, Eq. (2)) or ``K = 0,500`` for
+ a rectangular tube with ``d`` the largest section dimension (6.2.5). The
+ lower limit follows 6.2.3: the spacing shall be greater than 1 % of the
+ wavelength, i.e. ``f_l = c / (100 s)``.
+
+ With two different spacings ``s1``/``s2``, call with the larger one for
+ the upper bound and the smaller one for the lower bound (each bound is
+ binding for every microphone pair).
+
+ :param spacing: Microphone spacing ``s``, in metres.
+ :param speed_of_sound: Speed of sound ``c``, in metres per second.
+ :param diameter: Tube diameter (circular) or largest section dimension
+ (rectangular/square) ``d``, in metres; ``None`` applies only the
+ spacing bound.
+ :param shape: ``"circular"``, ``"rectangular"`` or ``"square"``.
+ :return: Tuple ``(f_l, f_u)`` of the lower and upper frequency limits, in Hz.
+ """
+ return _frequency_range(
+ spacing,
+ speed_of_sound,
+ diameter=diameter,
+ shape=shape,
+ ku_circular=_ASTM_KU_CIRCULAR,
+ ku_rectangular=_ASTM_KU_RECTANGULAR,
+ ku_spacing=_ASTM_KU_SPACING,
+ lower_fraction=_ASTM_LOWER_WAVELENGTH_FRACTION,
+ )
+
+
+def _frequency_range(
+ spacing: float,
+ speed_of_sound: float,
+ *,
+ diameter: float | None,
+ shape: str,
+ ku_circular: float,
+ ku_rectangular: float,
+ ku_spacing: float,
+ lower_fraction: float,
+) -> tuple[float, float]:
+ """Shared ISO/ASTM plane-wave frequency-range arithmetic."""
if spacing <= 0.0:
raise ValueError("'spacing' must be positive.")
if speed_of_sound <= 0.0:
raise ValueError("'speed_of_sound' must be positive.")
- if shape not in ("circular", "rectangular"):
- raise ValueError("'shape' must be 'circular' or 'rectangular'.")
- f_upper = _ISO_KU_SPACING * speed_of_sound / spacing
+ canonical = _canonical_shape(shape)
+ f_upper = ku_spacing * speed_of_sound / spacing
if diameter is not None:
if diameter <= 0.0:
- raise ValueError("'diameter' must be positive.")
- factor = (
- _ISO_KU_CIRCULAR if shape == "circular" else _ISO_KU_RECTANGULAR
- )
+ raise ValueError(_DIAMETER_POSITIVE)
+ factor = ku_circular if canonical == "circular" else ku_rectangular
f_upper = min(f_upper, factor * speed_of_sound / diameter)
- f_lower = speed_of_sound / (_ISO_LOWER_WAVELENGTH_FRACTION * spacing)
+ f_lower = speed_of_sound / (lower_fraction * spacing)
return f_lower, f_upper
@@ -464,6 +584,13 @@ class ImpedanceTubeResult:
(Eq. (19)), ``normalized_impedance`` the ratio ``Z / (rho c0)`` (Eq. (19))
and ``absorption`` the normal-incidence coefficient ``alpha = 1 - |r|^2``
(Eq. (18)).
+
+ The trailing fields retain the tube geometry the reduction was run with
+ (microphone ``spacing`` ``s``, distance ``x1`` from the sample to the
+ farther microphone, tube ``diameter`` and cross-section ``shape``, stored
+ canonically as ``"circular"``/``"rectangular"`` - a ``"square"`` input is
+ kept as ``"rectangular"``); they default to ``None`` when not supplied to
+ :func:`two_microphone_impedance`.
"""
frequency: Real
@@ -471,6 +598,10 @@ class ImpedanceTubeResult:
surface_impedance: Complex
normalized_impedance: Complex
absorption: Real
+ spacing: float | None = None
+ x1: float | None = None
+ diameter: float | None = None
+ shape: str | None = None
def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes:
"""Plot the absorption spectrum ``alpha(f)`` with ``|r|`` overlaid.
@@ -513,7 +644,8 @@ def report(
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``,
+ ``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
@@ -574,15 +706,18 @@ def two_microphone_impedance(
nepers/m (see :func:`tube_attenuation_constant`).
:param diameter: Optional tube diameter/lateral dimension, in metres, that
activates the plane-wave range check.
- :param shape: Tube cross-section, ``"circular"`` or ``"rectangular"``.
- :return: An :class:`ImpedanceTubeResult`.
+ :param shape: Tube cross-section, ``"circular"``, ``"rectangular"`` or
+ ``"square"``.
+ :return: An :class:`ImpedanceTubeResult` (the tube geometry is retained on
+ the result).
"""
f = np.asarray(frequency, dtype=np.float64)
k0 = tube_wavenumber(f, speed_of_sound, attenuation=attenuation)
r = reflection_factor(h12, spacing=spacing, x1=x1, wavenumber=k0)
+ canonical = _canonical_shape(shape)
if diameter is not None:
f_lower, f_upper = plane_wave_frequency_range(
- spacing, speed_of_sound, diameter=diameter, shape=shape
+ spacing, speed_of_sound, diameter=diameter, shape=canonical
)
_warn_frequency_range(f, f_lower, f_upper, stacklevel=2)
return ImpedanceTubeResult(
@@ -591,6 +726,10 @@ def two_microphone_impedance(
surface_impedance=surface_impedance(r, characteristic_impedance),
normalized_impedance=normalized_surface_impedance(r),
absorption=absorption_from_reflection(r),
+ spacing=spacing,
+ x1=x1,
+ diameter=diameter,
+ shape=canonical if diameter is not None else None,
)
@@ -698,6 +837,46 @@ def standing_wave_normalized_impedance(
# ---------------------------------------------------------------------------
# ASTM E2611-19: four-microphone transfer-matrix method.
# ---------------------------------------------------------------------------
+def _warn_astm_plane_wave(
+ wavenumber: ArrayLike,
+ *,
+ s1: float,
+ s2: float,
+ diameter: float,
+ shape: str,
+ stacklevel: int,
+) -> None:
+ """Advise when wavenumbers leave the ASTM E2611-19 plane-wave range.
+
+ The check runs on the real part of ``k`` so no speed of sound is needed:
+ ``f s < 0,40 c`` maps to ``k s < 0,80 pi`` (6.5.4), ``f d < K c`` to
+ ``k d < 2 pi K`` (6.2.4.1/6.2.5) and the greater-than-1 %-of-wavelength
+ spacing bound to ``k s > 0,02 pi`` (6.2.3). The upper spacing bound binds
+ every microphone pair (largest spacing), the lower one the smallest.
+ """
+ if s1 <= 0.0 or s2 <= 0.0:
+ raise ValueError("'s1' and 's2' must be positive.")
+ if diameter <= 0.0:
+ raise ValueError(_DIAMETER_POSITIVE)
+ k = np.real(np.asarray(wavenumber, dtype=np.complex128))
+ ku = _ASTM_KU_CIRCULAR if shape == "circular" else _ASTM_KU_RECTANGULAR
+ two_pi = 2.0 * np.pi
+ k_upper = min(
+ two_pi * _ASTM_KU_SPACING / max(s1, s2),
+ two_pi * ku / diameter,
+ )
+ k_lower = two_pi / (_ASTM_LOWER_WAVELENGTH_FRACTION * min(s1, s2))
+ if np.any(k < k_lower) or np.any(k > k_upper):
+ warnings.warn(
+ "Wavenumbers outside the ASTM E2611-19 plane-wave working range "
+ "(6.2.3-6.2.5, 6.5.4) for the given microphone spacings and tube "
+ "cross-section; results there are advisory. See "
+ "plane_wave_frequency_range_astm() for the limits in hertz.",
+ ImpedanceTubeWarning,
+ stacklevel=stacklevel + 1,
+ )
+
+
def wave_decomposition(
h1: ArrayLike,
h2: ArrayLike,
@@ -709,6 +888,8 @@ def wave_decomposition(
l2: float,
s2: float,
wavenumber: ArrayLike,
+ diameter: float | None = None,
+ shape: str = "circular",
) -> tuple[Complex, Complex, Complex, Complex]:
"""Decompose the wave field into ``(A, B, C, D)`` (ASTM E2611-19, Eqs. (17)-(20)).
@@ -738,10 +919,21 @@ def wave_decomposition(
:param l2: Distance ``l2`` from the front reference plane, in metres.
:param s2: Downstream microphone spacing ``s2``, in metres.
:param wavenumber: Air wavenumber ``k`` (real or complex), scalar or per band.
+ :param 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).
+ :param shape: Tube cross-section, ``"circular"``, ``"rectangular"`` or
+ ``"square"``.
:return: Tuple ``(A, B, C, D)`` of complex amplitudes.
"""
if s1 <= 0.0 or s2 <= 0.0:
raise ValueError("'s1' and 's2' must be positive.")
+ canonical = _canonical_shape(shape)
+ if diameter is not None:
+ _warn_astm_plane_wave(
+ wavenumber, s1=s1, s2=s2, diameter=diameter, shape=canonical,
+ stacklevel=2,
+ )
ha = np.asarray(h1, dtype=np.complex128)
hb = np.asarray(h2, dtype=np.complex128)
hc = np.asarray(h3, dtype=np.complex128)
@@ -813,12 +1005,30 @@ class TransferMatrix:
Relates the pressure and normal particle velocity across a specimen,
``[p; u]_{x=0} = T [p; u]_{x=d}`` (Eq. (16)). Each entry is complex and
may be scalar or a per-frequency array of matching shape.
+
+ The trailing fields retain the measurement context when the matrix comes
+ out of :func:`transfer_matrix_two_load` / :func:`transfer_matrix_one_load`
+ (tube geometry ``l1``/``s1``/``l2``/``s2``, specimen ``thickness``, tube
+ ``diameter`` and canonical cross-section ``shape``, the ``frequency``
+ vector when supplied to the solver, and the air
+ ``air_characteristic_impedance`` ``rho c``); all default to ``None`` so a
+ hand-built matrix (for example :func:`air_layer_transfer_matrix`) is
+ unchanged.
"""
t11: Complex
t12: Complex
t21: Complex
t22: Complex
+ l1: float | None = None
+ s1: float | None = None
+ l2: float | None = None
+ s2: float | None = None
+ thickness: float | None = None
+ diameter: float | None = None
+ shape: str | None = None
+ frequency: Real | None = None
+ air_characteristic_impedance: float | None = None
def determinant(self) -> Complex:
"""Determinant ``T11 T22 - T12 T21`` (unity for a reciprocal specimen)."""
@@ -896,8 +1106,8 @@ def characteristic_impedance_material(self) -> Complex:
def plot(
self,
- frequency: ArrayLike,
- characteristic_impedance: float,
+ frequency: ArrayLike | None = None,
+ characteristic_impedance: float | None = None,
ax: Axes | None = None,
*,
language: str = "en",
@@ -909,23 +1119,38 @@ def plot(
laboratory quotes: the normal-incidence transmission loss ``TLn(f)``
(Eq. (26), the primary curve, left axis) and the hard-backed
absorption coefficient ``alpha(f)`` (Eq. (28), a muted companion on a
- 0..1 right axis). The matrix itself carries no frequency axis, so the
- ``frequency`` vector of the measurement (matching the shape of the
- entries) and the air characteristic impedance ``rho c`` must be
- supplied.
+ 0..1 right axis). The four-pole entries carry no frequency axis of
+ their own, so the plot needs the measurement's ``frequency`` vector
+ (matching the shape of the entries) and the air characteristic
+ impedance ``rho c``. A matrix built by the solvers retains both
+ (``self.frequency`` / ``self.air_characteristic_impedance``), so
+ ``plot()`` takes no arguments there; only a hand-built matrix (for
+ example :func:`air_layer_transfer_matrix`) must supply them.
Requires matplotlib (``pip install phonometry[plot]``); returns the
:class:`~matplotlib.axes.Axes` of the transmission-loss curve.
:param frequency: Frequency vector ``f``, in hertz, matching the shape
- of the matrix entries.
+ of the matrix entries; ``None`` uses the stored ``frequency``.
:param characteristic_impedance: Characteristic impedance ``rho c`` of
- the air in the tube, in rayls.
+ the air in the tube, in rayls; ``None`` uses the stored
+ ``air_characteristic_impedance``.
:param ax: Existing axes, or ``None`` to create a figure.
:param language: Plot language: ``"en"`` (default) or ``"es"``.
:param kwargs: Forwarded to the transmission-loss ``plot`` call.
:return: The axes.
+ :raises ValueError: If ``frequency`` or ``characteristic_impedance``
+ is neither supplied nor stored on the matrix.
"""
+ if frequency is None:
+ frequency = self.frequency
+ if characteristic_impedance is None:
+ characteristic_impedance = self.air_characteristic_impedance
+ if frequency is None or characteristic_impedance is None:
+ raise ValueError(
+ "'frequency' and 'characteristic_impedance' must be supplied "
+ "when the matrix does not retain them (hand-built matrices)."
+ )
from .._i18n import check_language
from .._plot.materials import plot_transfer_matrix
@@ -1017,6 +1242,35 @@ def _warn_ill_conditioned(
)
+def _measurement_context(
+ *,
+ l1: float,
+ s1: float,
+ l2: float,
+ s2: float,
+ thickness: float,
+ diameter: float | None,
+ shape: str,
+ frequency: ArrayLike | None,
+ characteristic_impedance: float,
+) -> dict[str, Any]:
+ """Context fields a solver retains on the :class:`TransferMatrix`."""
+ return {
+ "l1": l1,
+ "s1": s1,
+ "l2": l2,
+ "s2": s2,
+ "thickness": thickness,
+ "diameter": diameter,
+ "shape": shape if diameter is not None else None,
+ "frequency": (
+ np.asarray(frequency, dtype=np.float64)
+ if frequency is not None else None
+ ),
+ "air_characteristic_impedance": characteristic_impedance,
+ }
+
+
def transfer_matrix_two_load(
load_a: tuple[ArrayLike, ArrayLike, ArrayLike, ArrayLike],
load_b: tuple[ArrayLike, ArrayLike, ArrayLike, ArrayLike],
@@ -1028,6 +1282,9 @@ def transfer_matrix_two_load(
thickness: float,
wavenumber: ArrayLike,
characteristic_impedance: float,
+ frequency: ArrayLike | None = None,
+ diameter: float | None = None,
+ shape: str = "circular",
) -> TransferMatrix:
"""Two-load transfer matrix (ASTM E2611-19, Eqs. (17)-(22)).
@@ -1050,8 +1307,22 @@ def transfer_matrix_two_load(
:param thickness: Specimen thickness ``d``, in metres.
:param wavenumber: Air wavenumber ``k``.
:param characteristic_impedance: Characteristic impedance ``rho c``.
- :return: The specimen :class:`TransferMatrix`.
+ :param frequency: Optional frequency vector ``f``, in hertz, retained on
+ the result so :meth:`TransferMatrix.plot` needs no arguments.
+ :param 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).
+ :param shape: Tube cross-section, ``"circular"``, ``"rectangular"`` or
+ ``"square"``.
+ :return: The specimen :class:`TransferMatrix` (measurement context
+ retained on the result).
"""
+ canonical = _canonical_shape(shape)
+ if diameter is not None:
+ _warn_astm_plane_wave(
+ wavenumber, s1=s1, s2=s2, diameter=diameter, shape=canonical,
+ stacklevel=2,
+ )
p0a, pda, u0a, uda = _face_from_loads(
load_a, l1=l1, s1=s1, l2=l2, s2=s2, thickness=thickness,
wavenumber=wavenumber, characteristic_impedance=characteristic_impedance,
@@ -1071,6 +1342,11 @@ def transfer_matrix_two_load(
t12=np.asarray((p0b * pda - p0a * pdb) / den, dtype=np.complex128),
t21=np.asarray((u0a * udb - u0b * uda) / den, dtype=np.complex128),
t22=np.asarray((pda * u0b - pdb * u0a) / den, dtype=np.complex128),
+ **_measurement_context(
+ l1=l1, s1=s1, l2=l2, s2=s2, thickness=thickness,
+ diameter=diameter, shape=canonical, frequency=frequency,
+ characteristic_impedance=characteristic_impedance,
+ ),
)
@@ -1084,6 +1360,9 @@ def transfer_matrix_one_load(
thickness: float,
wavenumber: ArrayLike,
characteristic_impedance: float,
+ frequency: ArrayLike | None = None,
+ diameter: float | None = None,
+ shape: str = "circular",
) -> TransferMatrix:
"""One-load transfer matrix, symmetric specimen (ASTM E2611-19, Eqs. (23)-(24)).
@@ -1103,8 +1382,22 @@ def transfer_matrix_one_load(
:param thickness: Specimen thickness ``d``, in metres.
:param wavenumber: Air wavenumber ``k``.
:param characteristic_impedance: Characteristic impedance ``rho c``.
- :return: The specimen :class:`TransferMatrix`.
+ :param frequency: Optional frequency vector ``f``, in hertz, retained on
+ the result so :meth:`TransferMatrix.plot` needs no arguments.
+ :param 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).
+ :param shape: Tube cross-section, ``"circular"``, ``"rectangular"`` or
+ ``"square"``.
+ :return: The specimen :class:`TransferMatrix` (measurement context
+ retained on the result).
"""
+ canonical = _canonical_shape(shape)
+ if diameter is not None:
+ _warn_astm_plane_wave(
+ wavenumber, s1=s1, s2=s2, diameter=diameter, shape=canonical,
+ stacklevel=2,
+ )
p0, pd, u0, ud = _face_from_loads(
load, l1=l1, s1=s1, l2=l2, s2=s2, thickness=thickness,
wavenumber=wavenumber, characteristic_impedance=characteristic_impedance,
@@ -1121,6 +1414,11 @@ def transfer_matrix_one_load(
t12=np.asarray((p0**2 - pd**2) / den, dtype=np.complex128),
t21=np.asarray((u0**2 - ud**2) / den, dtype=np.complex128),
t22=t_diag,
+ **_measurement_context(
+ l1=l1, s1=s1, l2=l2, s2=s2, thickness=thickness,
+ diameter=diameter, shape=canonical, frequency=frequency,
+ characteristic_impedance=characteristic_impedance,
+ ),
)
diff --git a/tests/materials/test_impedance_tube.py b/tests/materials/test_impedance_tube.py
index 4d62cf1f1..8261de20f 100644
--- a/tests/materials/test_impedance_tube.py
+++ b/tests/materials/test_impedance_tube.py
@@ -34,10 +34,12 @@
apply_mic_calibration,
characteristic_impedance,
face_quantities,
+ hydraulic_diameter,
mic_calibration_factor,
normalized_surface_admittance,
normalized_surface_impedance,
plane_wave_frequency_range,
+ plane_wave_frequency_range_astm,
reflection_factor,
speed_of_sound_astm,
speed_of_sound_iso,
@@ -248,6 +250,79 @@ def test_frequency_range_warning_fires() -> None:
)
+def test_plane_wave_range_rectangular_bound() -> None:
+ # ISO 10534-2 Eq. (3): a rectangular tube cuts on at 0,50 c0 / d with d the
+ # maximum side length (stricter than the 0,58 circular factor of Eq. (2)).
+ f_lower, f_upper = plane_wave_frequency_range(
+ 0.03, C0, diameter=0.05, shape="rectangular"
+ )
+ assert f_upper == pytest.approx(min(0.45 * C0 / 0.03, 0.50 * C0 / 0.05))
+ assert f_lower == pytest.approx(C0 / (20.0 * 0.03))
+
+
+def test_plane_wave_range_square_alias() -> None:
+ # A square tube is the rectangular case with d the side length, in both
+ # the ISO 10534-2 and the ASTM E2611-19 bounds.
+ for fn in (plane_wave_frequency_range, plane_wave_frequency_range_astm):
+ square = fn(0.03, C0, diameter=0.05, shape="square")
+ rectangular = fn(0.03, C0, diameter=0.05, shape="rectangular")
+ assert square == rectangular
+
+
+def test_plane_wave_range_invalid_shape() -> None:
+ with pytest.raises(ValueError, match="shape"):
+ plane_wave_frequency_range(0.03, C0, diameter=0.05, shape="oval")
+ with pytest.raises(ValueError, match="shape"):
+ plane_wave_frequency_range_astm(0.03, C0, diameter=0.05, shape="oval")
+
+
+def test_plane_wave_range_astm_bounds() -> None:
+ # ASTM E2611-19: upper = min(0,40 c/s (6.5.4), 0,586 c/d circular
+ # (6.2.4.1) or 0,500 c/d rectangular (6.2.5)); lower = c / (100 s), the
+ # spacing greater than 1 % of the wavelength (6.2.3).
+ f_lower, f_upper = plane_wave_frequency_range_astm(
+ 0.03, C0, diameter=0.05, shape="circular"
+ )
+ assert f_upper == pytest.approx(min(0.40 * C0 / 0.03, 0.586 * C0 / 0.05))
+ assert f_lower == pytest.approx(C0 / (100.0 * 0.03))
+ _, f_upper_rect = plane_wave_frequency_range_astm(
+ 0.03, C0, diameter=0.05, shape="rectangular"
+ )
+ assert f_upper_rect == pytest.approx(min(0.40 * C0 / 0.03, 0.50 * C0 / 0.05))
+
+
+def test_hydraulic_diameter_values() -> None:
+ # d_h = 4 A / P = 2 w h / (w + h) (ISO 10534-2 A.2.1.5); a square tube
+ # gives the side length back.
+ assert hydraulic_diameter(0.05, 0.05) == pytest.approx(0.05)
+ assert hydraulic_diameter(0.08, 0.04) == pytest.approx(
+ 4.0 * (0.08 * 0.04) / (2.0 * (0.08 + 0.04))
+ )
+ with pytest.raises(ValueError, match="positive"):
+ hydraulic_diameter(0.0, 0.05)
+
+
+def test_result_retains_tube_geometry() -> None:
+ f = np.array([800.0, 1200.0])
+ h12 = _synth_h12(0.3 + 0.1j, np.asarray(tube_wavenumber(f, C0)), 0.12, 0.03)
+ res = two_microphone_impedance(
+ h12, frequency=f, spacing=0.03, x1=0.12, speed_of_sound=C0,
+ characteristic_impedance=RC, diameter=0.05, shape="square",
+ )
+ assert res.spacing == pytest.approx(0.03)
+ assert res.x1 == pytest.approx(0.12)
+ assert res.diameter == pytest.approx(0.05)
+ # "square" is stored canonically as the rectangular case.
+ assert res.shape == "rectangular"
+ # Without a diameter no cross-section claim is retained.
+ res_no_d = two_microphone_impedance(
+ h12, frequency=f, spacing=0.03, x1=0.12, speed_of_sound=C0,
+ characteristic_impedance=RC,
+ )
+ assert res_no_d.diameter is None
+ assert res_no_d.shape is None
+
+
def test_frequency_range_no_warning_in_band() -> None:
f = np.array([800.0, 1200.0])
h12 = _synth_h12(0.3 + 0.1j, np.asarray(tube_wavenumber(f, C0)), 0.12, 0.03)
@@ -537,6 +612,99 @@ def test_one_load_recovers_symmetric_specimen() -> None:
assert np.allclose(rec.t11, rec.t22)
+def test_astm_solvers_warn_outside_plane_wave_range() -> None:
+ # With the tube diameter given, frequencies past the 6.2.4.1 cut-on
+ # (0,586 c/d for this 10 cm circular tube ~ 2011 Hz) must be flagged.
+ f = np.array([500.0, 2500.0])
+ k = np.asarray(tube_wavenumber(f, C0)).real.astype(np.complex128)
+ tm = air_layer_transfer_matrix(k, THICKNESS, RC)
+ load_a = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, 0.3 + 0.0j)
+ load_b = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, -0.5 + 0.2j)
+ with pytest.warns(ImpedanceTubeWarning, match="ASTM E2611-19"):
+ transfer_matrix_two_load(
+ load_a, load_b, thickness=THICKNESS, wavenumber=k,
+ characteristic_impedance=RC, diameter=0.10, **GEOM,
+ )
+ with pytest.warns(ImpedanceTubeWarning, match="ASTM E2611-19"):
+ transfer_matrix_one_load(
+ load_a, thickness=THICKNESS, wavenumber=k,
+ characteristic_impedance=RC, diameter=0.10, **GEOM,
+ )
+ with pytest.warns(ImpedanceTubeWarning, match="ASTM E2611-19"):
+ wave_decomposition(
+ *load_a, wavenumber=k, diameter=0.10, **GEOM,
+ )
+
+
+def test_astm_solvers_warn_below_lower_bound() -> None:
+ # The 6.2.3 lower bound: spacing greater than 1 % of the wavelength. For
+ # s = 3 cm that is ~114 Hz; 50 Hz must be flagged.
+ f = np.array([50.0])
+ k = np.asarray(tube_wavenumber(f, C0)).real.astype(np.complex128)
+ tm = air_layer_transfer_matrix(k, THICKNESS, RC)
+ load = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, 0.3 + 0.0j)
+ with pytest.warns(ImpedanceTubeWarning, match="ASTM E2611-19"):
+ wave_decomposition(*load, wavenumber=k, diameter=0.10, **GEOM)
+
+
+def test_astm_solvers_silent_in_band_and_without_diameter() -> None:
+ import warnings as _w
+
+ f = np.array([500.0, 1000.0, 1800.0])
+ k = np.asarray(tube_wavenumber(f, C0)).real.astype(np.complex128)
+ tm = air_layer_transfer_matrix(k, THICKNESS, RC)
+ load_a = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, 0.3 + 0.0j)
+ load_b = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, -0.5 + 0.2j)
+ with _w.catch_warnings():
+ _w.simplefilter("error", ImpedanceTubeWarning)
+ # In band with the diameter given (10 cm tube: 114 Hz .. 2011 Hz).
+ transfer_matrix_two_load(
+ load_a, load_b, thickness=THICKNESS, wavenumber=k,
+ characteristic_impedance=RC, diameter=0.10, **GEOM,
+ )
+ # No diameter -> no range check, as in the ISO branch.
+ k_wide = np.asarray(
+ tube_wavenumber(np.array([50.0, 2500.0]), C0)
+ ).real.astype(np.complex128)
+ tm_wide = air_layer_transfer_matrix(k_wide, THICKNESS, RC)
+ wide_a = _synth_four_mics(tm_wide, np.asarray(k_wide), 1.0 + 0.0j, 0.3 + 0.0j)
+ wide_b = _synth_four_mics(tm_wide, np.asarray(k_wide), 0.4 + 0.5j, -0.6 + 0.1j)
+ transfer_matrix_two_load(
+ wide_a, wide_b, thickness=THICKNESS, wavenumber=k_wide,
+ characteristic_impedance=RC, **GEOM,
+ )
+
+
+def test_transfer_matrix_retains_measurement_context() -> None:
+ # 500..1500 Hz sits inside the square-tube working range (114..1716 Hz).
+ f = np.array([500.0, 1000.0, 1500.0])
+ k = np.asarray(tube_wavenumber(f, C0)).real.astype(np.complex128)
+ tm = air_layer_transfer_matrix(k, THICKNESS, RC)
+ load_a = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, 0.3 + 0.0j)
+ load_b = _synth_four_mics(tm, np.asarray(k), 1.0 + 0.0j, -0.5 + 0.2j)
+ rec = transfer_matrix_two_load(
+ load_a, load_b, thickness=THICKNESS, wavenumber=k,
+ characteristic_impedance=RC, frequency=f, diameter=0.10,
+ shape="square", **GEOM,
+ )
+ assert rec.l1 == pytest.approx(GEOM["l1"])
+ assert rec.s1 == pytest.approx(GEOM["s1"])
+ assert rec.l2 == pytest.approx(GEOM["l2"])
+ assert rec.s2 == pytest.approx(GEOM["s2"])
+ assert rec.thickness == pytest.approx(THICKNESS)
+ assert rec.diameter == pytest.approx(0.10)
+ assert rec.shape == "rectangular"
+ assert rec.frequency is not None
+ assert np.allclose(rec.frequency, f)
+ assert rec.air_characteristic_impedance == pytest.approx(RC)
+ # A hand-built matrix retains nothing and plot() demands the arguments.
+ bare = air_layer_transfer_matrix(k, THICKNESS, RC)
+ assert bare.frequency is None
+ assert bare.air_characteristic_impedance is None
+ with pytest.raises(ValueError, match="hand-built"):
+ bare.plot()
+
+
def test_public_exports() -> None:
import phonometry
@@ -548,6 +716,7 @@ def test_public_exports() -> None:
"air_density_iso", "air_density_astm", "tube_wavenumber",
"tube_attenuation_constant", "mic_calibration_factor", "apply_mic_calibration",
"two_microphone_impedance", "plane_wave_frequency_range",
+ "plane_wave_frequency_range_astm", "hydraulic_diameter",
"standing_wave_absorption", "standing_wave_reflection",
"standing_wave_reflection_magnitude", "standing_wave_ratio_from_level",
"standing_wave_normalized_impedance", "wave_decomposition", "face_quantities",