Commits
Review follow-up:
- Resolve the pnpm store path via `pnpm store path` instead of hardcoding
~/.local/share/pnpm/store (it is actually .../store/v11 and varies by pnpm
version / XDG config, so the hardcoded key would never hit).
- Bump actions/cache v4 -> v6 (latest major).
pnpm stays on @latest via corepack by request (always track the newest release).
The Deploy Docs workflow failed at pnpm/action-setup@v6: with a floating
`version: 11` the action lays down a bundled pnpm and then self-updates to the
latest 11.x, and that switch crashes ("Cannot use 'in' operator to search for
'integrity' in undefined" in lockfileToDepGraph), so the build job never
reaches the site build.
Replace pnpm/action-setup with corepack (bundled with Node), which installs the
latest pnpm cleanly, and cache the pnpm store via actions/cache. Always tracks
the latest pnpm release.
Verified locally with pnpm 11.12.0: `pnpm install --frozen-lockfile`, `pnpm
build`, `pnpm run html-validate` (0 errors) and `pnpm run pa11y` (10/10 URLs, 0
errors) all pass.
* feat: underwater seabed reflection, ambient noise and ship-traffic source levels
Second half of the closed-form underwater propagation sub-phase (P1), building
on the transmission-loss/sound-speed/sonar core:
- seabed_reflection: fluid-fluid Rayleigh reflection coefficient (Medwin & Clay),
critical grazing angle arccos(c1/c2) and bottom loss BL = -20 lg|R|.
- ocean_ambient_noise: Wenz-framework spectrum levels, energy-summing wind noise
(rule of fives) and Mellen thermal noise, with an optional caller-supplied
shipping term.
- ship_traffic_noise: predicted ship source-level spectra from vessel class,
speed and length via JOMOPANS-ECHO (MacGillivray & de Jong 2021, default,
validated to the authors' File S1 reference calculator within 0.01 dB), plus
RANDI 3.1 and Wales & Heitmeyer (2002).
Each module exposes a frozen result dataclass with .plot(), a conformance check
with an independent oracle, one figure in four language/theme variants, and full
English docs plus EN/ES site guides. Conformance 179/179; full suite 2450 passed.
* refactor: address PR review feedback on underwater modules
- seabed_reflection: guard the c1 == c2 & grazing = 0 singular limit
(0/0 -> normal-incidence coefficient) so |R| never returns NaN; add test.
- ocean_ambient_noise: allow a calm sea (wind_speed_knots = 0) to return -inf
(zero energy contribution) rather than raising; add test.
- Migrate the three modules' input validation to the shared _validation helpers
(new require_positive_array enforces finite, positive, 1-D); removes
duplicated hand-rolled checks.
- Clarify units: source spectral-density is "dB re 1 µPa²/Hz at 1 m" (not the
ambiguous "/Hz m") in the plot label, figure, api-reference and docs; scope the
plane-wave reference note so it no longer claims to cover source levels; note
the wind-noise validity range (500 Hz-5 kHz) for the wide-range Wenz example.
- Label the wind/thermal component curves in the ambient-noise figure.
Kept the conformance thermal oracle's hardcoded physical constants (Boltzmann,
1 µPa) for clean-room independence from the module under test. Conformance
179/179; full suite 2452 passed.
* fix: SonarCloud float-equality and long-title docs-deploy failures
- seabed_reflection / ocean_ambient_noise: avoid float equality checks
(SonarCloud S1244). Detect the seabed 0/0 singular limit via np.isnan of the
result instead of comparing the denominator to 0.0; gate the calm-sea branch
with require_non_negative + a relational check.
- Shorten four site-guide titles so the rendered "<title> | phonometry" stays
within the 70-char html-validate limit (long-title), which was blocking the
docs deployment: the two underwater-propagation guides (EN/ES, lengthened in
this PR) and the two pre-existing wind-turbine-noise guides (EN/ES). The full
detail stays in each guide's description.
* feat(underwater): speed of sound in sea water (UNESCO / Del Grosso / Mackenzie)
sea_water_sound_speed() with three selectable equations (UNESCO/Chen-Millero
Wong-Zhu ITS-90 default, Del Grosso 1974, Mackenzie 1981), depth_to_pressure()
(Leroy-Parthiot 1998) and a SoundSpeedProfile with .plot(). Oracles: the
canonical Mackenzie check value 1550.744 m/s and mutual agreement of the three
equations to <1 m/s in their common domain.
* feat(underwater): transmission loss (spreading + volume absorption)
spreading_loss() (spherical/cylindrical/practical), seawater_absorption() with
Francois-Garrison (default), Ainslie-McColm and Thorp models, and
transmission_loss() -> TransmissionLossResult with .plot(). Oracles: closed-form
spreading recomputation, inline recomputation of each absorption formula, and
Francois-Garrison <-> Ainslie-McColm agreement within 10%.
* feat(underwater): sonar equation (passive and active)
passive_sonar_equation() (SE = SL - TL - (NL - DI) - DT) and
active_sonar_equation() (monostatic, noise- or reverberation-limited, 2.TL)
-> SonarEquationResult with signal excess, SNR, figure of merit and .plot().
Oracle: a hand-worked Urick/Etter term balance.
* test(underwater): conformance domain for transmission-loss propagation
Six checks: Mackenzie canonical sound speed (1550.744 m/s), UNESCO<->Mackenzie
cross-model agreement, spherical spreading, Thorp absorption, Ainslie-McColm<->
Francois-Garrison agreement, and the passive-sonar figure of merit. 174/174.
* docs(underwater): figures for TL, sound-speed profile and sonar equation
Three propagation figures x4 variants (light/dark x EN/ES): transmission loss
with the spreading/absorption split, the UNESCO sound-speed profile with the
sound-channel axis, and the passive sonar equation's signal excess vs TL.
* docs(underwater): guide, API reference, indices and sidebar for propagation
New EN docs page + EN/ES site guides for transmission loss, sea-water sound
speed and the sonar equation; api-reference rows for the new public symbols;
README and docs/README index entries; astro sidebar item under Underwater
acoustics.
* fix(underwater): Del Grosso pressure conversion (bar → kg/cm² multiplies)
Subagent review caught an inverted unit conversion: bar → kg/cm² is ×1.019716
(1 bar = 1.019716 kg/cm²), not ÷. With the fix Del Grosso at 10 °C/35 ‰/1000 m
gives 1506.31 m/s, agreeing with UNESCO (1506.52) and Mackenzie (1506.26) to
<0.3 m/s (was 1505.68). Update the cross-model test value and add an independent
Del Grosso conformance check (175/175).
* docs(underwater): address PR review (breadcrumb, examples, api-reference)
- add the documentation-index breadcrumb + descriptive H1 to the propagation
guide (repo convention)
- make the guide's absorption example self-contained (index the array, print α)
- clarify SonarEquationResult.noise_level (masking is RL when reverberation-
limited) and document Thorp's <50 kHz validity
- api-reference: add the reverberation-limited active formula and the full
SonarEquationResult field list
The brainstorming specs and implementation plans under docs/superpowers/ are
local working artefacts, like plan/ — not part of the published project. Stop
versioning them and add the directory to .gitignore. Files stay on disk.
* docs: implementation plan for wind-turbine noise (IEC 61400-11) PR-B2
* feat(wind-turbine): apparent sound power + tonal audibility chain (IEC 61400-11)
Add wind_turbine_noise.py: slant_distance/apparent_sound_power_level (Formula
26) and wind_turbine_tonality (Formulae 30-34) returning a
WindTurbineTonalityResult with .plot(). Named to avoid colliding with the
existing ISO 1996-2 critical_bandwidth/tonal_adjustment/tonal_audibility; the
K_T rating reuses the ISO 1996-2 tonal_adjustment already in the library.
* feat(wind-turbine): conformance domain (IEC 61400-11), 168/168
* feat(wind-turbine): tonal-audibility figure (x4 variants)
* docs(wind-turbine): EN guide + site EN/ES, api-reference, indices, sidebar
* fix(wind-turbine): correct low-frequency band and non-adjacent tone lines
Two correctness fixes from review, against IEC 61400-11:2012+A1:2018:
- The 20-70 Hz candidate uses the fixed absolute 20-120 Hz critical band, not
a band centred on the tone frequency (subclause 9.5.3). The previous
|f - fc| <= CBW/2 window only coincided with 20-120 Hz at fc = 70.
- Tone lines need not be contiguous with the peak. A1:2018 struck "adjacent";
every line in the critical band above the tone threshold and within 10 dB of
the peak is a tone. The Hanning /1.5 correction is now applied per contiguous
run rather than once for the whole set, so separated tones are summed
correctly.
Add tests for a low-frequency tone (excluding sub-20 Hz lines) and for two
separated tone lines.
* fix(wind-turbine): review pass — validation, shared band helper, plot color
- wind_turbine_tonality: validate strictly-increasing and uniformly-spaced
frequencies (the narrowband-spectrum precondition of Formulae 30-34)
- extract shared _critical_band_edges() so the plots use the fixed 20-120 Hz
low-frequency band instead of fc±CBW/2 (matches the classification logic)
- correct the WindTurbineTonalityResult docstring return type
- distinct masking-level line colour in the generated figure (no longer
blends with the grid)
- regenerate the 4 wind_turbine_tonality figure variants
- add regression tests for the frequency-axis validation
* fix(wind-turbine): address CodeRabbit review
- tighten the uniform-spacing tolerance to 1e-3·df so a near-miss axis (e.g.
2,2,2.4 Hz) that would bias df/ENBW is rejected; add a near-miss test
- fix a duplicate-keyword TypeError in plot_epnl / plot_wind_turbine_tonality
when a caller passes lw/label/color; merge defaults so caller values win;
add a plot-kwargs regression test
- remove the unused _AUDIBILITY_REPORT_LIMIT constant (the multi-spectrum
'No relevant tones' reporting rule is out of scope); is_audible stays ΔL_a>0
per IEC 61400-11 §9.5.8 ('a tone is audible if the tonal audibility is above
0 dB')
- add unit=dB to the apparent-power conformance row for table consistency
- document band_levels as background-corrected and the narrowband-spectrum
constraints in api-reference; make the guide code examples self-contained
- markdownlint-clean the wind-turbine plan doc
New aircraft-noise domain (PR-B1 of #19), clean-room from the standard text with the ICAO Doc 9501 ETM Vol. I worked examples as oracles:
- aircraft_noise — ICAO Annex 16 Vol. I App. 2 EPNL: perceived noisiness (Table A2-3), PNL, tone correction (slope method), and end-to-end EPNL with the 10 dB-down duration correction (EPNLResult + .plot()).
- verify_aircraft_noise_system — IEC 61265:1995 measurement-system tolerances (Table 1 directional response + scalar limits); one-third-octave filtering reuses the IEC 61260 class-2 verification.
Oracles: noy breakpoints; ETM Table 3-7 tone correction (C = 2.0 dB); ETM Table 4-4 integrated-method EPNL (92.6 EPNdB) including the largest-duration 10 dB-down rule; IEC 61265 Table 1 verbatim. Conformance 165/165. EPNL figure EN/ES x light/dark. EN + site EN/ES guides.
PR-B2 (wind-turbine noise, IEC 61400-11) follows. The full certification correction chain and airport contours are recorded as future phase #23.
New underwater-acoustics domain (PR-A of #19), clean-room from the standard texts:
- underwater_acoustics — ISO 18405:2017 reference levels re 1 µPa: SPL, SEL (re 1 µPa²·s), zero-to-peak, and 1 µPa <-> 20 µPa re-referencing.
- ship_radiated_noise — ISO 17208-1/-2: radiated noise level (re 1 µPa·m), equivalent monopole source level via the Lloyd's-mirror correction (d_s = 0.7·D), three-hydrophone geometry, tabulated uncertainty.
- pile_driving_noise — ISO 18406:2017: single-strike SEL, cumulative SEL (energy sum and N-identical), and per-strike metrics (SEL, peak SPL, Leq, 90 %-energy pulse duration) with .plot().
Conformance domain with 6 checks (161/161). Analytic oracles independent of the implementation, including an externally hand-computed Lloyd's-mirror transcription guard. Figures in EN/ES × light/dark; EN + site EN/ES guides, API reference and README.
Aeroacoustics (ICAO EPNL, IEC 61265/61400-11) is a separate follow-up PR (PR-B of #19).
* docs: design spec for electroacoustics distortion and frequency response
* feat: electroacoustic distortion and frequency response (IEC 60268-3 / AES17 / Bendat & Piersol)
Add two modules for audio-equipment characterisation from a captured signal.
distortion.py (IEC 60268-3:2013 + AES17-2015):
- thd (THD_F/THD_R), harmonic_distortion (nth-order)
- thd_plus_noise, sinad (AES17 standard notch, transient-trimmed residual)
- weighted_thd (A/C weighting of the residual)
- modulation_distortion (SMPTE), difference_frequency_distortion (CCIF),
total_difference_frequency_distortion, dynamic_intermodulation_distortion (DIM,
Table 2 nine products of the 15 kHz / 3.15 kHz signal)
- harmonic_analysis -> HarmonicDistortionResult with .plot()
frequency_response.py (Bendat & Piersol, Random Data 4e):
- transfer_function (H1 = Gxy/Gxx, H2 = Gyy/Gyx)
- coherence (ordinary gamma^2)
- FrequencyResponseResult with a Bode + coherence .plot()
Every quantity is verified against an exact analytic oracle (synthetic signals
with known harmonic/intermodulation amplitudes; a known LTI path with
gamma^2 = 1 noiseless and SNR/(1+SNR) with output noise). Adds a new conformance
domain (7 checks), tests, four-variant SVG figures, docs (EN + site EN/ES),
api-reference, indices and the astro sidebar.
* review: harden electroacoustics per AI-reviewer feedback
- distortion: reject a fundamental at/above Nyquist before the notch (clear
error instead of a SciPy crash); validate notch_q in weighted_thd for parity
with thd_plus_noise; narrow the harmonic peak-search window to ±0.1·f0 so a
nearby non-harmonic tone is not latched onto a harmonic bin.
- frequency_response: clamp noverlap to nperseg-1 (high overlap no longer
raises); use gyx != 0 instead of abs(gyx) > 0 for the H2 zero-guard.
- tests: assert the full nine-product DIM Table 2 list; add high-overlap,
above-Nyquist and weighted_thd notch_q validation cases.
- conformance: extract a shared _electro_tone helper (de-duplication).
- api-reference: document the window parameter of thd_plus_noise/harmonic_analysis.
* review: avoid float-equality zero-guard flagged by SonarCloud
Use |gyx|^2 > 0 for the H2 denominator guard instead of gyx != 0 — keeps the
cheaper (no sqrt) form gemini asked for while satisfying SonarCloud's
no-float-equality reliability rule.
* docs: design spec for psychoacoustic annoyance and fluctuation strength
* feat: psychoacoustic annoyance and fluctuation strength (Fastl & Zwicker)
Close the modern sound-quality set with two new modules.
Psychoacoustic annoyance (Fastl & Zwicker Eqs 16.2-16.4; origin Widmann 1992):
`psychoacoustic_annoyance(n5, sharpness, fluctuation_strength, roughness)`
computes PA = N5·sqrt(1 + wS² + wFR²) exactly, and
`psychoacoustic_annoyance_from_signal` derives the four sensations from a
calibrated signal (documented model-mixing caveat; F is free-field).
Fluctuation strength (Fastl & Zwicker Ch. 10; no ISO standard):
`fluctuation_strength_am_noise` is the exact closed form for AM broadband
noise (Eq. 10.2); `fluctuation_strength` is the Osses et al. (2016) signal
model, implemented clean-room and cross-checked against the paper's Table 1
literature values and the open SQAT reference (used only as a numeric oracle).
Calibrated so the 1 kHz / 60 dB / m=1 / 4 Hz AM tone reads 1.00 vacil by
construction; the AM-tone 70 dB modulation sweep tracks the literature at
Pearson r ≈ 0.98 with the 4 Hz band-pass peak. The Osses model is free-field
only (no diffuse variant), so the function takes no `field` argument.
- 27 new tests (exact PA/closed-form + signal cross-check); full suite 2266 pass
- conformance +3 (PA exact, Eq. 10.2 exact, calibration): 148/148, no drift
- EN/ES guides, api-reference, README, sidebar; two figures (4 variants each)
- `.plot()` on both result dataclasses
* fix: address PR review findings (PA/fluctuation strength)
- fluctuation_strength: guard _cross_covariance against catastrophic
cancellation — a (near-)constant band envelope could make dx·dy slightly
negative, and sqrt() then returned NaN (which slipped past the `denom <= 0`
test and poisoned the sum). Clamp the product to 0 (gemini/coderabbit).
- fluctuation_strength: precompute the Bark-inversion grid at module load
instead of rebuilding a 20k-point grid on every _bark_center_hz call (copilot).
- _plotting: let callers override color/edgecolor in plot_psychoacoustic_annoyance
via **kwargs instead of raising TypeError on a duplicate keyword (coderabbit).
- generate_graphs: the closed-form Eq. 10.2 maximum is at ∛50 ≈ 3.68 Hz, so
relabel the 4 Hz guide line as a reference (the marker already sits on the
true peak); update the ES string. Regenerate the 4 figure variants (coderabbit).
- docs: add the missing combined legend to the fluctuation-strength figure
snippet in the EN/ES guides (coderabbit).
- reference_data: sync the wFR worked-example comment with the constant (copilot).
* feat: objective audibility of tones in noise (ISO/PAS 20065:2016)
Engineering-method tonal audibility: the critical band about a tone
(Δfc, Formula 2) and its geometric corner frequencies (Formulae 3-5),
the critical-band masking level LG = LS + 10 lg(Δfc/Δf) (Formula 12),
the masking index av = -2 - lg[1 + (f/502)^2.5] (Formula 13), the
per-tone audibility ΔL = LT - LG - av (Formula 14), and the decisive
and energy-mean mean audibility over spectra (Formula 20/21).
Narrow-band front-end (Clauses 5.3.2/5.3.3/5.3.8, Annex D): from a
spectrum, mean_narrowband_level determines LS iteratively (energy
average with the -1.76 dB Hanning correction, dropping any line more
than 6 dB above the running LS until stable or fewer than five lines
remain each side), tone_level sums the tonal lines about a peak,
analyze_spectrum detects the distinct audible tones (peak detection +
distinctness criteria, Clause 5.3.4), and combined_tone_level performs
the multi-tone "FG" combination (Formula 17). The algorithm is confirmed
against the parent standard DIN 45681:2005-03 and its Annex J reference
program.
Conformance is anchored on the Annex E combustion-engine worked example
(Tables E.1/E.2/E.3): LS = 49.22 dB and LT = 67.96 dB from the spectral
lines, tone detection recovering {118.4, 137.3, 158.8} Hz, the FG
combined level LT = 72.15 dB, the per-tone audibility at 137.3 Hz, the
masking index at 137.3/592.2 Hz and the mean audibility of the five
spectra (144/144). A decisive audibility reproduced exactly needs the
complete spectrum (Table E.1 is truncated to one critical band).
- src/phonometry/tone_audibility.py: the full audibility chain, the
LS/LT spectral front-end, whole-spectrum detection + FG combination,
and ToneAudibilityResult
- _plotting.py: plot_tone_audibility (per-tone ΔL bars, decisive marked)
- generate_graphs.py: figure (4 variants) with ES translations
- conformance: 7 ISO/PAS 20065 checks (new domain)
- docs + site guides (EN/ES) + api-reference + sidebar
- tests: 50 tests, full suite 2225 passed
* feat: separate evaluation of two tones below 1000 Hz (ISO/PAS 20065 §5.3.8)
Add the two-tone resolution branch of Clause 5.3.8 (Formulae 18/19): when
exactly two tones share a critical band and both lie below 1000 Hz, the ear
can still resolve them, so they are rated separately instead of FG-combined
when their frequency difference exceeds
fD = 21·10^(1.2·|lg(fT/212)|^1.8) Hz
evaluated at the more prominent tone (the larger audibility ΔL).
New public helpers `two_tone_separation_frequency` (Formula 19) and
`resolve_tones_separately` (the Formula 18 + threshold decision). No ISO/PAS
20065 worked example exercises this branch (the Annex E band groups three
tones, so the "exactly two tones" rule never fires); the formula and decision
are implemented clean-room from the text and verified against the parent
standard DIN 45681:2005-03 Annex J reference program
(fD = 21 * 10 ^ (1.2 * Abs(Log(fT / 212) / Log(10)) ^ 1.8)). As a consistency
check, at the Annex E tones the threshold keeps them combined, matching that
example's FG grouping.
- 9 new logic/consistency tests; conformance +1 (now 145/145)
- EN/ES guides, API reference and module docstring updated
Add the EN 12354-5:2009 prediction of the receiving-room sound pressure
level from building service equipment that injects structure-borne sound
into the building. Final PR of the structural vibroacoustics series (#16),
closing the chain source (EN 15657) -> coupling (ISO 10846 / ISO 7626
mobilities) -> building transmission.
New module `installed_structure_borne`:
- `coupling_term` DC = 10 lg(|Ys+Yi+Yk|^2/(|Ys| Re{Yi})) (Formula 19b/e),
with force-source (19c) and velocity-source (19d) limits.
- `installed_structure_borne_power_level` LWs,inst = LWs,c - DC (18b).
- `structure_borne_pressure_level_path` Ln,s,ij = LWs,inst - Dsa - Rij,ref
- 10 lg(Si/S0) - 10 lg(A0/4) (18a, S0 = A0 = 10 m2) and
`total_structure_borne_pressure_level` energetic path sum (17).
- `InstalledSourceResult` (`.overall_level`, `.plot()`) and
`installed_source_prediction` for a multi-path prediction.
The source and receiver mobilities/impedances are those of
mechanical_mobility and transfer_stiffness; LWs,c comes from EN 15657.
Conformance is anchored on the coupling-term force-source limit, the
installed-power identity and the area/absorption terms of Formula 18a;
Dsa and Rij,ref are inputs (measurement / EN 12354-1 / Annexes D, F)
(137/137).
Adds the cascade figure (characteristic -> installed -> paths -> total),
the EN/ES guides, the docs index and api-reference rows, and the sidebar
entry.
Add the EN 15657:2018 characterisation of building service equipment as a
structure-borne sound source by its characteristic structure-borne sound
power level LWs, measured with the reception-plate method. Fifth PR of the
structural vibroacoustics series (#16); the LWs + plate mobility are the
source term of the EN 12354-5 installed-equipment prediction.
New module `structure_borne_power`:
- `spatial_mean_velocity_level` energetic spatial average (Formula 12).
- `plate_loss_factor` eta = 2.2/(f Ts) (Formula 13; delegates to the
ISO 10848 total loss factor).
- `structure_borne_power_level` LWs = 10 lg(2 pi f eta m S/(f0 m0 S0)) +
Lv - 60 dB (Formula 14; the -60 dB is 10 lg(v0^2/P0) for v0 = 1e-9 m/s).
- `StructureBornePowerResult` (`.total_level`, `.plot()`) and
`reception_plate_power` (loss factor given directly or via Ts).
Conformance is anchored on the resonant-plate power balance
P = omega eta (m S) <v^2> -- of which Formula 14 is the level -- and the
loss-factor identity (134/134).
Adds the module figure (low- vs high-mobility plate per band), the EN/ES
guides, the docs index and api-reference rows, and the sidebar entry.
Add the ISO/TS 7849 estimation of the airborne sound power a machine
radiates through the structure-borne vibration of its outer surface,
from the surface vibratory velocity and a radiation factor. Fourth PR of
the structural vibroacoustics series (#16); feeds ISO 9611, EN 15657 and
EN 12354-5.
New module `vibration_sound_power`:
- `velocity_level` Lv = 20 lg(v/v0) re v0 = 5e-8 m/s (Eq. 3) and
`velocity_level_from_acceleration` for a sinusoidal calibration (Eq. 8).
- `mean_velocity_level` energetic / area-weighted surface mean (Eq. 10/11).
- `radiation_factor` eps = P/(Zc <v^2> S) from a measured power (Eq. 4/8).
- `radiated_sound_power_level` LW = Lv + 10 lg(S/S0) + 10 lg(eps) +
10 lg(411/400) (Eq. 12/15): eps = 1 gives the Part 1 upper limit, a
measured eps the Part 2 engineering value.
- `extraneous_velocity_correction` K1A (Table 2).
- `VibrationSoundPowerResult` (`.total_level`, `.plot()`) and
`sound_power_from_vibration`.
Conformance is anchored on the standard's own worked calibration example
(a = 9,81 m/s^2 at 100 Hz -> Lv = 106,9 dB), the exact round-trip between
the radiation factor and LW = 10 lg(P/P0), and the fixed 10 lg(411/400)
impedance term (132/132).
Adds the module figure (Part 1 upper limit vs Part 2 engineering per
band), the EN/ES guides, the docs index and api-reference rows, and the
sidebar entry.
Add the ISO 10846 dynamic transfer stiffness k2,1 = F2,b/u1 of resilient
elements (vibration isolators, mounts, bellows, hoses) — the quantity
that characterises their vibro-acoustic transmission (Part 1, clause 5).
This is the third PR of the structural vibroacoustics series (#16) and
feeds ISO 9611, EN 15657 and EN 12354-5.
New module `transfer_stiffness`:
- `transfer_stiffness_level` Lk = 20 lg(|k2,1|/k0) re k0 = 1 N/m
(Parts 2/3, 3.17) and `loss_factor` eta = Im/Re (Part 1, 3.8).
- `transfer_stiffness_direct` k2,1 = F2,b/u1 (direct method, Part 2).
- `transfer_stiffness_indirect` k2,1 = -(2 pi f)^2 (m2 + mf) T from the
vibration transmissibility and a blocking mass (indirect method,
Part 3, Formula 1), plus `base_transmissibility` for the ideal
mass-loaded Kelvin-Voigt element used in examples and the figure.
- `TransferStiffnessResult` (`.level`, `.loss_factor`, `.magnitude`,
`.to("impedance"/"apparent_mass")`, `.plot()`) and
`indirect_transfer_stiffness_result`.
The dynamic stiffness is the reciprocal receptance of the FRF family, so
k = jw Z = -w^2 m_eff reuse the mechanical-mobility convert_frf pivot
(Part 1, Annex A / Table A.2).
Conformance is anchored on the standard's closed-form definitions: the
level of a decade of stiffness (120 dB re 1 N/m), the indirect inertia
relation, and the Table-A.2 identity k = jw Z (129/129).
Adds the module figure, the EN/ES guides, the docs index and
api-reference rows, and the sidebar entry.
Add the ISO 7626-1:2011 frequency-response-function family — the
motion-per-force FRFs (receptance, mobility, accelerance) and their
force-per-motion reciprocals (dynamic stiffness, impedance, apparent
mass) of Table 1 — plus the single-degree-of-freedom reference resonator
of Annex A that underpins the structure-borne source and transmission
standards (ISO 9611, ISO 10846, EN 15657, EN 12354-5).
New module `mechanical_mobility`:
- `convert_frf` converts between any two of the six FRFs, pivoting
through the receptance H (Y = jωH, A = −ω²H, reciprocals = 1/FRF).
- `sdof_receptance` / `sdof_mobility` / `sdof_accelerance` and
`resonance_frequency` for the viscously damped SDOF resonator
H = 1/(k − ω²m + jωc).
- `MobilityResult` (`.magnitude`, `.phase`, `.to(target)`, `.plot()`)
and `sdof_mobility_result`.
Conformance is anchored on the closed-form SDOF identities: the
driving-point mobility peak |Y(f0)| = 1/c, the static receptance
H(0) = 1/k, and the exact Table-1 reciprocity Z·Y = 1 (126/126).
Adds the module figure, the EN/ES guides, the docs index and
api-reference rows, and the sidebar section.
First module of the structure-borne / vibro-acoustic characterisation chain
that culminates in EN 12354-5. EN 29052-1 (= ISO 9052-1:1989) determines the
dynamic stiffness per unit area s' of resilient layers under floating floors
from the load-plate resonance:
- apparent_dynamic_stiffness s't = 4 pi^2 m't fr^2 (Formula 4)
- enclosed_gas_stiffness s'a = p0 / (d eps) (Formula 7)
- installed_dynamic_stiffness airflow-resistivity regimes (clause 8.2)
- natural_frequency f0 = (1/2pi) sqrt(s'/m') (Formula 2)
- floating_floor_resonance full chain -> DynamicStiffnessResult + .plot()
Conformance is anchored on the standard's own worked NOTE (s'a = 111/d MN/m3
for p0 = 0,1 MPa, eps = 0,9), reproduced exactly by the closed form, plus
hand-computed values of the resonance relations. s' feeds the ISO 16251 /
EN 12354-2 floating-floor impact improvement.
Docs (repo + bilingual site guide + API reference), the dynamic_stiffness
figure (4 variants), 3 conformance checks (123/123) and 17 tests.
* feat: reverberation-time prediction (Sabine, Eyring, Millington-Sette, Fitzroy, Arau-Puchades)
Add a general reverberation-time prediction module complementing the
EN 12354-6 enclosed-space model and the ISO 3382 measurement code. Five
statistical-acoustics models predict T from a room's volume, boundary
areas and surface absorption:
- Sabine, Eyring (Norris-Eyring) and Millington-Sette over an arbitrary
surface list;
- Fitzroy (area-weighted arithmetic mean) and Arau-Puchades (geometric
mean, Acustica 65, 1988, Formula 18) over the three wall pairs of a
rectangular room;
- the air-absorption term 4mV throughout (m from air_attenuation_m);
- reverberation_time_models + ReverberationModelResult.plot() to compare
all five per band.
k = 24 ln10 / c0 (0.161 at c0 = 343 m/s). Conformance is anchored on a
real worked oracle — Everest, Master Handbook of Acoustics 4th ed,
Fig. 7-22 Example 1, reproduced to <= 0.02 s after ft -> SI conversion —
reinforced by hand-computed closed-form values and the model identities
(all reduce to Eyring for uniform absorption; Eyring -> Sabine as a -> 0).
Docs (repo + bilingual site guide + API reference), the reverberation_models
figure (4 variants), 6 conformance checks (120/120) and 26 tests.
* build: regenerate reverberation_models figures with pinned matplotlib 3.11.0
The four new SVGs were first rendered with the local matplotlib 3.10.1,
whose glyph/path layout differs structurally from the CI-pinned 3.11.0
(requirements-figures.txt), tripping the figure-staleness check. Regenerate
them under the pinned toolchain so they match a fresh 'make graphs' on CI.
* feat: environmental-noise determination (ISO 1996-2 tonal adjustment + uncertainty)
ISO 1996-2:2017 is the determination part of the ISO 1996 environmental-noise
family (the descriptors Lden/Ldn live in ISO 1996-1, already in
phonometry.environmental). New module phonometry.environmental_measurement:
* Tonal adjustment (engineering method, ISO 1996-2:2007 Annex C):
tonal_audibility() forms ΔLta = Lpt − Lpn + 2 + lg[1 + (fc/502)^2.5] dB
(Formula C.3) and tonal_adjustment() the piecewise Kt (Formulae C.4-C.6);
assess_tonal_audibility() bundles them with the critical bandwidth (Table
C.1) into a plottable result. tonal_seeking_survey() is the one-third-octave
15/8/5 dB screen (Annex K) and tonal_adjustment_from_mean_audibility() the
Table J.1 mean-audibility route.
* Residual-noise correction: residual_sound_correction() (Formula 16, flags a
<3 dB margin as an upper bound) and gaussian_residual_level() (Annex I).
* Measurement uncertainty: combined_standard_uncertainty() (Formula 2),
environmental_expanded_uncertainty() (k=2/1.3), residual_correction_uncertainty()
(Formulae F.7-F.9) and uncertainty_from_repeated_measurements() (Formulae 17-20).
Unlike the removed 2017 engineering method (which defers to ISO/PAS 20065), the
detailed 2007/2009 Annex C algorithm is self-contained and has worked numeric
examples: conformance is anchored on Annex C.5 (ΔLta and Kt) and Annex G.2 (the
combined uncertainty u = 2.18 dB) -- three new checks (115/115, byte-stable).
The raw-FFT tone-detection pipeline is intentionally out of scope: the standard
gives no raw input spectra, so it could not be verified. 25 unit tests; docs
(GitHub + site EN/ES) get a new levels section with a figure and API rows.
* test: anchor ISO 1996-2 residual/gaussian/uncertainty tests on hand-computed values
Review found four tests re-derived their expected value from the same
arithmetic expression as the implementation, so a transcription error would
go undetected. Replace them with independent hand-computed literals (residual
correction Formula 16, Gaussian residual I.1/I.2, residual-correction
uncertainty F.9, repeated-measurement mean/uncertainty 18/20).
* fix: avoid assert for type narrowing in gaussian_residual_level (bandit B101)
The CI quality job runs bandit -r src, which flags assert statements (stripped
under python -O). Replace the mypy-narrowing assert with an elif/else-raise so
the branch is both bandit-clean and correctly narrowed.
ISO 10848:2006/2010 is the laboratory measurement counterpart of the EN 12354
flanking-transmission prediction: it measures the junction vibration reduction
index Kij that the prediction takes as an input, plus the overall flanking
descriptors Dn,f and Ln,f.
New module phonometry.flanking_transmission:
* vibration_reduction_index() -- Kij (Formula (13), or the simplified (14) for
lightweight well-damped elements), with the equivalent absorption length
(Formula (12)), the direction-averaged velocity level difference (Formula
(11), making Kij symmetric), octave-band combination and the single-number
mean Kij over 200-1250 Hz (Annex A). Returns a plottable
VibrationReductionResult.
* velocity_level_difference() / direction_averaged_level_difference() /
total_loss_factor() / equivalent_absorption_length() -- the building blocks.
* normalized_flanking_level_difference() (Dn,f, Formula (4)) and
normalized_flanking_impact_level() (Ln,f, Formula (5)), rated through the
verified ISO 717-1/-2 engines.
* vibration_reduction_index_from_flanking() -- the indirect Kij from Dn,f.
* Validity criteria: strong_coupling_satisfied() (Formula (15)),
critical_frequency() (Formula (20)), and the Part 4 modal-density /
band-mode-count / modal-overlap-factor checks (Formulas (5)/(4)/(6)).
ISO 10848 contains no worked numeric example, so conformance is anchored on
closed-form identities (simplified Kij, aj at f_ref, the total loss factor) --
three new checks in the conformance report. 29 unit tests cover the closed
forms, the Kij symmetry and simplified/full-formula relationship, the octave
conversion, the Dn,f/Ln,f ratings, the validity criteria and the plotting.
Docs (GitHub + site EN/ES) get a new section 8 with a figure and the API
reference rows; the measured Kij feeds the existing EN 12354 flanking model.
* fix: make documentation-figure rendering deterministic across machines
The "Documentation figures up to date" CI job was intermittently failing on the
heavy compute figures (excitation_signals, multiple_shock, sii_vocal_efforts,
sottek_specific_loudness, tonality_roughness_demo): their multi-threaded
numerical backends reorder floating-point reductions depending on the available
core count, so the CI runner produced byte-different SVG/PNG output than the
committed (single-threaded) baseline, and a plain re-run usually "fixed" it.
Pin the numerical thread pools to a single thread so rendering is reproducible
regardless of the host core count:
- generate_graphs.py / generate_diagrams.py set OMP/MKL/OPENBLAS/NUMEXPR/NUMBA/
VECLIB thread counts to 1 before the numeric backends import (covers direct
invocation).
- The Makefile `graphs` target also exports those plus PYTHONHASHSEED=0 before
the interpreter starts (PYTHONHASHSEED cannot be set from within the process);
CI runs `make graphs`, so it inherits this with no workflow change.
Regenerating every figure under these settings produces a zero-byte diff against
the committed images, confirming the current baseline is already the
single-threaded canonical output — so no figure files change here.
* fix: compare documentation figures within tolerance, not byte-exact
The "Documentation figures up to date" job regenerated .github/images with
`make graphs` and required a byte-identical result. That is unachievable on
GitHub's hardware-heterogeneous runner fleet: the pinned software stack
computes a few plotted path coordinates ~1 ULP apart depending on which CPU
microarchitecture (SIMD kernel) the run lands on, so the job failed
intermittently even though the figures were numerically and visually
identical. Single-thread pinning removes intra-machine reduction races but
cannot remove this cross-CPU last-bit difference.
Replace the byte diff with scripts/check_figures.py, which compares the
freshly regenerated figures against the committed versions within a tolerance:
* SVG -- non-numeric structure (elements, text, colours, ordering) must match
exactly; every numeric token must agree within an absolute/relative
tolerance. A moved element or relabelled axis fails; a last-bit
coordinate wobble passes.
* PNG -- identical dimensions, at most a small number of pixels changed beyond
a level threshold (catches localised changes a global RMS dilutes),
and a bounded RMS (catches broad changes the pixel count misses).
* other -- exact byte compare. Added/removed files always fail.
The thread-pinning determinism work from the previous commit is retained (it
still removes genuine intra-machine flakiness); this makes the check robust to
the cross-machine floating-point non-determinism it cannot eliminate.
* fix: harden figure check against hash-id drift and orphan figures
Two robustness gaps found reviewing the tolerance-aware figure check:
* SVG clip-path / marker ids are SHA256 hashes of the underlying float
geometry (matplotlib backend_svg._make_id). A cross-CPU 1-ULP wobble in that
geometry avalanches the whole hash, so the id text would change and trip the
strict structural gate -- the very drift the check exists to tolerate. Now
canonicalise every id and its url(#...)/href="#..." references to
first-appearance placeholders before comparing, so equivalent documents match
regardless of hash text while a genuine id add/remove/reorder/repoint fails.
* The generators only overwrite files, never delete, so a figure that is no
longer produced would survive on disk byte-identical to HEAD and pass the
"committed figure no longer generated" check. Clear the generated SVG/PNG at
the start of the `graphs` target before regenerating (animations *.gif/*.webm
are from the separate `animations` target and are preserved). A full
clear+regenerate reproduces the committed tree exactly, confirming no
committed figure is orphaned.
Add the laboratory method for the improvement of impact sound insulation ΔL of
soft, locally-reacting floor coverings (carpet, PVC, linoleum) measured on a
small concrete mock-up via structure-borne acceleration levels, plus the
ISO 717-2 weighted improvement ΔLw it feeds.
- weighted_impact_improvement (insulation.py): ISO 717-2:2020 Clause 5 ΔLw =
78 - Ln,r,w using the heavyweight reference floor of Table 4, reusing the
verified weighted_impact_rating engine
- floor_covering_improvement.py (ISO 16251-1):
- acceleration_level (Formula 1, a0 = 1e-6 m/s²)
- background_corrected_level (Formula 2, three-branch rule with a
limit-of-measurement flag)
- impact_improvement: Formula (2) per position -> difference (3) -> mean over
positions (4); accepts (bands,) or (positions, bands) levels
- improvement_octave_bands (Formula 5)
- FloorCoveringImprovementResult with ΔLw, the > ΔL limit mask and .plot()
- Conformance checks: ISO 717-2 reference-floor rating (78 dB, CI -11) and the
ΔL=0 -> ΔLw=0 identity (109/109). ISO 16251-1 has no worked numeric example
(Annex B is a blank form), so the oracle is the ISO 717-2 reference floor.
- generate_floor_covering_improvement figure (EN/ES × light/dark)
- Building-acoustics guide §7 and API reference rows (EN + ES)
- 20 tests covering all formulas, the per-position ordering and validation
Add the sound-absorption companion of the ISO 12999-1 insulation uncertainty:
the standard uncertainty u of the quantities produced by a reverberation-room
measurement (ISO 354) and its ratings (ISO 11654, EN 1793-1).
- sound_absorption_coefficient_uncertainty: sigma_R = m*alpha_s + n
(Table 1, 1/3-oct 63-5000 Hz), sigma_r = 0.6*sigma_R
- equivalent_area_uncertainty: sigma_R = m*A_T + n*S, S = 10 m² (Formula 2)
- practical_coefficient_uncertainty: sigma_R = m*alpha_p + n
(Table 2, octave 250-4000 Hz)
- weighted_coefficient_uncertainty (alpha_w: 0.035 / 0.020) and
single_number_rating_uncertainty (DLalpha,NRD: 0.10*DLa / 0.02*DLa)
- absorption_coverage_factor: Table 3 rounded factors (2.0 at 95 %), distinct
from ISO 12999-1's Gaussian-exact ones
- AbsorptionUncertaintyResult with exact U = k*u and a Clause 8
reporting-rounded view (.reported_expanded_uncertainty), plus .plot()
- Conformance checks reproducing the standard's worked Tables 4/5 and
Examples 1/2 exactly (107/107)
- generate_absorption_uncertainty figure (EN/ES x light/dark)
- Acoustic-materials guide section 4 and API reference rows (EN + ES)
- 24 tests covering every formula, the worked examples and validation
The naming avoids the existing scattering_diffusion.absorption_coefficient_
uncertainty (a GUM propagation from reverberation times); this module uses the
full "sound_absorption_coefficient" name for the ISO 12999-2 tabulated value.
* feat: field survey method (ISO 10052:2021)
Add the survey (simplified) method of ISO 10052:2021 for airborne,
impact, façade and service-equipment sound insulation, plus a Table 4
reverberation-index estimator for when T is not measured.
- reverberation_index (k = 10·lg(T/T0)) and estimate_reverberation_index
(Table 4 lookup by receiving-room volume and furnishing type)
- survey_airborne_insulation: D, DnT, Dn, R' with the V/7.5 effective
area floor (Clause 3.6)
- survey_impact_insulation, survey_facade_insulation
- survey_service_equipment_level: 3-position energy average (Clause 3.16)
- SurveyAirborne/Impact/Facade/ServiceEquipment result dataclasses with
ISO 717 ratings and .plot() where a single-number rating applies
- Conformance checks (R' area rule, service equipment, Table 4 estimate)
- generate_survey_insulation figure (EN/ES × light/dark)
- Building-acoustics guide §6 and API reference rows (EN + ES)
- 22 tests covering all formulas, the estimator and validation
* review: address bot findings on the ISO 10052 survey PR
- estimate_reverberation_index: normalize the room label (strip + lower)
so " Kitchen "/"KITCHEN" resolve, and name the actual volume range in
the "not tabulated" error instead of an internal index
- survey_service_equipment_level: validate measurement dimensionality so
a scalar input raises a clean ValueError, not IndexError
- fix the four survey-function docstring citations to ISO 10052:2021
- fix the Table 4 volume-range comments to 60 <= V <= 150 (inclusive)
- conformance report: got.tolist() for plain floats in the k estimate row
- docs (EN + ES): document that reverberation_index accepts a scalar k
for service equipment; localize "Table 4" -> "Tabla 4" and "m3" -> "m³"
- add tests for room normalization, the range-named error and the
scalar-measurements guard
* feat: sound insulation by intensity (ISO 15186-1:2000)
Adds the sound-intensity method as the direct-power counterpart to the
ISO 10140 laboratory pressure method — the tool of choice when flanking
transmission defeats the traditional method.
New module `intensity_insulation.py`:
- `intensity_sound_reduction` — intensity sound reduction index
RI = Lp1 - 6 - [LIn + 10 lg(Sm/S)] (Formula (7)); also the field apparent
R'I (ISO 15186-2) and the Kc-modified RI,M = RI + Kc (Formula (9)).
- `adaptation_term_kc` — Annex B term: exact Formula (B.1)
10 lg(1 + Sb2·λ/8V2) for a well-defined room, or the room-independent
approximation (B.2) 10 lg(1 + 61,4/f); both with c = 340 m/s so (B.1) with
the reference room reduces to (B.2).
- `intensity_element_normalized_difference` — DI,n,e (Formula (8)).
- `surface_pressure_intensity_indicator` — FpI qualification (Formula (10)).
- `combine_subareas` — per-subarea energy average (Formulas (11)-(12)).
Weighted ratings (RI,w, RI,M,w, DI,n,e,w) reuse the verified ISO 717-1
`weighted_rating` engine; result objects mirror `lab_insulation.py`
(frozen dataclasses, `.plot()` delegating to the rating).
Adds 19 tests, two conformance checks (RI reproduces the ISO 717-1 Rw=30
through the intensity path; Annex B B.1 reduces to B.2), the deterministic
`intensity_insulation` figure (EN/ES × light/dark), and a §5 in the
building-acoustics guide (docs + site EN/ES) plus API-reference rows.
* Address bot review — ES figure localisation, nominal centres, table escaping
- generate_graphs.py: the intensity_insulation info box is now data-only
(RI,w / RI,M,w), so the untranslatable multiline annotation is gone; add an
_ES_EXACT entry for the "RI (intensity)" legend label. Regenerated the four
deterministic SVG variants — the ES figure is now fully localised.
- docs + site EN/ES: the RI,M example uses the nominal one-third-octave centres
(100-3150 Hz) instead of np.geomspace, matching the "1/3-octave bands" claim;
printed values are unchanged. Same in the test.
- conformance_report.py: the Annex B check message uses abs(B.1 - B.2) instead
of |B.1 - B.2| so the pipes no longer break the CONFORMANCE.md table row;
regenerated the report.
- intensity_insulation.py: restructure adaptation_term_kc's branch so mypy
narrows both room parameters, dropping the type: ignore.
* feat: IEC 61260:1995 / ANSI S1.11-2004 class 0 filter verification
verify_filter_class and class_limits gain an `edition` argument. The default
"2014" path (IEC 61260-1:2014, classes 1 and 2) is unchanged; edition "1995"
selects the withdrawn IEC 61260:1995 / ANSI S1.11-2004 Table 1, which adds the
stricter laboratory-grade class 0 and whose class 1/2 masks differ slightly
from the 2014 edition.
The 1995/ANSI octave-band Table 1 was transcribed digit-for-digit and verified
identical between the two standards (dual-oracle clean-room). The fractional-
octave breakpoint mapping is shared: 1995 Annex B equation (10) is identical to
2014 Formula (9), so the existing _map_breakpoint is reused for both editions.
The library default (Butterworth order 6) already meets class 0 across the usual
configurations (octave/third-octave, 32-96 kHz); the non-Butterworth
architectures cannot reach the class mask by construction (ripple / flat group
delay), so defaults are unchanged and the class each architecture reaches is
documented instead.
Adds a class-0 conformance check (99/99), the EN/ES filter_class0_mask figure,
tests cross-checked against a shared reference_data copy, the "Class 0" doc
section (guide EN/ES + API refs EN/ES) and regenerated CONFORMANCE.md /
llms-full.txt.
* docs: address bot review — scope class-0 claims, fix English decimals
- compliance.py: English docstring tolerances use dot decimals and ± symbol
(were decimal commas, inconsistent in an English context).
- api-reference (docs + site EN/ES): qualify overall_class by edition
(1/2/None for "2014"; 0/1/2/None for "1995"); neutralize the ES headers that
still said "IEC 61260-1:2014" while documenting the edition switch.
- filter-banks (docs + site EN/ES): narrow the class-0 wording to the verified
order-6 Butterworth octave/third-octave banks at 48 kHz; drop the untested
32-96 kHz range and advise re-running verify_filter_class away from those
settings.
- generate_graphs.py: fix misleading "filter_class0_mask.png" print (output is
SVG).
- test_compliance.py: assert the class-0/1/2 minimum ordering the docstring
already promised (previously only the maximum ordering was checked).
* feat: façade insulation & outdoor radiation prediction (EN 12354-3/-4:2000)
New module phonometry.facade_prediction implementing the two building-envelope
prediction directions on a shared energy summation of element transmission
factors:
- EN 12354-3 (outdoor -> indoor): facade_sound_reduction() -> apparent R'
(Formula 10), the loudspeaker/traffic indices R45/Rtr,s (11/12) and the
standardized level difference D2m,nT (13), with ISO 717-1 single numbers.
- EN 12354-4 (indoor -> outdoor): radiated_sound_power() -> segment R' and the
radiated power level LW (Formula 2), plus the simplified Annex E outdoor
attenuation (outdoor_attenuation / outdoor_level).
FacadeElement models an area element (R), a small element / air path (Dn,e) or
an opening (insertion loss); FacadePredictionResult and RadiatedPowerResult are
frozen dataclasses exposing .plot().
Validated clean-room against the Part 3 Annex F and Part 4 Annex G worked
examples: the low octave bands, every single-number rating and the whole Annex E
propagation reproduce the published values exactly (3 new conformance checks,
98/98). The standards' own worked examples carry documented internal rounding
inconsistencies at the higher bands, tracked in the reference-data notes.
Adds the EN/ES facade_prediction figure (SVG, deterministic), the EN 12354-3/-4
section in the building-acoustics guide (EN + ES site), API-reference rows and
regenerated CONFORMANCE.md / llms-full.txt.
* fix: avoid assert for type-narrowing in FacadeElement.tau (bandit B101)
The rest of src/ avoids assert (removed under python -O); fetch the single
non-None quantity via getattr, matching the _band_count pattern, so mypy is
satisfied without an assert.
* review: harden facade_prediction per bot review
- Fix a real bug: _apparent_reduction keyed transmission factors by element
name in a dict, so two elements sharing a name silently dropped one from the
R' sum. Sum over the element list and require unique names (they key the
per-element result). (gemini)
- outdoor_level: allow NumPy broadcasting (scalar Atot vs per-side LW array).
- Validate 'frequencies' / 'octave_bands' length against the band count. (Copilot)
- Guard tau() against non-positive total_area; include the element kind in
finite-check error labels. (gemini/Copilot)
- Docstring: bands accepts 'third-octave' (not 'third'). (Copilot)
- Tests for all of the above.
* docs: address CodeRabbit review on facade prediction docs
- Fix worked-example prose: the snippet has a window + a skylight, not
'two windows' (docs + EN/ES site guides).
- Complete the façade parameter table with facade_sound_reduction(frequencies),
radiated_sound_power(octave_bands) and outdoor_level() rows (docs + EN/ES).
- Document the optional 'frequencies' parameter in the facade_sound_reduction
API-reference rows (docs + EN/ES).
- Regenerate llms-full.txt.
The Major duplicate-name finding was already fixed in af23efc.
Add `verify_weighting_class(wf)` and `weighting_class_limits(class)`, the
weighting-domain counterparts of the existing `verify_filter_class` /
`class_limits`. The verifier evaluates a WeightingFilter's designed response
at each Table 3 nominal frequency below Nyquist, subtracts the design-goal
weighting and reports the per-frequency and overall performance class (1, 2
or None) with dB margins. The class 1 and class 2 acceptance masks and the
A/C/Z design goals are transcribed from BS EN 61672-1:2013 Table 3 (standard
page 22); the class 1 columns are cross-checked in the tests against the
independent reference_data copy shared with the CI conformance report, and a
second test cross-validates the deviations against a time-domain tone RMS.
Includes a weighting_class_mask figure (A/C deviation threading the class 1
corridor with the wider class 2 limits) wired into `make graphs`, and docs in
weighting.md + the site (EN/ES) and the API reference.
Also harden the "Documentation figures up to date" CI job: pin the full
render+compute stack (matplotlib/fonttools/pillow/numpy/scipy/...) in
requirements-figures.txt instead of matplotlib alone. A newer numpy or
fonttools silently shifts the computed path coordinates / text layout of a
few figures, which flaked the byte check.
Class 0 for filters is intentionally out of scope: IEC 61260-1:2014 dropped
it, and the withdrawn IEC 61260:1995 / ANSI S1.11-2004 class-0 masks use
class 1/2 limits that differ from the 2014 edition the filter verifier is
built on, so they cannot be mixed in consistently.
Move the 68 vector documentation figures from PNG to byte-reproducible
SVG (4 EN/ES x light/dark variants each) and swap every doc/site embed
to match. Five figures stay PNG because SVG would be strictly heavier:
the two raster-backed plots (spectrogram_example, excitation_signals)
and three dense time series whose SVG runs 5.5-7.75x the PNG
(schroeder_decay, calibration_stability, impulse_response) -- all clear
the ~4.5x "SVG no longer wins" bar.
Reproducibility:
- SVG: fixed svg.hashsalt, svg.fonttype=none (text as <text>, no freetype
glyph paths), no date metadata -> byte-identical run to run and across
font builds.
- PNG: strip matplotlib's version-stamped Software chunk so the bytes are
reproducible across matplotlib builds too.
- Verified: a fresh `make graphs` reproduces all 452 images byte for byte.
Add a `figures` CI job (mirror of `conformance`) that regenerates the
figures with the pinned matplotlib and fails on any drift in
.github/images, staging first so added/removed files are caught too.
* Audit pass 11b: Tier-1 documentation animations
Add the four Tier-1 animation clips the audit prioritised — level-vs-time
phenomena the library already computes, rendered deterministically by
generate_graphs.py:
- time-weighting ballistics: the Fast/Slow/Impulse detectors chasing a tone
burst, Impulse decaying slowest (IEC 61672-1);
- onset detection: an L_AF history with the >10 dB/s onset highlighted and
OR/LD/P/KI shown live (NT ACOU 112);
- instantaneous intensity: p·u flowing for a progressive wave vs sloshing
about zero for a standing wave (IEC 61043);
- Schroeder backward integration: the decay curve emerging from the tail,
ending with the T20/T30 lines (ISO 3382).
Each is produced in the four language x theme variants as a compact WebM for
the site (embedded as an autoplaying, looping <video>) and, for English, an
animated GIF for the GitHub docs. A new `_translate_str` localises the
per-frame labels (the clips never pass through themed_path), the site gains
<video> theme-switching CSS, and `make animations` (kept out of CI, since
video is not byte-reproducible) regenerates them.
* Address review of the animations PR
- generate_animations: fail fast with a clear message when ffmpeg is absent
(Gemini).
- Schroeder _fit: guard against fewer than two points in a T20/T30 range and
a non-decaying slope, and keep xmax robust when a fit is unavailable
(Copilot).
- Site <video>s: add native `controls` (a keyboard-accessible pause/stop for
the looping motion, WCAG 2.2.2) and move the description to `title`, dropping
the `role="img"`/`aria-label` pair so the controls stay exposed to assistive
tech (CodeRabbit).
- Spanish: "energy sloshes" -> "la energía va y viene" (technical register;
ES intensity clips regenerated) (CodeRabbit).
- Group `no-autoplay` with the other `no-*` html-validate rules (CodeRabbit).
* Fix stale 'no controls' comment in theme-images.css
The Tier-1 <video>s now carry native controls; update the shared CSS comment
to match (CodeRabbit).
* Audit pass 11a: seven new documentation diagrams
Add SVG flow diagrams for the seven topics the audit flagged as having no
schematic: the exponential-detector chain of the time weightings
(IEC 61672-1), stateful vs reset block processing, the multichannel
array-shape flow, the open-plan spatial-decay line (ISO 3382-3), the
ISO 12999-1 uncertainty pipeline, the ISO 11654 absorption-rating flow and
the Zwicker loudness-model chain (ISO 532-1).
Each is generated deterministically by generate_diagrams.py in the four
EN/ES x light/dark variants, with every Spanish label added to the _ES
table (decimals localised to commas), and embedded in the repo docs and
both site trees beside the existing worked-example plots.
* Fix ISO 532-1 table citations in the Zwicker diagram
Review of the diagram against loudness_zwicker.py found two mislabelled
tables: the equal-loudness correction and lower-critical-band grouping is
Table A.3 (not A.4), and the threshold in quiet LTQ is Table A.6 (not A.3).
Reassign the a0/DDF/LTQ corrections to the core-loudness step (Tables
A.4-A.7) where they belong, update the Spanish labels, and align the ES
psychoacoustics alt text with the guide's own term "sonos" (was "sonios").
* feat: complete the .plot() convention and tidy the plotting layer
Audit batch 10:
Every result dataclass with a plottable series now exposes .plot()
(eleven new): open-plan spatial decay with the Clause 6.2 regression
and rD/rP markers; outdoor attenuation with signed pos/neg stacking (a
net ground gain stacks below zero); impedance-tube alpha(f) with the
muted |r| companion; Monte Carlo histogram with the coverage interval -
via a new opt-in monte_carlo(keep_samples=True) that stores the sample
on the result (the 8 MB/1M-trial cost stays off the default path, and
.plot() without samples raises with the hint); occupational-exposure
per-task contributions with the LEX and LEX+U lines; plus simple
renderers for static airflow, airborne/impact prediction,
airborne/impact insulation bands and band uncertainty. TransferMatrix
and ScatteringUncertainty are documented skips (value objects with no
stored frequency axis). The five priority plots are mentioned in their
guides on all three surfaces.
Plotting hygiene: _freq_axis in the enclosed-space renderer (minor-tick
suppression restored); shared _band_axis/_fractile_band/_hatch_invalid
helpers and plot_weighted_absorption folded into _plot_rating; named
color constants with the three neutral greys collapsed into one _C_MUTED
(the per-metric tonality red and roughness brown stay, commented);
every title now leads with its standard designation (17 aligned, with
per-type designations where one renderer serves several standards);
band x-labels unified to the dominant wording; the 11 result: Any
renderers typed; kwargs documented; the missing small legend fixed;
user color propagates into the companion fill_between in the five
loudness/tonality/roughness renderers.
Tests: 11 new kwargs-forwarding cases, 13 content/raise tests, and the
external-ax contract extended to all single-axes results (64/64).
* fix: address the review findings on the plotting batch
- plot_monte_carlo lets callers override density (setdefault instead of
a hardcoded kwarg that collided) (Copilot)
- plot_open_plan imports matplotlib.ticker after the lazy axes creation
so a missing matplotlib still raises the actionable hint (Copilot)
- the exposure plot computes its y-limits from the data instead of
clamping at 0 dB (negative levels no longer clip) (Copilot)
- the sound-power legend also renders when the caller passes label=
(Gemini)
- the insulation plot annotations are quoted for consistency (the file
already had future annotations, so this is style, not a crash fix)
(Gemini)
- the GUM example comment now says honestly that the committed figure
overlays the Gaussian, and the open-plan one-liner snippet gains
plt.show(), on all surfaces (CodeRabbit)
* test: the tests batch of the audit
Audit batch 9 - the test layer hardens, with src/ numerics untouched:
reference_data drift closure: the test files that duplicated oracle
values as literals now import the shared constants (11 files), with a
rewritten module docstring describing the actual regime; the EN 12354-6
Annex E surfaces move into reference_data (test + conformance import
them); all 8 previously-dead constants are wired into asserts instead
of deleted.
Conformance: 90 -> 95 checks across 22 domains, all passing and
byte-stable. New: ECMA-418-1 critical-band and proximity anchors (new
Prominent discrete tones domain), an ISO 10534-2 synthesize->recover
identity, the ISO 717-2 Annex C.1 impact rating, and an ISO 18233 sweep
deconvolution vs freqz check - each with a tolerance-rationale comment
(new convention). The STI domain is now Speech transmission
(IEC 60268-16), the ISO 9613-1 checks live under outdoor propagation,
and the registry floors are realistic.
match= backfill: 98 bare pytest.raises(ValueError) sites now pin the
actual message; one test found passing via the wrong validation path is
fixed to exercise the intended error.
Performance: the eight heavy files drop from 203 s to 148 s and the
full suite from ~360 s to 288 s (1785 tests) - module-scoped STIPA
fixture, shortened property-test signals with measured-margin comments
(exact anchors kept at the length their tolerance needs, with the
measurements documented), and the zero-margin cached<uncached
performance assert gains a 1.5x margin (it flaked on CI by 0.06 ms).
Small gaps: 13 zips gain strict=True and 3 become itertools.pairwise;
StatefulWeightingFilter gains its three missing invalid-input tests;
utils.py verified as having no raising paths.
* test: show both chained values in the ISO 10534-1 check display
A |r| mismatch used to fail the check while displaying only matching
absorption values; the expected/computed strings now carry both, like
the ECMA-418-1 check does (CodeRabbit).
* test: escape the pipes in the ISO 10534-1 display strings
Unescaped | inside GFM table cells broke the CONFORMANCE.md rendering
(CodeRabbit); regenerated byte-stable.
* docs: traceability fixes and the target taxonomy
Approved conclusions of the standards-traceability report:
Traceability (P1):
- the room-acoustics footer claimed ISO 3382-3:2022 while the code
implements and verifies :2012 - the footer now tells the truth
- CONFORMANCE.md becomes visible: a Reference page on the site (EN/ES)
explains the report and links the generated table (copying 300
auto-generated lines would drift), docs/README.md indexes it, and
why-phonometry links it after its comparison table
- nine citation patches with honest scoping: the ISO 1996-2 facade
claim reworded as measurement context; ECMA-74 out of the
tone-prominence footer (context note added); ISO 9613-1 declared in
surface-scattering with clause delimitation; materials cites the
BS EN ISO 10534:2001 editions the code cites; the EN 12354 typo;
ISO 266 and ISO 5725 added with narrow scope; IEC 61252 designations
unified
- class_limits exported (it was public-but-unimportable), documented in
the API reference and surfaced in filter-banks section 6
Taxonomy (approved target, sidebar moves only - zero URL changes):
- renamed: Signal processing & filters
- new groups: Instrumentation, calibration & compliance; Materials &
surfaces
- Perception & speech splits into Psychoacoustics (+tone-prominence),
Speech & intelligibility, and Hearing & occupational exposure
(+occupational-exposure)
- docs/README.md mirrors the new order; Conformance joins Reference
* docs: terminology polish on the new conformance pages
key differentiator (not 'differential asset'), governing band/frequency
(not 'binding'), pasa/no pasa, expresiones en forma cerrada, 'en la que
se basan las etiquetas nominales', and air-attenuation relations in
both languages (Gemini)
* docs: retrofit the house conventions onto the legacy guides
Audit batch 8 - the pre-convention guides catch up with the style the
newer ones follow, on all three surfaces (repo, site EN, site ES):
- Standards footers for the ten guides that cited clauses inline but
never closed with the normative scope (calibration, filter-banks,
intensity, levels, time-weighting, weighting, outdoor-propagation,
sound-power, psychoacoustics, plus materials synced from the site
style and a new room-acoustics footer); block-processing,
multichannel and getting-started verified as having no normative
citations to footer
- every touched snippet is now self-contained or carries an explicit
reuse annotation, and ~20 prints gained expected outputs EXECUTED
against the library (levels LN values re-verified, materials
# 222956, human-vibration VDV/MTVV/crest, getting-started ~91 dB
bands, Zwicker sone/phon values, enclosed-space RT spectrum)
- 12 figure-reproduction <details> blocks added (materials x3,
outdoor-propagation x2, time-weighting x2, weighting x3,
speech-intelligibility, getting-started - with an honest note where
the committed figure overlays extra context), all verified runnable
- structural reorders, content-preserving: the exponential detector now
precedes the F/S/I modes; the filter parameter table follows the
comparison it references; weighting opens with 'Where the curves come
from'; block-processing gains a motivating opening and drops its
duplicated constraints list; the raw LaTeX in human-vibration code
spans becomes proper math; F2/F3/F4 defined in prose in intensity
- sound-power SS1-3 gain result figures: three new deterministic
generators (12 PNG variants, ES translations, visually verified)
embedded with captions and <details> using each result's .plot()
* docs: address the review findings on the legacy-conventions batch
- NullFormatter imported from matplotlib.ticker in the SII snippet
(plt.NullFormatter does not exist) (Copilot)
- eps floor inside the log10 of the three FFT-magnitude plots in the
weighting snippets, verified warning-free with -W error (Copilot)
- the getting-started figure snippet now reproduces the committed
figure including the gray PSD overlay, note removed (CodeRabbit)
- 8-tau settling is ~99.97 %, not 99.8 % (CodeRabbit)
- the intensity result generator drops its blanket warning suppression
(this data fires none) and masks non-finite bands instead of
nan_to_num zero bars; regenerated variants are byte-identical
(Gemini, CodeRabbit)
- the outdoor attenuation stacks use separate positive/negative
baselines - the 63 Hz ground term (-4.65 dB) now hangs below zero in
BOTH the snippet and the committed generator figure, whose four
variants regenerate (CodeRabbit, Major)
- the IEC verification claim narrows to what is true: the example
verifies the 200 ms Fast row; CI covers Table 4 down to 1 ms for F
and 2 ms for S (CodeRabbit)
- freqs = res.frequencies added to the three sound-power by-hand blocks
(CodeRabbit)
* docs: bilingual EN/ES consistency batch
Audit batch 7 - the Spanish side catches up with the English one:
Content drift: the five per-architecture filter figures join the ES
filter-banks guide; the missing outdoor-propagation card joins the ES
index (7 cards on both sides); the sound-power card says five methods
in both languages (it said three since before ISO 3745/9614-3).
Decimals: the four prose dot-decimals and four code-comment
comma-decimals fixed per convention, and the systematic LaTeX {,}
migration lands - 146 conversions across 16 ES files, every hit
manually reviewed (clause numbers, standard designations, def.:
literals and code identifiers deliberately untouched; zero \d.\d
remain in ES math). The theory band-edge table keeps points (it
mirrors a code snippet's printed output); the why-phonometry report
tables convert to commas (prose).
Terminology unified: fonios, diezmado (including the divergent
multirate alt pair), umbral de audicion, banda atenuada, nivel de
evaluacion (including the rendered NT ACOU 112 ES diagram - the
generator string is fixed and only that SVG pair regenerated),
sharpness (agudeza), and clausula/apartado unified per file majority
(theory: clausula; api: apartado).
Code comments: 22 English comments translated across gum-uncertainty,
hearing-threshold, noise-induced-hearing-loss, room-noise and
occupational-exposure ES (the occupational f-string outputs were
re-executed and match the echoed comments byte-for-byte).
* docs: 'atenuación en banda atenuada' in the ES filter guide
The glossary fix produced a clumsy repetition ('atenuación de banda
atenuada'); the natural phrasing for stopband attenuation is 'en'
(Copilot).
* docs: the sound-power cards enumerate the guide's five routes exactly
The card grouped ISO 9614-2/3 into one item, making 'five routes' read
as four groups (or six standards); it now mirrors the guide's canonical
five-item enumeration, in both languages (CodeRabbit).
* docs: split the three overloaded guides
Audit batch 6 - reorganization with full content preservation (verified
line-by-line against HEAD across the three surfaces):
- room-acoustics (1096 lines) keeps ISO 18233 + ISO 3382-1/2/3 + ISO 354
measurement; the building half (ISO 16283 + ISO 717, ISO 10140,
EN 12354-1/2, ISO 12999-1) moves to a new docs/building-acoustics.md
built from the site's existing split, resolving the repo/site
asymmetry. The flanking figure is reunited with its <details> code and
flanking_path is introduced before it is referenced (audit fixes), on
all three surfaces.
- psychoacoustics extracts STI/STIPA to speech-transmission.md
(paralleling speech-intelligibility.md) with STI-vs-SII disambiguation
boxes on the three pages involved; ISO 226 moves into psychoacoustics
where its perception content belongs, fixing its stale 'upcoming
feature' sentence.
- levels extracts occupational-exposure.md (ISO 9612) and
tone-prominence.md (ECMA-418-1), keeping Leq/LN/peak/SEL/Lden and the
spectrogram.
Indexes, sidebar, theory/guide cross-links (~40 edits in 19 files) and
the llms.txt generator PAGES list follow the moves; the four extracted
topics now survive into llms-full.txt instead of dropping out.
* docs: Spanish grammar fixes in the split guides
- 'función de conveniencia' instead of the false friend 'comodidad'
for convenience function (Gemini)
- plural agreement 'estrategias basadas' at the two flagged sites
(Gemini); the remaining 'basada' occurrences are correct singular
agreements ('medición basada en tareas', 'la estrategia basada...')
* docs: address second-round review on the splits
- flanking_path() gains its own parameter table (with the kij_min
Formula E.4 floor contract) in the building guide, all three surfaces
(CodeRabbit)
- the flanking figure snippet explicitly annotates its reuse of paths/
res from the snippet above; the speech-transmission figure snippet is
now fully self-contained (imports + fs) on all surfaces (CodeRabbit)
- ISO 10140-3 joins the laboratory standards footer (CodeRabbit)
- the Speech Intelligibility guide joins llms.txt (regenerated) and the
root README table (CodeRabbit)
* docs: complete API reference and theory coverage
Audit batch 5 — the reference layer catches up with everything merged
since PR ~#95:
API reference (repo + site EN/ES): from 152 to 351 documented names,
verified complete against phonometry.__all__ (the six callable aliases
and four PEP 562 renamed names live in a deprecated-aliases note
instead of rows). All quoted example outputs are computed, not written
by hand. Also deduplicates two warning rows in the site mirror, updates
the .plot() availability note (38 result classes) and escapes the
math | characters that broke GFM table rendering.
Theory (repo + site EN/ES): sections for the sixteen standards the
document did not cover - NT ACOU 112, ANSI S3.5 SII, ISO 389-7/7029,
ISO 1999, ANSI S12.2, ISO 17497-1/2, ISO 13472-1/2, ISO 11654,
ISO 9053-1/2, ISO 10534-1/2 + ASTM E2611, ISO 8041-1 + ISO 2631-1/2 +
ISO 5349 + Directive 2002/44/EC, ISO 2631-5, EN 12354-6, ISO 3745,
ISO 9614-3, DIN 45692 and the GUM + Supplement 1 - each with its
formulas, normative constants taken from the code, design decisions and
validation anchors, cross-linked to its guide. EN site mirror verified
byte-identical to the repo file modulo frontmatter; ES translated with
the site's conventions.
* docs: Spanish decimal fixes in the new reference content
- the described |H| output list in the ES API reference uses comma
decimals like the surrounding prose (Gemini)
- the two LaTeX exponents in the new ES theory sections use the {,}
convention (Gemini)
The def.: literals keep points deliberately - they are Python default
literals and the file's ~150 existing rows already follow that style;
the legacy point-decimals in pre-existing ES theory LaTeX belong to the
systematic {,} migration scheduled in the bilingual batch.
* refactor: final identifier sweep against the naming convention
Audit batch 4c — the remaining non-conforming identifiers, closing the
library-wide rename batch:
- public constants drop the unit suffix (units belong in docstrings):
OCTAVE_BANDS, THIRD_OCTAVE_BANDS (absorption_rating) and
BASE_PLATE_BANDS (scattering_diffusion), with PEP 562 __getattr__
shims in the home modules and at the package root; plain import stays
warning-free (verified with -W error)
- sii.BAND_CENTRES -> BAND_CENTERS (American spelling, module shim);
the private road_absorption centres constant renames directly
- ExposureWarning -> OccupationalExposureWarning; the module
__getattr__ returns the same class object so isinstance/except and
warning filters via the old name keep matching
- multiple_shock_vibration reuses hearing.SEXES instead of a private
duplicate
- tests/reference_data.py families gain their standard designations
(ANSIS3_5_*, ANSIS12_2_*, NTACOU112_*, ISO7029_*, ISO389_7_*)
- misleading test files renamed: test_parametrized_signals.py (it never
tested parametric_filters) and test_loudness_contours.py (1:1 with
its module); scripts/gen_llms.py -> generate_llms.py (workflow and
Makefile updated)
- conformance internals catch up (_chk_impulse_*) and an internal use
of the deprecated expanded_uncertainty alias is migrated
Flagged and deliberately left: REFERENCE_DURATION_S (the mixed-units
carve-out applies - seconds and hours coexist in the exposure API) and
the private British 'unfavourable' constants (ISO 717/11654 normative
wording; private tables are named after the standard).
Six new alias tests (root and module access, class identity, the
AttributeError path).
* test: anchor the renamed band tables value-by-value
The tests around the renamed constants asserted lengths or spot values;
they now pin every element (nominal one-third-octave and base-plate
bands, the SII band centers, and the per-impulse Formula 1 values),
verified against the code, so an accidental table edit cannot pass
(Gemini).
* refactor: deprecation-cycle renames of published API
Audit batch 4b — the published names that violate the naming convention
gain canonical replacements, with the old names working for one cycle
(NEP 23 DeprecationWarning: deprecated since 3.1, removal in 4.0):
- module loudness -> loudness_zwicker, with a PEP 562 __getattr__ shim
(plain 'import phonometry' emits no warning)
- renamed keywords via the sklearn sentinel, positional compatibility
preserved: road_absorption sample_rate -> fs; outdoor_propagation
humidity -> relative_humidity; sound_power room_volume -> volume
- legacy PyOctaveBand names normalized: octave_filter,
nominal_frequencies, normalized_frequencies and sensitivity are the
canonical implementations; octavefilter, getansifrequencies,
normalizedfreq and calculate_sensitivity delegate with a warning via
the shared _warn_renamed helper
- public string enums annotated with Literal (sex, field, presentation,
method) - annotation only, runtime validation untouched
Tests, scripts, docs and site sweep to the canonical names (~310
occurrences in 46 files); tests/test_deprecated_aliases.py pins every
alias with pytest.warns plus delegation equality and the both-given /
missing-required error paths.
* fix: address review feedback on the deprecation batch
- an explicit room_volume=None (the old default) no longer trips the
deprecation warning; only a real value through the alias warns, with
a regression test (Copilot)
- the calibration snippets stop shadowing the imported sensitivity()
with their result variable, in the repo doc and both site languages
(Copilot)
FutureWarning declined again with the rationale posted on the PR:
DeprecationWarning is the ecosystem norm for renames (NEP 23, scipy),
now codified in CONTRIBUTING.
* fix: second-pass review feedback on the deprecation batch
- loudness_zwicker validates calibration_factor through the shared
require_positive, closing the NaN/inf-permeable check (CodeRabbit)
- test names catch up with the octave_filter and sensitivity renames
(CodeRabbit)
- markdownlint MD022 blank line before the ES calibration heading
(CodeRabbit)
* refactor: concept-based names for the four unpublished modules
Audit batch 4a — the four modules named after standard numbers were all
merged after v3.0.0 and never published, so they rename cleanly (no
deprecation shims), aligned with their documentation pages:
- iso1999 -> noise_induced_hearing_loss
- iso2631_5 -> multiple_shock_vibration
- ntacou112 -> impulse_prominence
- en12354_6 -> enclosed_space_absorption
- class ImpulseProminence -> ImpulseProminenceResult
Everything that referenced the old module paths moves with them: package
imports/__all__, the plotting layer (including plot_en12354_6 ->
plot_enclosed_space_absorption), the test files (1:1 names, history
preserved), the conformance checks and the figure/diagram generator
functions (generate_nihl -> generate_noise_induced_hearing_loss for
consistency). Committed image basenames stay unchanged (published docs
link them).
CONTRIBUTING.md gains the validated Naming Conventions section (per-group
table + deprecation mechanics: NEP 23-format DeprecationWarning, PEP 562
module __getattr__ shims, the sklearn deprecated-kwarg sentinel, removal
only in majors) so the drift that produced the standard-number names
cannot recur.
* docs: align the deprecation policy with the implemented shims
- CONTRIBUTING: the stacklevel rule now says what it is for (point at
the caller's line: 2 direct, 3 via a shared helper) instead of a
literal that contradicted the existing helper-based shims (CodeRabbit)
- the insulation_* deprecation messages carry the NEP 23 version
metadata (deprecated since 3.1, removal in 4.0)
Gemini's FutureWarning suggestion is declined again: the ecosystem
research (NEP 23, scipy) settles DeprecationWarning for renames;
FutureWarning is reserved for behavior changes.
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
* fix: reject multichannel signals and resolve the uncertainty shadowing
Multichannel safety (audit finding): time-series inputs that were
silently flattened with ravel() — concatenating the channels into one
wrong series — now raise a clear ValueError telling the caller to
process channels separately:
- loudness_ecma / tonality_ecma / roughness_ecma (ECMA-418-2)
- loudness_moore_glasberg (stationary); the time-varying sibling keeps
its explicit mono/two-channel support and now rejects ndim >= 3
instead of flattening
- iso2631_5.response_peaks (a flattened 2-D response created a false
zero crossing at the channel seam)
ravel() over per-item value collections (response peaks, dose
conditions, NT ACOU impulses) is intentional normalization of scalar
lists and stays.
Uncertainty shadowing (audit finding): coverage_factor and
expanded_uncertainty were defined by both the GUM module and the
ISO 12999-1 module, and the package root silently exported the
ISO 12999-1 pair. The ISO 12999-1 functions are now canonically
insulation_coverage_factor / insulation_expanded_uncertainty (Table 8);
the bare names remain as deprecated aliases that point to both the new
name and the GUM counterpart, and the API reference and figure
generator use the new names.
Adds 7 tests (stereo/3-D rejection per module, 2-D response_peaks,
deprecation delegation).
* refactor: deduplicate the new validation and tests (quality gate)
- introduce the shared private _validation.require_1d_signal helper
(seeding the audit's shared-validation batch) and use it in the ECMA
trio, the stationary Moore-Glasberg model and response_peaks
- the two deprecation wrappers share one _warn_renamed emitter
- consolidate the per-module stereo-rejection tests into a single
parametrized test_multichannel_rejection.py
* fix: first pass of audit findings (docs, figures, plotting)
Documentation:
- fix the Spanish filter-figure regression: _showfilter gains a close
flag and the generator saves through themed_path after drawing, so the
translation pass runs on the finished figure; restore the two
_ES_EXACT keys removed in #82 (they are emitted by filter_design, not
by the script)
- repair the broken building-acoustics.md link in the enclosed-space
guide (the repo split lives in room-acoustics.md)
- make the multiple-shock and intensity snippets self-contained: define
az/fs with a synthetic shock train (verified outputs 20.94 m/s2,
R=0.46, probability 0.03) and import numpy with stated outputs
(F2=3.41, Ld=8.0), in the repo doc and both site languages
- decimal points in the English Formula texts (1.07, 55.3) that had
kept the Spanish comma
Figures:
- stop drawing data series in the grid colour (invisible on light
backgrounds): time-weighting input burst, hearing-threshold 20 yr and
NIPTS 10 yr curves, and the unweighted/partial bars now use a visible
neutral grey; regenerate the 15 affected figure sets
Plotting layer:
- plot_facade_insulation forwards user kwargs to the primary D2m,nT
curve only, so label=/color= no longer raise or merge the four curves;
the kwargs-forwarding test now covers the facade plot
- harden three edge cases: all-masked SII (zero contribution no longer
divides 0/0), empty per-impulse prominence, and nearest-band lookup
for the NC governing marker instead of float equality
- sharpness normalization sanity guards raise RuntimeError instead of
AssertionError
* refactor: address review feedback on the NC marker and facade test
- plot_noise_criterion: pick the nearest valid (non-NaN) band and place
the governing marker on that band's frequency so x and y stay paired;
skip the marker if every level is NaN (Copilot)
- add an explicit regression test for plot_facade_insulation(label=...),
which used to raise TypeError (Copilot)
* feat: EN 12354-6 sound absorption in enclosed spaces
Add the EN 12354-6:2003 prediction of a room's total equivalent sound
absorption area and reverberation time from the absorption of its
surfaces and objects (the normative Clause 4 model).
The absorption area sums the surface contributions, the object areas and
the air absorption (Formula 1), with the air term Aair = 4*m*V*(1 - psi)
(Formula 2, Table 1), the object fraction psi = sum(Vobj)/V (Formula 3)
and the empirical hard-object area Aobj = Vobj^(2/3) (Formula 4). The
reverberation time follows as T = 55.3/c0 * V*(1 - psi)/A (Formula 5,
the 0.16 factor at c0 = 345.6 m/s). The informative Annex D method for
irregular spaces is out of scope.
New module phonometry.en12354_6 with equivalent_absorption_area,
object_fraction, hard_object_absorption, air_absorption_area,
reverberation_time and the per-band enclosed_space_reverberation
returning a ReverberationResult with .plot(). Conformance report gains
an enclosed-space absorption domain (2 checks, 90 total). Adds the
enclosed-space-absorption guide (repo + EN/ES site) with a figure and a
flow diagram.
Validated digit-exact against the three worked cases of Annex E
(A = 2.26/5.03/10.21 m2, T = 2.1/0.9/0.5 s, and the air-absorption
note Aair = 0.12 m2 -> T = 2.0 s).
* refactor: address review feedback on EN 12354-6
- validation helpers _require_positive/_require_fraction: single error
literal and no negated comparison, still rejecting NaN (SonarCloud
S1192/S1940)
- object_fraction: reject negative object volumes and a fraction >= 1
(Gemini)
- air_absorption_area: validate the object fraction range and a
non-negative attenuation coefficient (Gemini)
- equivalent_absorption_area: reject negative air_area, absorption
coefficients and object areas (Gemini)
- reverberation_time: validate the object fraction range (Gemini)
- enclosed_space_reverberation: clear error when a built-in
air_condition is combined with non-standard frequencies (Gemini)
- diagram + alt text: the speed-of-sound symbol is c0, not co
(CodeRabbit); the diagram now renders it as a proper subscript
- tests: cover the speed_of_sound guard (CodeRabbit), NaN volume, the
physical-range validations and the standard-bands guard
* docs: label the object-array sum in the Formula 1 prose
The third summand of Formula 1 is the standard's object arrays k
(groups of identical objects treated as an absorbing surface), not a
duplicated surface term; name all three indices in the surrounding
prose so the equation reads unambiguously (CodeRabbit).
* feat: ISO 2631-5 multiple-shock whole-body vibration
Add the ISO 2631-5:2018 spinal-response model for whole-body vibration
containing multiple shocks: the normative Clause 5 dose and the Annex C
injury assessment (the vertical z-axis).
A seat-to-spine transfer function (clause 5.2, Formula 1) maps the
measured seat acceleration to the spinal response Az(t); the acceleration
dose Dz = 1.07*(sum Az,i^6)^(1/6) combines the positive response peaks
(Formula 3), scaled to a daily dose (Formula 4/5). Annex C turns the
daily dose into the compressive stress Sd = mz*Dzd (C.1), the
age-cumulated stress variable R (C.3/C.4) and the Weibull probability of
lumbar injury (C.5).
New module phonometry.iso2631_5 with seat_to_spine_transfer,
spinal_response, acceleration_dose, daily_dose(_multi), compression_dose,
injury_risk, injury_probability and the multiple_shock_assessment
convenience returning a MultipleShockResult with .plot(). Conformance
report gains a multiple-shock domain (3 checks, 88 total). Adds the
multiple-shock-vibration guide (repo + EN/ES site) with a figure and a
flow diagram.
Validated against the standard: the seat-to-spine transfer function
reproduces the Annex D 256 Hz digital realization to within the clause
5.2 tolerance, and the Annex C worked example (5 x 40 m/s2, 82 kg male)
gives Dzd=55.97 m/s2, R=1.22 and injury probability 0.37.
* refactor: address review feedback on ISO 2631-5
- response_peaks: vectorize the positive-peak search with NumPy instead
of a Python loop (Gemini, CodeRabbit)
- injury_probability: accept array-like R and return an array or float,
clamping negative R to zero (Gemini)
- multiple_shock_assessment: raise if only one of exposure_time /
measurement_time is given instead of silently skipping the daily-dose
scaling (Gemini, CodeRabbit)
- spinal_response: reject multi-dimensional input with a clear error
(CodeRabbit)
- export static_stress alongside the other Annex C helpers (CodeRabbit)
- figure/plot: use the vectorized injury_probability directly
- tests: assert_allclose over the transfer-function band, use MZ_MALE
and strict zip, cover array probability and the partial-argument guard
- es guide: consistent decimal style in the code comment (CodeRabbit)
* fix: Spanish decimal comma in mathtext figure legends
The decimal-comma pass skips any label containing mathtext ($...$), so
the ISO 2631-5 'Ejemplo $R$ = 1.22' and NT ACOU 112 'Determinante
$K_I$ = 13.0 dB' legends kept a dot in Spanish. Fold the decimal
conversion into the _ES_PATTERNS rules for those labels (CodeRabbit).
* feat: NT ACOU 112 impulsive-sound prominence and LAeq adjustment
Add the Nordtest NT ACOU 112:2002 method for the prominence of
impulsive sounds: the predicted prominence P = 3 lg(OR) + 2 lg(LD)
from each impulse's onset rate and level difference (clause 7,
Formula 1), the graduated adjustment KI = 1.8 (P - 5) dB for P > 5
(clause 8, Formula 2), and the rating level over a reference time
interval (clause 8, Note 1).
New module phonometry.ntacou112 with predicted_prominence,
impulse_adjustment, impulse_prominence (ImpulseProminence result with
.plot()) and rating_level. Conformance report gains an impulsive-sound
domain (2 checks). Adds the impulse-prominence guide (repo + EN/ES
site) with figure and flow diagram.
* refactor: address review feedback on NT ACOU 112
- impulse_prominence: ravel inputs so multi-dimensional arrays flatten
to a 1-D impulse sequence (Gemini)
- figure: use a distinct neutral for the threshold line instead of the
grid color, matching plot_impulse_prominence (Gemini)
- tests: use the shared NTACOU_PROMINENCE / NTACOU_ADJUSTMENT_P10
reference constants instead of hardcoded literals (CodeRabbit)
* refactor: second-pass review polish for NT ACOU 112
- figure: give the impulse markers a distinct light blue (#aec7e8),
matching plot_impulse_prominence, so they read apart from the neutral
threshold line (CodeRabbit)
- tests: split the chained magnitude/approx assertion into two clearer
assertions (CodeRabbit)
Completes the ANSI S3.5-1997 Table 3 standard speech spectra in the SII module (#97), which shipped with only the normal-effort spectrum. Adds raised, loud and shout so `standard_speech_spectrum` and `speech_intelligibility_index` accept all four vocal-effort names.
The three 18-band Table 3 arrays were cross-verified digit-exact against independent reference implementations (Google speech_intelligibility_index, R CRAN SII) and their reconstructed overall levels match the known ANSI vocal-effort overalls (normal 62.35, raised 68.3, loud 74.86, shout 82.36 dB SPL), strictly increasing. Speaking louder raises the index in a fixed noise (0.12 -> 0.79).
Conformance 83/83; full suite 1691 passed. Adds a vocal-effort figure (EN/ES, light/dark) and a "vocal effort" section in the repo and site SII guides.
Adds a `phonometry.iso1999` module estimating occupational noise-induced hearing loss, extending the ISO 7029 age-related threshold with a noise component.
* `nipts(l_ex, years, fractile)` — noise-induced permanent threshold shift over 500-6000 Hz: median N50 (clause 6.3.1, Formula 2/3, Table 1) and population fractiles from the half-Gaussian spreads du/dl (clause 6.3.2, Formulae 4-7, Tables 2-3), clamped at zero.
* `htlan(age, sex, l_ex, years, fractile)` — hearing threshold level associated with age and noise, combining the age component (HTLA = ISO 7029) with the NIPTS by H' = H + N - H*N/120 (clause 6.1, Formula 1).
Validated against ISO 1999 Annex D (Tables D.1-D.4, 85-100 dB, 10-40 years): reproduces every tabulated dB value. Conformance 82/82; full suite 1686 passed. Figure + two-lane NIHL diagram (EN/ES, light/dark), repo doc and EN/ES site guides.
Adds a `phonometry.uncertainty` module implementing the two propagation methods of the GUM:
* Law of propagation of uncertainty (ISO/IEC Guide 98-3:2008, clause 5) — `combine_uncertainty` builds the combined standard uncertainty from central-difference sensitivity coefficients, with optional input correlations (validated for shape, symmetry, unit diagonal and positive-semidefiniteness), Welch-Satterthwaite effective degrees of freedom (Annex G.4) and the expanded uncertainty U = k*uc (clause 6).
* Monte Carlo method (ISO/IEC Guide 98-3-1:2008, Supplement 1, clause 7) — `monte_carlo` propagates the input PDFs and returns the estimate, its standard uncertainty and the probabilistically symmetric coverage interval (clause 7.7).
Type B quantities from a half-width: `rectangular` (a/sqrt3), `triangular` (a/sqrt6), `u_shaped` (a/sqrt2). Results expose `.expanded()` and `.plot()` (uncertainty budget).
Validated against the Guides' worked examples: additive uc=2.0 (Suppl. 1, 9.2), coverage factor k=2.92 at p=0.99/v=16 (Table G.2), Welch-Satterthwaite v_eff=40 (Annex G.4), Monte Carlo interval [-3.88, 3.88] (Suppl. 1, 9.2.3). Conformance 79/79.
Includes the budget/Monte-Carlo figure, the two-lane GUM-vs-Monte-Carlo diagram (EN/ES, light/dark), the repo doc and the EN/ES site guides under a new "Metrology & uncertainty" section.
Add phonometry.hearing: age_threshold (ISO 7029:2017 statistical age distribution — median, spreads, population fractile) and reference_threshold (ISO 389-7:2006 free/diffuse-field audiometric zero) over 125 Hz - 8000 Hz. Coefficients parsed digit-exact from the standard. AgeThresholdResult with .plot(); 11 tests; 3 conformance checks (76/76); figure + diagram (EN/ES, light/dark); repo doc + site guides EN/ES.
Hoist the repeated "upper right" legend location into a Final constant (S1192); build the RC spectral tag with explicit concatenation (S5797); show the interpolated NC rating with :g so the figure title and prose agree (NC-42.5), regenerating the figure and alt text.
Add `phonometry.room_noise`: the NC tangency method (Table 1) and RC Mark II rating + rumble/hiss/neutral spectral tag (Annex D). Frozen result dataclasses with .plot(); RC curves reproduce Table D.1 digit-exact. 22 tests, 3 conformance checks (73/73), figure + diagram (EN/ES, light/dark), repo doc + site guides EN/ES.
impulse_response / mls_impulse_response now return an ImpulseResponseResult
with .plot() (waveform + log-magnitude envelope + Schroeder decay), staying a
drop-in for the raw IR array via __array__ so existing callers are unchanged.
Adds plot_excitation() for the sweep and MLS excitation signals, and an ISO 3382
measurement-setup diagram (source/microphone positions and ISO 3382-1 placement
rules, plus the ISO 3382-2 minimum-position grades). Room-acoustics guide §1
(repo + EN/ES site) shows the new signal/IR plots and the diagram.
Adds the ANSI S3.5-1997 (R2017) Speech Intelligibility Index over the 18
one-third-octave bands (160 Hz - 8000 Hz): speech_intelligibility_index() and
standard_speech_spectrum(), with SIIResult.plot(). The self-speech and upward
spread of masking, equivalent disturbance, level-distortion factor and
band-audibility function are weighted by the Table 3 band-importance function
(clause 6).
Constants are the standard's tabulated values; the procedure reproduces the
standard's masking intermediates (~1e-14) and gives SII = 0.996 for standard
speech in quiet with normal hearing. Includes a conformance domain (70/70),
a band-audibility figure, a computation-flow diagram, a reference page and a
bilingual site guide.
transfer_matrix_two_load / transfer_matrix_one_load take the four microphone
transfer functions (H1, H2, H3, H4) of each load (ASTM E2611), not "wave
amplitudes". Aligns docs/materials.md with the API docstring and the
documentation site.
Bring the Astro/Starlight docs site to parity with the library's implemented
functionality and reorganize the sidebar into thematic groups (docs-only).
- New bilingual guides (EN + ES): Human Vibration (ISO 8041-1 / 2631 / 5349 /
Directive 2002/44/EC), Acoustic Materials (ISO 11654 / 9053 / 10534 /
ASTM E2611), Surface Scattering (ISO 17497 / 13472).
- Split Room & Building Acoustics into 'Room Acoustics' (ISO 18233 / 3382 / 354)
and a new 'Building Acoustics & Sound Insulation' guide (ISO 16283 / 717 /
10140, EN 12354, ISO 12999-1).
- Sound Power extended to five methods (added ISO 3745 anechoic and ISO 9614-3
precision-intensity-scanning sections).
- Sidebar reorganized from a flat 12-item list into eight thematic groups,
translated in Spanish.
Site build, internal-link validation and html-validate all pass.
New `human_vibration` module implementing the ISO 8041-1:2017 master
weighting cascade (all nine weightings Wb/Wc/Wd/We/Wf/Wh/Wj/Wk/Wm),
ISO 2631-1/-2/-4 whole-body metrics (running RMS, MTVV, VDV, MSDV, crest
factor, vibration total value, energy-equivalent acceleration), the
ISO 5349-1/-2 hand-arm A(8) arithmetic and vibration-white-finger latency,
and the Directive 2002/44/EC EAV/ELV assessment. Every result object exposes
.plot(); validated to <0.05% against the ISO 8041-1 Annex B tables and the
ISO 5349-2 Annex E worked examples. Includes docs page, figures, setup
diagram, and 7 conformance checks (100%).
Three <img> tags in the room-acoustics guide carried a percentage width
attribute (width="92%"/"80%"), which is invalid HTML5 (the width attribute
must be an integer pixel count). html-validate's attribute-allowed-values
rule failed the Deploy Docs build, blocking site deployment. Switch them to
the inline style form (style="width:NN%") already used by every other figure
on the site, matching site/src/styles/theme-images.css responsive rules.
Claude-Session: https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm
Clean-room ISO 17497-1/-2 (scattering & diffusion), ISO 13472-1/-2 (in-situ road absorption) and ISO 3745 / 9614-3 (precision sound power). Plottable result objects with .plot(), single-concept documentation figures with dual-snippet <details>, 12 new conformance checks (60/60), and 6 experimental-setup diagrams. All subagent-reviewed and visually validated.
https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm
Review follow-up:
- Resolve the pnpm store path via `pnpm store path` instead of hardcoding
~/.local/share/pnpm/store (it is actually .../store/v11 and varies by pnpm
version / XDG config, so the hardcoded key would never hit).
- Bump actions/cache v4 -> v6 (latest major).
pnpm stays on @latest via corepack by request (always track the newest release).
The Deploy Docs workflow failed at pnpm/action-setup@v6: with a floating
`version: 11` the action lays down a bundled pnpm and then self-updates to the
latest 11.x, and that switch crashes ("Cannot use 'in' operator to search for
'integrity' in undefined" in lockfileToDepGraph), so the build job never
reaches the site build.
Replace pnpm/action-setup with corepack (bundled with Node), which installs the
latest pnpm cleanly, and cache the pnpm store via actions/cache. Always tracks
the latest pnpm release.
Verified locally with pnpm 11.12.0: `pnpm install --frozen-lockfile`, `pnpm
build`, `pnpm run html-validate` (0 errors) and `pnpm run pa11y` (10/10 URLs, 0
errors) all pass.
* feat: underwater seabed reflection, ambient noise and ship-traffic source levels
Second half of the closed-form underwater propagation sub-phase (P1), building
on the transmission-loss/sound-speed/sonar core:
- seabed_reflection: fluid-fluid Rayleigh reflection coefficient (Medwin & Clay),
critical grazing angle arccos(c1/c2) and bottom loss BL = -20 lg|R|.
- ocean_ambient_noise: Wenz-framework spectrum levels, energy-summing wind noise
(rule of fives) and Mellen thermal noise, with an optional caller-supplied
shipping term.
- ship_traffic_noise: predicted ship source-level spectra from vessel class,
speed and length via JOMOPANS-ECHO (MacGillivray & de Jong 2021, default,
validated to the authors' File S1 reference calculator within 0.01 dB), plus
RANDI 3.1 and Wales & Heitmeyer (2002).
Each module exposes a frozen result dataclass with .plot(), a conformance check
with an independent oracle, one figure in four language/theme variants, and full
English docs plus EN/ES site guides. Conformance 179/179; full suite 2450 passed.
* refactor: address PR review feedback on underwater modules
- seabed_reflection: guard the c1 == c2 & grazing = 0 singular limit
(0/0 -> normal-incidence coefficient) so |R| never returns NaN; add test.
- ocean_ambient_noise: allow a calm sea (wind_speed_knots = 0) to return -inf
(zero energy contribution) rather than raising; add test.
- Migrate the three modules' input validation to the shared _validation helpers
(new require_positive_array enforces finite, positive, 1-D); removes
duplicated hand-rolled checks.
- Clarify units: source spectral-density is "dB re 1 µPa²/Hz at 1 m" (not the
ambiguous "/Hz m") in the plot label, figure, api-reference and docs; scope the
plane-wave reference note so it no longer claims to cover source levels; note
the wind-noise validity range (500 Hz-5 kHz) for the wide-range Wenz example.
- Label the wind/thermal component curves in the ambient-noise figure.
Kept the conformance thermal oracle's hardcoded physical constants (Boltzmann,
1 µPa) for clean-room independence from the module under test. Conformance
179/179; full suite 2452 passed.
* fix: SonarCloud float-equality and long-title docs-deploy failures
- seabed_reflection / ocean_ambient_noise: avoid float equality checks
(SonarCloud S1244). Detect the seabed 0/0 singular limit via np.isnan of the
result instead of comparing the denominator to 0.0; gate the calm-sea branch
with require_non_negative + a relational check.
- Shorten four site-guide titles so the rendered "<title> | phonometry" stays
within the 70-char html-validate limit (long-title), which was blocking the
docs deployment: the two underwater-propagation guides (EN/ES, lengthened in
this PR) and the two pre-existing wind-turbine-noise guides (EN/ES). The full
detail stays in each guide's description.
* feat(underwater): speed of sound in sea water (UNESCO / Del Grosso / Mackenzie)
sea_water_sound_speed() with three selectable equations (UNESCO/Chen-Millero
Wong-Zhu ITS-90 default, Del Grosso 1974, Mackenzie 1981), depth_to_pressure()
(Leroy-Parthiot 1998) and a SoundSpeedProfile with .plot(). Oracles: the
canonical Mackenzie check value 1550.744 m/s and mutual agreement of the three
equations to <1 m/s in their common domain.
* feat(underwater): transmission loss (spreading + volume absorption)
spreading_loss() (spherical/cylindrical/practical), seawater_absorption() with
Francois-Garrison (default), Ainslie-McColm and Thorp models, and
transmission_loss() -> TransmissionLossResult with .plot(). Oracles: closed-form
spreading recomputation, inline recomputation of each absorption formula, and
Francois-Garrison <-> Ainslie-McColm agreement within 10%.
* feat(underwater): sonar equation (passive and active)
passive_sonar_equation() (SE = SL - TL - (NL - DI) - DT) and
active_sonar_equation() (monostatic, noise- or reverberation-limited, 2.TL)
-> SonarEquationResult with signal excess, SNR, figure of merit and .plot().
Oracle: a hand-worked Urick/Etter term balance.
* test(underwater): conformance domain for transmission-loss propagation
Six checks: Mackenzie canonical sound speed (1550.744 m/s), UNESCO<->Mackenzie
cross-model agreement, spherical spreading, Thorp absorption, Ainslie-McColm<->
Francois-Garrison agreement, and the passive-sonar figure of merit. 174/174.
* docs(underwater): figures for TL, sound-speed profile and sonar equation
Three propagation figures x4 variants (light/dark x EN/ES): transmission loss
with the spreading/absorption split, the UNESCO sound-speed profile with the
sound-channel axis, and the passive sonar equation's signal excess vs TL.
* docs(underwater): guide, API reference, indices and sidebar for propagation
New EN docs page + EN/ES site guides for transmission loss, sea-water sound
speed and the sonar equation; api-reference rows for the new public symbols;
README and docs/README index entries; astro sidebar item under Underwater
acoustics.
* fix(underwater): Del Grosso pressure conversion (bar → kg/cm² multiplies)
Subagent review caught an inverted unit conversion: bar → kg/cm² is ×1.019716
(1 bar = 1.019716 kg/cm²), not ÷. With the fix Del Grosso at 10 °C/35 ‰/1000 m
gives 1506.31 m/s, agreeing with UNESCO (1506.52) and Mackenzie (1506.26) to
<0.3 m/s (was 1505.68). Update the cross-model test value and add an independent
Del Grosso conformance check (175/175).
* docs(underwater): address PR review (breadcrumb, examples, api-reference)
- add the documentation-index breadcrumb + descriptive H1 to the propagation
guide (repo convention)
- make the guide's absorption example self-contained (index the array, print α)
- clarify SonarEquationResult.noise_level (masking is RL when reverberation-
limited) and document Thorp's <50 kHz validity
- api-reference: add the reverberation-limited active formula and the full
SonarEquationResult field list
* docs: implementation plan for wind-turbine noise (IEC 61400-11) PR-B2
* feat(wind-turbine): apparent sound power + tonal audibility chain (IEC 61400-11)
Add wind_turbine_noise.py: slant_distance/apparent_sound_power_level (Formula
26) and wind_turbine_tonality (Formulae 30-34) returning a
WindTurbineTonalityResult with .plot(). Named to avoid colliding with the
existing ISO 1996-2 critical_bandwidth/tonal_adjustment/tonal_audibility; the
K_T rating reuses the ISO 1996-2 tonal_adjustment already in the library.
* feat(wind-turbine): conformance domain (IEC 61400-11), 168/168
* feat(wind-turbine): tonal-audibility figure (x4 variants)
* docs(wind-turbine): EN guide + site EN/ES, api-reference, indices, sidebar
* fix(wind-turbine): correct low-frequency band and non-adjacent tone lines
Two correctness fixes from review, against IEC 61400-11:2012+A1:2018:
- The 20-70 Hz candidate uses the fixed absolute 20-120 Hz critical band, not
a band centred on the tone frequency (subclause 9.5.3). The previous
|f - fc| <= CBW/2 window only coincided with 20-120 Hz at fc = 70.
- Tone lines need not be contiguous with the peak. A1:2018 struck "adjacent";
every line in the critical band above the tone threshold and within 10 dB of
the peak is a tone. The Hanning /1.5 correction is now applied per contiguous
run rather than once for the whole set, so separated tones are summed
correctly.
Add tests for a low-frequency tone (excluding sub-20 Hz lines) and for two
separated tone lines.
* fix(wind-turbine): review pass — validation, shared band helper, plot color
- wind_turbine_tonality: validate strictly-increasing and uniformly-spaced
frequencies (the narrowband-spectrum precondition of Formulae 30-34)
- extract shared _critical_band_edges() so the plots use the fixed 20-120 Hz
low-frequency band instead of fc±CBW/2 (matches the classification logic)
- correct the WindTurbineTonalityResult docstring return type
- distinct masking-level line colour in the generated figure (no longer
blends with the grid)
- regenerate the 4 wind_turbine_tonality figure variants
- add regression tests for the frequency-axis validation
* fix(wind-turbine): address CodeRabbit review
- tighten the uniform-spacing tolerance to 1e-3·df so a near-miss axis (e.g.
2,2,2.4 Hz) that would bias df/ENBW is rejected; add a near-miss test
- fix a duplicate-keyword TypeError in plot_epnl / plot_wind_turbine_tonality
when a caller passes lw/label/color; merge defaults so caller values win;
add a plot-kwargs regression test
- remove the unused _AUDIBILITY_REPORT_LIMIT constant (the multi-spectrum
'No relevant tones' reporting rule is out of scope); is_audible stays ΔL_a>0
per IEC 61400-11 §9.5.8 ('a tone is audible if the tonal audibility is above
0 dB')
- add unit=dB to the apparent-power conformance row for table consistency
- document band_levels as background-corrected and the narrowband-spectrum
constraints in api-reference; make the guide code examples self-contained
- markdownlint-clean the wind-turbine plan doc
New aircraft-noise domain (PR-B1 of #19), clean-room from the standard text with the ICAO Doc 9501 ETM Vol. I worked examples as oracles:
- aircraft_noise — ICAO Annex 16 Vol. I App. 2 EPNL: perceived noisiness (Table A2-3), PNL, tone correction (slope method), and end-to-end EPNL with the 10 dB-down duration correction (EPNLResult + .plot()).
- verify_aircraft_noise_system — IEC 61265:1995 measurement-system tolerances (Table 1 directional response + scalar limits); one-third-octave filtering reuses the IEC 61260 class-2 verification.
Oracles: noy breakpoints; ETM Table 3-7 tone correction (C = 2.0 dB); ETM Table 4-4 integrated-method EPNL (92.6 EPNdB) including the largest-duration 10 dB-down rule; IEC 61265 Table 1 verbatim. Conformance 165/165. EPNL figure EN/ES x light/dark. EN + site EN/ES guides.
PR-B2 (wind-turbine noise, IEC 61400-11) follows. The full certification correction chain and airport contours are recorded as future phase #23.
New underwater-acoustics domain (PR-A of #19), clean-room from the standard texts:
- underwater_acoustics — ISO 18405:2017 reference levels re 1 µPa: SPL, SEL (re 1 µPa²·s), zero-to-peak, and 1 µPa <-> 20 µPa re-referencing.
- ship_radiated_noise — ISO 17208-1/-2: radiated noise level (re 1 µPa·m), equivalent monopole source level via the Lloyd's-mirror correction (d_s = 0.7·D), three-hydrophone geometry, tabulated uncertainty.
- pile_driving_noise — ISO 18406:2017: single-strike SEL, cumulative SEL (energy sum and N-identical), and per-strike metrics (SEL, peak SPL, Leq, 90 %-energy pulse duration) with .plot().
Conformance domain with 6 checks (161/161). Analytic oracles independent of the implementation, including an externally hand-computed Lloyd's-mirror transcription guard. Figures in EN/ES × light/dark; EN + site EN/ES guides, API reference and README.
Aeroacoustics (ICAO EPNL, IEC 61265/61400-11) is a separate follow-up PR (PR-B of #19).
* docs: design spec for electroacoustics distortion and frequency response
* feat: electroacoustic distortion and frequency response (IEC 60268-3 / AES17 / Bendat & Piersol)
Add two modules for audio-equipment characterisation from a captured signal.
distortion.py (IEC 60268-3:2013 + AES17-2015):
- thd (THD_F/THD_R), harmonic_distortion (nth-order)
- thd_plus_noise, sinad (AES17 standard notch, transient-trimmed residual)
- weighted_thd (A/C weighting of the residual)
- modulation_distortion (SMPTE), difference_frequency_distortion (CCIF),
total_difference_frequency_distortion, dynamic_intermodulation_distortion (DIM,
Table 2 nine products of the 15 kHz / 3.15 kHz signal)
- harmonic_analysis -> HarmonicDistortionResult with .plot()
frequency_response.py (Bendat & Piersol, Random Data 4e):
- transfer_function (H1 = Gxy/Gxx, H2 = Gyy/Gyx)
- coherence (ordinary gamma^2)
- FrequencyResponseResult with a Bode + coherence .plot()
Every quantity is verified against an exact analytic oracle (synthetic signals
with known harmonic/intermodulation amplitudes; a known LTI path with
gamma^2 = 1 noiseless and SNR/(1+SNR) with output noise). Adds a new conformance
domain (7 checks), tests, four-variant SVG figures, docs (EN + site EN/ES),
api-reference, indices and the astro sidebar.
* review: harden electroacoustics per AI-reviewer feedback
- distortion: reject a fundamental at/above Nyquist before the notch (clear
error instead of a SciPy crash); validate notch_q in weighted_thd for parity
with thd_plus_noise; narrow the harmonic peak-search window to ±0.1·f0 so a
nearby non-harmonic tone is not latched onto a harmonic bin.
- frequency_response: clamp noverlap to nperseg-1 (high overlap no longer
raises); use gyx != 0 instead of abs(gyx) > 0 for the H2 zero-guard.
- tests: assert the full nine-product DIM Table 2 list; add high-overlap,
above-Nyquist and weighted_thd notch_q validation cases.
- conformance: extract a shared _electro_tone helper (de-duplication).
- api-reference: document the window parameter of thd_plus_noise/harmonic_analysis.
* review: avoid float-equality zero-guard flagged by SonarCloud
Use |gyx|^2 > 0 for the H2 denominator guard instead of gyx != 0 — keeps the
cheaper (no sqrt) form gemini asked for while satisfying SonarCloud's
no-float-equality reliability rule.
* docs: design spec for psychoacoustic annoyance and fluctuation strength
* feat: psychoacoustic annoyance and fluctuation strength (Fastl & Zwicker)
Close the modern sound-quality set with two new modules.
Psychoacoustic annoyance (Fastl & Zwicker Eqs 16.2-16.4; origin Widmann 1992):
`psychoacoustic_annoyance(n5, sharpness, fluctuation_strength, roughness)`
computes PA = N5·sqrt(1 + wS² + wFR²) exactly, and
`psychoacoustic_annoyance_from_signal` derives the four sensations from a
calibrated signal (documented model-mixing caveat; F is free-field).
Fluctuation strength (Fastl & Zwicker Ch. 10; no ISO standard):
`fluctuation_strength_am_noise` is the exact closed form for AM broadband
noise (Eq. 10.2); `fluctuation_strength` is the Osses et al. (2016) signal
model, implemented clean-room and cross-checked against the paper's Table 1
literature values and the open SQAT reference (used only as a numeric oracle).
Calibrated so the 1 kHz / 60 dB / m=1 / 4 Hz AM tone reads 1.00 vacil by
construction; the AM-tone 70 dB modulation sweep tracks the literature at
Pearson r ≈ 0.98 with the 4 Hz band-pass peak. The Osses model is free-field
only (no diffuse variant), so the function takes no `field` argument.
- 27 new tests (exact PA/closed-form + signal cross-check); full suite 2266 pass
- conformance +3 (PA exact, Eq. 10.2 exact, calibration): 148/148, no drift
- EN/ES guides, api-reference, README, sidebar; two figures (4 variants each)
- `.plot()` on both result dataclasses
* fix: address PR review findings (PA/fluctuation strength)
- fluctuation_strength: guard _cross_covariance against catastrophic
cancellation — a (near-)constant band envelope could make dx·dy slightly
negative, and sqrt() then returned NaN (which slipped past the `denom <= 0`
test and poisoned the sum). Clamp the product to 0 (gemini/coderabbit).
- fluctuation_strength: precompute the Bark-inversion grid at module load
instead of rebuilding a 20k-point grid on every _bark_center_hz call (copilot).
- _plotting: let callers override color/edgecolor in plot_psychoacoustic_annoyance
via **kwargs instead of raising TypeError on a duplicate keyword (coderabbit).
- generate_graphs: the closed-form Eq. 10.2 maximum is at ∛50 ≈ 3.68 Hz, so
relabel the 4 Hz guide line as a reference (the marker already sits on the
true peak); update the ES string. Regenerate the 4 figure variants (coderabbit).
- docs: add the missing combined legend to the fluctuation-strength figure
snippet in the EN/ES guides (coderabbit).
- reference_data: sync the wFR worked-example comment with the constant (copilot).
* feat: objective audibility of tones in noise (ISO/PAS 20065:2016)
Engineering-method tonal audibility: the critical band about a tone
(Δfc, Formula 2) and its geometric corner frequencies (Formulae 3-5),
the critical-band masking level LG = LS + 10 lg(Δfc/Δf) (Formula 12),
the masking index av = -2 - lg[1 + (f/502)^2.5] (Formula 13), the
per-tone audibility ΔL = LT - LG - av (Formula 14), and the decisive
and energy-mean mean audibility over spectra (Formula 20/21).
Narrow-band front-end (Clauses 5.3.2/5.3.3/5.3.8, Annex D): from a
spectrum, mean_narrowband_level determines LS iteratively (energy
average with the -1.76 dB Hanning correction, dropping any line more
than 6 dB above the running LS until stable or fewer than five lines
remain each side), tone_level sums the tonal lines about a peak,
analyze_spectrum detects the distinct audible tones (peak detection +
distinctness criteria, Clause 5.3.4), and combined_tone_level performs
the multi-tone "FG" combination (Formula 17). The algorithm is confirmed
against the parent standard DIN 45681:2005-03 and its Annex J reference
program.
Conformance is anchored on the Annex E combustion-engine worked example
(Tables E.1/E.2/E.3): LS = 49.22 dB and LT = 67.96 dB from the spectral
lines, tone detection recovering {118.4, 137.3, 158.8} Hz, the FG
combined level LT = 72.15 dB, the per-tone audibility at 137.3 Hz, the
masking index at 137.3/592.2 Hz and the mean audibility of the five
spectra (144/144). A decisive audibility reproduced exactly needs the
complete spectrum (Table E.1 is truncated to one critical band).
- src/phonometry/tone_audibility.py: the full audibility chain, the
LS/LT spectral front-end, whole-spectrum detection + FG combination,
and ToneAudibilityResult
- _plotting.py: plot_tone_audibility (per-tone ΔL bars, decisive marked)
- generate_graphs.py: figure (4 variants) with ES translations
- conformance: 7 ISO/PAS 20065 checks (new domain)
- docs + site guides (EN/ES) + api-reference + sidebar
- tests: 50 tests, full suite 2225 passed
* feat: separate evaluation of two tones below 1000 Hz (ISO/PAS 20065 §5.3.8)
Add the two-tone resolution branch of Clause 5.3.8 (Formulae 18/19): when
exactly two tones share a critical band and both lie below 1000 Hz, the ear
can still resolve them, so they are rated separately instead of FG-combined
when their frequency difference exceeds
fD = 21·10^(1.2·|lg(fT/212)|^1.8) Hz
evaluated at the more prominent tone (the larger audibility ΔL).
New public helpers `two_tone_separation_frequency` (Formula 19) and
`resolve_tones_separately` (the Formula 18 + threshold decision). No ISO/PAS
20065 worked example exercises this branch (the Annex E band groups three
tones, so the "exactly two tones" rule never fires); the formula and decision
are implemented clean-room from the text and verified against the parent
standard DIN 45681:2005-03 Annex J reference program
(fD = 21 * 10 ^ (1.2 * Abs(Log(fT / 212) / Log(10)) ^ 1.8)). As a consistency
check, at the Annex E tones the threshold keeps them combined, matching that
example's FG grouping.
- 9 new logic/consistency tests; conformance +1 (now 145/145)
- EN/ES guides, API reference and module docstring updated
Add the EN 12354-5:2009 prediction of the receiving-room sound pressure
level from building service equipment that injects structure-borne sound
into the building. Final PR of the structural vibroacoustics series (#16),
closing the chain source (EN 15657) -> coupling (ISO 10846 / ISO 7626
mobilities) -> building transmission.
New module `installed_structure_borne`:
- `coupling_term` DC = 10 lg(|Ys+Yi+Yk|^2/(|Ys| Re{Yi})) (Formula 19b/e),
with force-source (19c) and velocity-source (19d) limits.
- `installed_structure_borne_power_level` LWs,inst = LWs,c - DC (18b).
- `structure_borne_pressure_level_path` Ln,s,ij = LWs,inst - Dsa - Rij,ref
- 10 lg(Si/S0) - 10 lg(A0/4) (18a, S0 = A0 = 10 m2) and
`total_structure_borne_pressure_level` energetic path sum (17).
- `InstalledSourceResult` (`.overall_level`, `.plot()`) and
`installed_source_prediction` for a multi-path prediction.
The source and receiver mobilities/impedances are those of
mechanical_mobility and transfer_stiffness; LWs,c comes from EN 15657.
Conformance is anchored on the coupling-term force-source limit, the
installed-power identity and the area/absorption terms of Formula 18a;
Dsa and Rij,ref are inputs (measurement / EN 12354-1 / Annexes D, F)
(137/137).
Adds the cascade figure (characteristic -> installed -> paths -> total),
the EN/ES guides, the docs index and api-reference rows, and the sidebar
entry.
Add the EN 15657:2018 characterisation of building service equipment as a
structure-borne sound source by its characteristic structure-borne sound
power level LWs, measured with the reception-plate method. Fifth PR of the
structural vibroacoustics series (#16); the LWs + plate mobility are the
source term of the EN 12354-5 installed-equipment prediction.
New module `structure_borne_power`:
- `spatial_mean_velocity_level` energetic spatial average (Formula 12).
- `plate_loss_factor` eta = 2.2/(f Ts) (Formula 13; delegates to the
ISO 10848 total loss factor).
- `structure_borne_power_level` LWs = 10 lg(2 pi f eta m S/(f0 m0 S0)) +
Lv - 60 dB (Formula 14; the -60 dB is 10 lg(v0^2/P0) for v0 = 1e-9 m/s).
- `StructureBornePowerResult` (`.total_level`, `.plot()`) and
`reception_plate_power` (loss factor given directly or via Ts).
Conformance is anchored on the resonant-plate power balance
P = omega eta (m S) <v^2> -- of which Formula 14 is the level -- and the
loss-factor identity (134/134).
Adds the module figure (low- vs high-mobility plate per band), the EN/ES
guides, the docs index and api-reference rows, and the sidebar entry.
Add the ISO/TS 7849 estimation of the airborne sound power a machine
radiates through the structure-borne vibration of its outer surface,
from the surface vibratory velocity and a radiation factor. Fourth PR of
the structural vibroacoustics series (#16); feeds ISO 9611, EN 15657 and
EN 12354-5.
New module `vibration_sound_power`:
- `velocity_level` Lv = 20 lg(v/v0) re v0 = 5e-8 m/s (Eq. 3) and
`velocity_level_from_acceleration` for a sinusoidal calibration (Eq. 8).
- `mean_velocity_level` energetic / area-weighted surface mean (Eq. 10/11).
- `radiation_factor` eps = P/(Zc <v^2> S) from a measured power (Eq. 4/8).
- `radiated_sound_power_level` LW = Lv + 10 lg(S/S0) + 10 lg(eps) +
10 lg(411/400) (Eq. 12/15): eps = 1 gives the Part 1 upper limit, a
measured eps the Part 2 engineering value.
- `extraneous_velocity_correction` K1A (Table 2).
- `VibrationSoundPowerResult` (`.total_level`, `.plot()`) and
`sound_power_from_vibration`.
Conformance is anchored on the standard's own worked calibration example
(a = 9,81 m/s^2 at 100 Hz -> Lv = 106,9 dB), the exact round-trip between
the radiation factor and LW = 10 lg(P/P0), and the fixed 10 lg(411/400)
impedance term (132/132).
Adds the module figure (Part 1 upper limit vs Part 2 engineering per
band), the EN/ES guides, the docs index and api-reference rows, and the
sidebar entry.
Add the ISO 10846 dynamic transfer stiffness k2,1 = F2,b/u1 of resilient
elements (vibration isolators, mounts, bellows, hoses) — the quantity
that characterises their vibro-acoustic transmission (Part 1, clause 5).
This is the third PR of the structural vibroacoustics series (#16) and
feeds ISO 9611, EN 15657 and EN 12354-5.
New module `transfer_stiffness`:
- `transfer_stiffness_level` Lk = 20 lg(|k2,1|/k0) re k0 = 1 N/m
(Parts 2/3, 3.17) and `loss_factor` eta = Im/Re (Part 1, 3.8).
- `transfer_stiffness_direct` k2,1 = F2,b/u1 (direct method, Part 2).
- `transfer_stiffness_indirect` k2,1 = -(2 pi f)^2 (m2 + mf) T from the
vibration transmissibility and a blocking mass (indirect method,
Part 3, Formula 1), plus `base_transmissibility` for the ideal
mass-loaded Kelvin-Voigt element used in examples and the figure.
- `TransferStiffnessResult` (`.level`, `.loss_factor`, `.magnitude`,
`.to("impedance"/"apparent_mass")`, `.plot()`) and
`indirect_transfer_stiffness_result`.
The dynamic stiffness is the reciprocal receptance of the FRF family, so
k = jw Z = -w^2 m_eff reuse the mechanical-mobility convert_frf pivot
(Part 1, Annex A / Table A.2).
Conformance is anchored on the standard's closed-form definitions: the
level of a decade of stiffness (120 dB re 1 N/m), the indirect inertia
relation, and the Table-A.2 identity k = jw Z (129/129).
Adds the module figure, the EN/ES guides, the docs index and
api-reference rows, and the sidebar entry.
Add the ISO 7626-1:2011 frequency-response-function family — the
motion-per-force FRFs (receptance, mobility, accelerance) and their
force-per-motion reciprocals (dynamic stiffness, impedance, apparent
mass) of Table 1 — plus the single-degree-of-freedom reference resonator
of Annex A that underpins the structure-borne source and transmission
standards (ISO 9611, ISO 10846, EN 15657, EN 12354-5).
New module `mechanical_mobility`:
- `convert_frf` converts between any two of the six FRFs, pivoting
through the receptance H (Y = jωH, A = −ω²H, reciprocals = 1/FRF).
- `sdof_receptance` / `sdof_mobility` / `sdof_accelerance` and
`resonance_frequency` for the viscously damped SDOF resonator
H = 1/(k − ω²m + jωc).
- `MobilityResult` (`.magnitude`, `.phase`, `.to(target)`, `.plot()`)
and `sdof_mobility_result`.
Conformance is anchored on the closed-form SDOF identities: the
driving-point mobility peak |Y(f0)| = 1/c, the static receptance
H(0) = 1/k, and the exact Table-1 reciprocity Z·Y = 1 (126/126).
Adds the module figure, the EN/ES guides, the docs index and
api-reference rows, and the sidebar section.
First module of the structure-borne / vibro-acoustic characterisation chain
that culminates in EN 12354-5. EN 29052-1 (= ISO 9052-1:1989) determines the
dynamic stiffness per unit area s' of resilient layers under floating floors
from the load-plate resonance:
- apparent_dynamic_stiffness s't = 4 pi^2 m't fr^2 (Formula 4)
- enclosed_gas_stiffness s'a = p0 / (d eps) (Formula 7)
- installed_dynamic_stiffness airflow-resistivity regimes (clause 8.2)
- natural_frequency f0 = (1/2pi) sqrt(s'/m') (Formula 2)
- floating_floor_resonance full chain -> DynamicStiffnessResult + .plot()
Conformance is anchored on the standard's own worked NOTE (s'a = 111/d MN/m3
for p0 = 0,1 MPa, eps = 0,9), reproduced exactly by the closed form, plus
hand-computed values of the resonance relations. s' feeds the ISO 16251 /
EN 12354-2 floating-floor impact improvement.
Docs (repo + bilingual site guide + API reference), the dynamic_stiffness
figure (4 variants), 3 conformance checks (123/123) and 17 tests.
* feat: reverberation-time prediction (Sabine, Eyring, Millington-Sette, Fitzroy, Arau-Puchades)
Add a general reverberation-time prediction module complementing the
EN 12354-6 enclosed-space model and the ISO 3382 measurement code. Five
statistical-acoustics models predict T from a room's volume, boundary
areas and surface absorption:
- Sabine, Eyring (Norris-Eyring) and Millington-Sette over an arbitrary
surface list;
- Fitzroy (area-weighted arithmetic mean) and Arau-Puchades (geometric
mean, Acustica 65, 1988, Formula 18) over the three wall pairs of a
rectangular room;
- the air-absorption term 4mV throughout (m from air_attenuation_m);
- reverberation_time_models + ReverberationModelResult.plot() to compare
all five per band.
k = 24 ln10 / c0 (0.161 at c0 = 343 m/s). Conformance is anchored on a
real worked oracle — Everest, Master Handbook of Acoustics 4th ed,
Fig. 7-22 Example 1, reproduced to <= 0.02 s after ft -> SI conversion —
reinforced by hand-computed closed-form values and the model identities
(all reduce to Eyring for uniform absorption; Eyring -> Sabine as a -> 0).
Docs (repo + bilingual site guide + API reference), the reverberation_models
figure (4 variants), 6 conformance checks (120/120) and 26 tests.
* build: regenerate reverberation_models figures with pinned matplotlib 3.11.0
The four new SVGs were first rendered with the local matplotlib 3.10.1,
whose glyph/path layout differs structurally from the CI-pinned 3.11.0
(requirements-figures.txt), tripping the figure-staleness check. Regenerate
them under the pinned toolchain so they match a fresh 'make graphs' on CI.
* feat: environmental-noise determination (ISO 1996-2 tonal adjustment + uncertainty)
ISO 1996-2:2017 is the determination part of the ISO 1996 environmental-noise
family (the descriptors Lden/Ldn live in ISO 1996-1, already in
phonometry.environmental). New module phonometry.environmental_measurement:
* Tonal adjustment (engineering method, ISO 1996-2:2007 Annex C):
tonal_audibility() forms ΔLta = Lpt − Lpn + 2 + lg[1 + (fc/502)^2.5] dB
(Formula C.3) and tonal_adjustment() the piecewise Kt (Formulae C.4-C.6);
assess_tonal_audibility() bundles them with the critical bandwidth (Table
C.1) into a plottable result. tonal_seeking_survey() is the one-third-octave
15/8/5 dB screen (Annex K) and tonal_adjustment_from_mean_audibility() the
Table J.1 mean-audibility route.
* Residual-noise correction: residual_sound_correction() (Formula 16, flags a
<3 dB margin as an upper bound) and gaussian_residual_level() (Annex I).
* Measurement uncertainty: combined_standard_uncertainty() (Formula 2),
environmental_expanded_uncertainty() (k=2/1.3), residual_correction_uncertainty()
(Formulae F.7-F.9) and uncertainty_from_repeated_measurements() (Formulae 17-20).
Unlike the removed 2017 engineering method (which defers to ISO/PAS 20065), the
detailed 2007/2009 Annex C algorithm is self-contained and has worked numeric
examples: conformance is anchored on Annex C.5 (ΔLta and Kt) and Annex G.2 (the
combined uncertainty u = 2.18 dB) -- three new checks (115/115, byte-stable).
The raw-FFT tone-detection pipeline is intentionally out of scope: the standard
gives no raw input spectra, so it could not be verified. 25 unit tests; docs
(GitHub + site EN/ES) get a new levels section with a figure and API rows.
* test: anchor ISO 1996-2 residual/gaussian/uncertainty tests on hand-computed values
Review found four tests re-derived their expected value from the same
arithmetic expression as the implementation, so a transcription error would
go undetected. Replace them with independent hand-computed literals (residual
correction Formula 16, Gaussian residual I.1/I.2, residual-correction
uncertainty F.9, repeated-measurement mean/uncertainty 18/20).
* fix: avoid assert for type narrowing in gaussian_residual_level (bandit B101)
The CI quality job runs bandit -r src, which flags assert statements (stripped
under python -O). Replace the mypy-narrowing assert with an elif/else-raise so
the branch is both bandit-clean and correctly narrowed.
ISO 10848:2006/2010 is the laboratory measurement counterpart of the EN 12354
flanking-transmission prediction: it measures the junction vibration reduction
index Kij that the prediction takes as an input, plus the overall flanking
descriptors Dn,f and Ln,f.
New module phonometry.flanking_transmission:
* vibration_reduction_index() -- Kij (Formula (13), or the simplified (14) for
lightweight well-damped elements), with the equivalent absorption length
(Formula (12)), the direction-averaged velocity level difference (Formula
(11), making Kij symmetric), octave-band combination and the single-number
mean Kij over 200-1250 Hz (Annex A). Returns a plottable
VibrationReductionResult.
* velocity_level_difference() / direction_averaged_level_difference() /
total_loss_factor() / equivalent_absorption_length() -- the building blocks.
* normalized_flanking_level_difference() (Dn,f, Formula (4)) and
normalized_flanking_impact_level() (Ln,f, Formula (5)), rated through the
verified ISO 717-1/-2 engines.
* vibration_reduction_index_from_flanking() -- the indirect Kij from Dn,f.
* Validity criteria: strong_coupling_satisfied() (Formula (15)),
critical_frequency() (Formula (20)), and the Part 4 modal-density /
band-mode-count / modal-overlap-factor checks (Formulas (5)/(4)/(6)).
ISO 10848 contains no worked numeric example, so conformance is anchored on
closed-form identities (simplified Kij, aj at f_ref, the total loss factor) --
three new checks in the conformance report. 29 unit tests cover the closed
forms, the Kij symmetry and simplified/full-formula relationship, the octave
conversion, the Dn,f/Ln,f ratings, the validity criteria and the plotting.
Docs (GitHub + site EN/ES) get a new section 8 with a figure and the API
reference rows; the measured Kij feeds the existing EN 12354 flanking model.
* fix: make documentation-figure rendering deterministic across machines
The "Documentation figures up to date" CI job was intermittently failing on the
heavy compute figures (excitation_signals, multiple_shock, sii_vocal_efforts,
sottek_specific_loudness, tonality_roughness_demo): their multi-threaded
numerical backends reorder floating-point reductions depending on the available
core count, so the CI runner produced byte-different SVG/PNG output than the
committed (single-threaded) baseline, and a plain re-run usually "fixed" it.
Pin the numerical thread pools to a single thread so rendering is reproducible
regardless of the host core count:
- generate_graphs.py / generate_diagrams.py set OMP/MKL/OPENBLAS/NUMEXPR/NUMBA/
VECLIB thread counts to 1 before the numeric backends import (covers direct
invocation).
- The Makefile `graphs` target also exports those plus PYTHONHASHSEED=0 before
the interpreter starts (PYTHONHASHSEED cannot be set from within the process);
CI runs `make graphs`, so it inherits this with no workflow change.
Regenerating every figure under these settings produces a zero-byte diff against
the committed images, confirming the current baseline is already the
single-threaded canonical output — so no figure files change here.
* fix: compare documentation figures within tolerance, not byte-exact
The "Documentation figures up to date" job regenerated .github/images with
`make graphs` and required a byte-identical result. That is unachievable on
GitHub's hardware-heterogeneous runner fleet: the pinned software stack
computes a few plotted path coordinates ~1 ULP apart depending on which CPU
microarchitecture (SIMD kernel) the run lands on, so the job failed
intermittently even though the figures were numerically and visually
identical. Single-thread pinning removes intra-machine reduction races but
cannot remove this cross-CPU last-bit difference.
Replace the byte diff with scripts/check_figures.py, which compares the
freshly regenerated figures against the committed versions within a tolerance:
* SVG -- non-numeric structure (elements, text, colours, ordering) must match
exactly; every numeric token must agree within an absolute/relative
tolerance. A moved element or relabelled axis fails; a last-bit
coordinate wobble passes.
* PNG -- identical dimensions, at most a small number of pixels changed beyond
a level threshold (catches localised changes a global RMS dilutes),
and a bounded RMS (catches broad changes the pixel count misses).
* other -- exact byte compare. Added/removed files always fail.
The thread-pinning determinism work from the previous commit is retained (it
still removes genuine intra-machine flakiness); this makes the check robust to
the cross-machine floating-point non-determinism it cannot eliminate.
* fix: harden figure check against hash-id drift and orphan figures
Two robustness gaps found reviewing the tolerance-aware figure check:
* SVG clip-path / marker ids are SHA256 hashes of the underlying float
geometry (matplotlib backend_svg._make_id). A cross-CPU 1-ULP wobble in that
geometry avalanches the whole hash, so the id text would change and trip the
strict structural gate -- the very drift the check exists to tolerate. Now
canonicalise every id and its url(#...)/href="#..." references to
first-appearance placeholders before comparing, so equivalent documents match
regardless of hash text while a genuine id add/remove/reorder/repoint fails.
* The generators only overwrite files, never delete, so a figure that is no
longer produced would survive on disk byte-identical to HEAD and pass the
"committed figure no longer generated" check. Clear the generated SVG/PNG at
the start of the `graphs` target before regenerating (animations *.gif/*.webm
are from the separate `animations` target and are preserved). A full
clear+regenerate reproduces the committed tree exactly, confirming no
committed figure is orphaned.
Add the laboratory method for the improvement of impact sound insulation ΔL of
soft, locally-reacting floor coverings (carpet, PVC, linoleum) measured on a
small concrete mock-up via structure-borne acceleration levels, plus the
ISO 717-2 weighted improvement ΔLw it feeds.
- weighted_impact_improvement (insulation.py): ISO 717-2:2020 Clause 5 ΔLw =
78 - Ln,r,w using the heavyweight reference floor of Table 4, reusing the
verified weighted_impact_rating engine
- floor_covering_improvement.py (ISO 16251-1):
- acceleration_level (Formula 1, a0 = 1e-6 m/s²)
- background_corrected_level (Formula 2, three-branch rule with a
limit-of-measurement flag)
- impact_improvement: Formula (2) per position -> difference (3) -> mean over
positions (4); accepts (bands,) or (positions, bands) levels
- improvement_octave_bands (Formula 5)
- FloorCoveringImprovementResult with ΔLw, the > ΔL limit mask and .plot()
- Conformance checks: ISO 717-2 reference-floor rating (78 dB, CI -11) and the
ΔL=0 -> ΔLw=0 identity (109/109). ISO 16251-1 has no worked numeric example
(Annex B is a blank form), so the oracle is the ISO 717-2 reference floor.
- generate_floor_covering_improvement figure (EN/ES × light/dark)
- Building-acoustics guide §7 and API reference rows (EN + ES)
- 20 tests covering all formulas, the per-position ordering and validation
Add the sound-absorption companion of the ISO 12999-1 insulation uncertainty:
the standard uncertainty u of the quantities produced by a reverberation-room
measurement (ISO 354) and its ratings (ISO 11654, EN 1793-1).
- sound_absorption_coefficient_uncertainty: sigma_R = m*alpha_s + n
(Table 1, 1/3-oct 63-5000 Hz), sigma_r = 0.6*sigma_R
- equivalent_area_uncertainty: sigma_R = m*A_T + n*S, S = 10 m² (Formula 2)
- practical_coefficient_uncertainty: sigma_R = m*alpha_p + n
(Table 2, octave 250-4000 Hz)
- weighted_coefficient_uncertainty (alpha_w: 0.035 / 0.020) and
single_number_rating_uncertainty (DLalpha,NRD: 0.10*DLa / 0.02*DLa)
- absorption_coverage_factor: Table 3 rounded factors (2.0 at 95 %), distinct
from ISO 12999-1's Gaussian-exact ones
- AbsorptionUncertaintyResult with exact U = k*u and a Clause 8
reporting-rounded view (.reported_expanded_uncertainty), plus .plot()
- Conformance checks reproducing the standard's worked Tables 4/5 and
Examples 1/2 exactly (107/107)
- generate_absorption_uncertainty figure (EN/ES x light/dark)
- Acoustic-materials guide section 4 and API reference rows (EN + ES)
- 24 tests covering every formula, the worked examples and validation
The naming avoids the existing scattering_diffusion.absorption_coefficient_
uncertainty (a GUM propagation from reverberation times); this module uses the
full "sound_absorption_coefficient" name for the ISO 12999-2 tabulated value.
* feat: field survey method (ISO 10052:2021)
Add the survey (simplified) method of ISO 10052:2021 for airborne,
impact, façade and service-equipment sound insulation, plus a Table 4
reverberation-index estimator for when T is not measured.
- reverberation_index (k = 10·lg(T/T0)) and estimate_reverberation_index
(Table 4 lookup by receiving-room volume and furnishing type)
- survey_airborne_insulation: D, DnT, Dn, R' with the V/7.5 effective
area floor (Clause 3.6)
- survey_impact_insulation, survey_facade_insulation
- survey_service_equipment_level: 3-position energy average (Clause 3.16)
- SurveyAirborne/Impact/Facade/ServiceEquipment result dataclasses with
ISO 717 ratings and .plot() where a single-number rating applies
- Conformance checks (R' area rule, service equipment, Table 4 estimate)
- generate_survey_insulation figure (EN/ES × light/dark)
- Building-acoustics guide §6 and API reference rows (EN + ES)
- 22 tests covering all formulas, the estimator and validation
* review: address bot findings on the ISO 10052 survey PR
- estimate_reverberation_index: normalize the room label (strip + lower)
so " Kitchen "/"KITCHEN" resolve, and name the actual volume range in
the "not tabulated" error instead of an internal index
- survey_service_equipment_level: validate measurement dimensionality so
a scalar input raises a clean ValueError, not IndexError
- fix the four survey-function docstring citations to ISO 10052:2021
- fix the Table 4 volume-range comments to 60 <= V <= 150 (inclusive)
- conformance report: got.tolist() for plain floats in the k estimate row
- docs (EN + ES): document that reverberation_index accepts a scalar k
for service equipment; localize "Table 4" -> "Tabla 4" and "m3" -> "m³"
- add tests for room normalization, the range-named error and the
scalar-measurements guard
* feat: sound insulation by intensity (ISO 15186-1:2000)
Adds the sound-intensity method as the direct-power counterpart to the
ISO 10140 laboratory pressure method — the tool of choice when flanking
transmission defeats the traditional method.
New module `intensity_insulation.py`:
- `intensity_sound_reduction` — intensity sound reduction index
RI = Lp1 - 6 - [LIn + 10 lg(Sm/S)] (Formula (7)); also the field apparent
R'I (ISO 15186-2) and the Kc-modified RI,M = RI + Kc (Formula (9)).
- `adaptation_term_kc` — Annex B term: exact Formula (B.1)
10 lg(1 + Sb2·λ/8V2) for a well-defined room, or the room-independent
approximation (B.2) 10 lg(1 + 61,4/f); both with c = 340 m/s so (B.1) with
the reference room reduces to (B.2).
- `intensity_element_normalized_difference` — DI,n,e (Formula (8)).
- `surface_pressure_intensity_indicator` — FpI qualification (Formula (10)).
- `combine_subareas` — per-subarea energy average (Formulas (11)-(12)).
Weighted ratings (RI,w, RI,M,w, DI,n,e,w) reuse the verified ISO 717-1
`weighted_rating` engine; result objects mirror `lab_insulation.py`
(frozen dataclasses, `.plot()` delegating to the rating).
Adds 19 tests, two conformance checks (RI reproduces the ISO 717-1 Rw=30
through the intensity path; Annex B B.1 reduces to B.2), the deterministic
`intensity_insulation` figure (EN/ES × light/dark), and a §5 in the
building-acoustics guide (docs + site EN/ES) plus API-reference rows.
* Address bot review — ES figure localisation, nominal centres, table escaping
- generate_graphs.py: the intensity_insulation info box is now data-only
(RI,w / RI,M,w), so the untranslatable multiline annotation is gone; add an
_ES_EXACT entry for the "RI (intensity)" legend label. Regenerated the four
deterministic SVG variants — the ES figure is now fully localised.
- docs + site EN/ES: the RI,M example uses the nominal one-third-octave centres
(100-3150 Hz) instead of np.geomspace, matching the "1/3-octave bands" claim;
printed values are unchanged. Same in the test.
- conformance_report.py: the Annex B check message uses abs(B.1 - B.2) instead
of |B.1 - B.2| so the pipes no longer break the CONFORMANCE.md table row;
regenerated the report.
- intensity_insulation.py: restructure adaptation_term_kc's branch so mypy
narrows both room parameters, dropping the type: ignore.
* feat: IEC 61260:1995 / ANSI S1.11-2004 class 0 filter verification
verify_filter_class and class_limits gain an `edition` argument. The default
"2014" path (IEC 61260-1:2014, classes 1 and 2) is unchanged; edition "1995"
selects the withdrawn IEC 61260:1995 / ANSI S1.11-2004 Table 1, which adds the
stricter laboratory-grade class 0 and whose class 1/2 masks differ slightly
from the 2014 edition.
The 1995/ANSI octave-band Table 1 was transcribed digit-for-digit and verified
identical between the two standards (dual-oracle clean-room). The fractional-
octave breakpoint mapping is shared: 1995 Annex B equation (10) is identical to
2014 Formula (9), so the existing _map_breakpoint is reused for both editions.
The library default (Butterworth order 6) already meets class 0 across the usual
configurations (octave/third-octave, 32-96 kHz); the non-Butterworth
architectures cannot reach the class mask by construction (ripple / flat group
delay), so defaults are unchanged and the class each architecture reaches is
documented instead.
Adds a class-0 conformance check (99/99), the EN/ES filter_class0_mask figure,
tests cross-checked against a shared reference_data copy, the "Class 0" doc
section (guide EN/ES + API refs EN/ES) and regenerated CONFORMANCE.md /
llms-full.txt.
* docs: address bot review — scope class-0 claims, fix English decimals
- compliance.py: English docstring tolerances use dot decimals and ± symbol
(were decimal commas, inconsistent in an English context).
- api-reference (docs + site EN/ES): qualify overall_class by edition
(1/2/None for "2014"; 0/1/2/None for "1995"); neutralize the ES headers that
still said "IEC 61260-1:2014" while documenting the edition switch.
- filter-banks (docs + site EN/ES): narrow the class-0 wording to the verified
order-6 Butterworth octave/third-octave banks at 48 kHz; drop the untested
32-96 kHz range and advise re-running verify_filter_class away from those
settings.
- generate_graphs.py: fix misleading "filter_class0_mask.png" print (output is
SVG).
- test_compliance.py: assert the class-0/1/2 minimum ordering the docstring
already promised (previously only the maximum ordering was checked).
* feat: façade insulation & outdoor radiation prediction (EN 12354-3/-4:2000)
New module phonometry.facade_prediction implementing the two building-envelope
prediction directions on a shared energy summation of element transmission
factors:
- EN 12354-3 (outdoor -> indoor): facade_sound_reduction() -> apparent R'
(Formula 10), the loudspeaker/traffic indices R45/Rtr,s (11/12) and the
standardized level difference D2m,nT (13), with ISO 717-1 single numbers.
- EN 12354-4 (indoor -> outdoor): radiated_sound_power() -> segment R' and the
radiated power level LW (Formula 2), plus the simplified Annex E outdoor
attenuation (outdoor_attenuation / outdoor_level).
FacadeElement models an area element (R), a small element / air path (Dn,e) or
an opening (insertion loss); FacadePredictionResult and RadiatedPowerResult are
frozen dataclasses exposing .plot().
Validated clean-room against the Part 3 Annex F and Part 4 Annex G worked
examples: the low octave bands, every single-number rating and the whole Annex E
propagation reproduce the published values exactly (3 new conformance checks,
98/98). The standards' own worked examples carry documented internal rounding
inconsistencies at the higher bands, tracked in the reference-data notes.
Adds the EN/ES facade_prediction figure (SVG, deterministic), the EN 12354-3/-4
section in the building-acoustics guide (EN + ES site), API-reference rows and
regenerated CONFORMANCE.md / llms-full.txt.
* fix: avoid assert for type-narrowing in FacadeElement.tau (bandit B101)
The rest of src/ avoids assert (removed under python -O); fetch the single
non-None quantity via getattr, matching the _band_count pattern, so mypy is
satisfied without an assert.
* review: harden facade_prediction per bot review
- Fix a real bug: _apparent_reduction keyed transmission factors by element
name in a dict, so two elements sharing a name silently dropped one from the
R' sum. Sum over the element list and require unique names (they key the
per-element result). (gemini)
- outdoor_level: allow NumPy broadcasting (scalar Atot vs per-side LW array).
- Validate 'frequencies' / 'octave_bands' length against the band count. (Copilot)
- Guard tau() against non-positive total_area; include the element kind in
finite-check error labels. (gemini/Copilot)
- Docstring: bands accepts 'third-octave' (not 'third'). (Copilot)
- Tests for all of the above.
* docs: address CodeRabbit review on facade prediction docs
- Fix worked-example prose: the snippet has a window + a skylight, not
'two windows' (docs + EN/ES site guides).
- Complete the façade parameter table with facade_sound_reduction(frequencies),
radiated_sound_power(octave_bands) and outdoor_level() rows (docs + EN/ES).
- Document the optional 'frequencies' parameter in the facade_sound_reduction
API-reference rows (docs + EN/ES).
- Regenerate llms-full.txt.
The Major duplicate-name finding was already fixed in af23efc.
Add `verify_weighting_class(wf)` and `weighting_class_limits(class)`, the
weighting-domain counterparts of the existing `verify_filter_class` /
`class_limits`. The verifier evaluates a WeightingFilter's designed response
at each Table 3 nominal frequency below Nyquist, subtracts the design-goal
weighting and reports the per-frequency and overall performance class (1, 2
or None) with dB margins. The class 1 and class 2 acceptance masks and the
A/C/Z design goals are transcribed from BS EN 61672-1:2013 Table 3 (standard
page 22); the class 1 columns are cross-checked in the tests against the
independent reference_data copy shared with the CI conformance report, and a
second test cross-validates the deviations against a time-domain tone RMS.
Includes a weighting_class_mask figure (A/C deviation threading the class 1
corridor with the wider class 2 limits) wired into `make graphs`, and docs in
weighting.md + the site (EN/ES) and the API reference.
Also harden the "Documentation figures up to date" CI job: pin the full
render+compute stack (matplotlib/fonttools/pillow/numpy/scipy/...) in
requirements-figures.txt instead of matplotlib alone. A newer numpy or
fonttools silently shifts the computed path coordinates / text layout of a
few figures, which flaked the byte check.
Class 0 for filters is intentionally out of scope: IEC 61260-1:2014 dropped
it, and the withdrawn IEC 61260:1995 / ANSI S1.11-2004 class-0 masks use
class 1/2 limits that differ from the 2014 edition the filter verifier is
built on, so they cannot be mixed in consistently.
Move the 68 vector documentation figures from PNG to byte-reproducible
SVG (4 EN/ES x light/dark variants each) and swap every doc/site embed
to match. Five figures stay PNG because SVG would be strictly heavier:
the two raster-backed plots (spectrogram_example, excitation_signals)
and three dense time series whose SVG runs 5.5-7.75x the PNG
(schroeder_decay, calibration_stability, impulse_response) -- all clear
the ~4.5x "SVG no longer wins" bar.
Reproducibility:
- SVG: fixed svg.hashsalt, svg.fonttype=none (text as <text>, no freetype
glyph paths), no date metadata -> byte-identical run to run and across
font builds.
- PNG: strip matplotlib's version-stamped Software chunk so the bytes are
reproducible across matplotlib builds too.
- Verified: a fresh `make graphs` reproduces all 452 images byte for byte.
Add a `figures` CI job (mirror of `conformance`) that regenerates the
figures with the pinned matplotlib and fails on any drift in
.github/images, staging first so added/removed files are caught too.
* Audit pass 11b: Tier-1 documentation animations
Add the four Tier-1 animation clips the audit prioritised — level-vs-time
phenomena the library already computes, rendered deterministically by
generate_graphs.py:
- time-weighting ballistics: the Fast/Slow/Impulse detectors chasing a tone
burst, Impulse decaying slowest (IEC 61672-1);
- onset detection: an L_AF history with the >10 dB/s onset highlighted and
OR/LD/P/KI shown live (NT ACOU 112);
- instantaneous intensity: p·u flowing for a progressive wave vs sloshing
about zero for a standing wave (IEC 61043);
- Schroeder backward integration: the decay curve emerging from the tail,
ending with the T20/T30 lines (ISO 3382).
Each is produced in the four language x theme variants as a compact WebM for
the site (embedded as an autoplaying, looping <video>) and, for English, an
animated GIF for the GitHub docs. A new `_translate_str` localises the
per-frame labels (the clips never pass through themed_path), the site gains
<video> theme-switching CSS, and `make animations` (kept out of CI, since
video is not byte-reproducible) regenerates them.
* Address review of the animations PR
- generate_animations: fail fast with a clear message when ffmpeg is absent
(Gemini).
- Schroeder _fit: guard against fewer than two points in a T20/T30 range and
a non-decaying slope, and keep xmax robust when a fit is unavailable
(Copilot).
- Site <video>s: add native `controls` (a keyboard-accessible pause/stop for
the looping motion, WCAG 2.2.2) and move the description to `title`, dropping
the `role="img"`/`aria-label` pair so the controls stay exposed to assistive
tech (CodeRabbit).
- Spanish: "energy sloshes" -> "la energía va y viene" (technical register;
ES intensity clips regenerated) (CodeRabbit).
- Group `no-autoplay` with the other `no-*` html-validate rules (CodeRabbit).
* Fix stale 'no controls' comment in theme-images.css
The Tier-1 <video>s now carry native controls; update the shared CSS comment
to match (CodeRabbit).
* Audit pass 11a: seven new documentation diagrams
Add SVG flow diagrams for the seven topics the audit flagged as having no
schematic: the exponential-detector chain of the time weightings
(IEC 61672-1), stateful vs reset block processing, the multichannel
array-shape flow, the open-plan spatial-decay line (ISO 3382-3), the
ISO 12999-1 uncertainty pipeline, the ISO 11654 absorption-rating flow and
the Zwicker loudness-model chain (ISO 532-1).
Each is generated deterministically by generate_diagrams.py in the four
EN/ES x light/dark variants, with every Spanish label added to the _ES
table (decimals localised to commas), and embedded in the repo docs and
both site trees beside the existing worked-example plots.
* Fix ISO 532-1 table citations in the Zwicker diagram
Review of the diagram against loudness_zwicker.py found two mislabelled
tables: the equal-loudness correction and lower-critical-band grouping is
Table A.3 (not A.4), and the threshold in quiet LTQ is Table A.6 (not A.3).
Reassign the a0/DDF/LTQ corrections to the core-loudness step (Tables
A.4-A.7) where they belong, update the Spanish labels, and align the ES
psychoacoustics alt text with the guide's own term "sonos" (was "sonios").
* feat: complete the .plot() convention and tidy the plotting layer
Audit batch 10:
Every result dataclass with a plottable series now exposes .plot()
(eleven new): open-plan spatial decay with the Clause 6.2 regression
and rD/rP markers; outdoor attenuation with signed pos/neg stacking (a
net ground gain stacks below zero); impedance-tube alpha(f) with the
muted |r| companion; Monte Carlo histogram with the coverage interval -
via a new opt-in monte_carlo(keep_samples=True) that stores the sample
on the result (the 8 MB/1M-trial cost stays off the default path, and
.plot() without samples raises with the hint); occupational-exposure
per-task contributions with the LEX and LEX+U lines; plus simple
renderers for static airflow, airborne/impact prediction,
airborne/impact insulation bands and band uncertainty. TransferMatrix
and ScatteringUncertainty are documented skips (value objects with no
stored frequency axis). The five priority plots are mentioned in their
guides on all three surfaces.
Plotting hygiene: _freq_axis in the enclosed-space renderer (minor-tick
suppression restored); shared _band_axis/_fractile_band/_hatch_invalid
helpers and plot_weighted_absorption folded into _plot_rating; named
color constants with the three neutral greys collapsed into one _C_MUTED
(the per-metric tonality red and roughness brown stay, commented);
every title now leads with its standard designation (17 aligned, with
per-type designations where one renderer serves several standards);
band x-labels unified to the dominant wording; the 11 result: Any
renderers typed; kwargs documented; the missing small legend fixed;
user color propagates into the companion fill_between in the five
loudness/tonality/roughness renderers.
Tests: 11 new kwargs-forwarding cases, 13 content/raise tests, and the
external-ax contract extended to all single-axes results (64/64).
* fix: address the review findings on the plotting batch
- plot_monte_carlo lets callers override density (setdefault instead of
a hardcoded kwarg that collided) (Copilot)
- plot_open_plan imports matplotlib.ticker after the lazy axes creation
so a missing matplotlib still raises the actionable hint (Copilot)
- the exposure plot computes its y-limits from the data instead of
clamping at 0 dB (negative levels no longer clip) (Copilot)
- the sound-power legend also renders when the caller passes label=
(Gemini)
- the insulation plot annotations are quoted for consistency (the file
already had future annotations, so this is style, not a crash fix)
(Gemini)
- the GUM example comment now says honestly that the committed figure
overlays the Gaussian, and the open-plan one-liner snippet gains
plt.show(), on all surfaces (CodeRabbit)
* test: the tests batch of the audit
Audit batch 9 - the test layer hardens, with src/ numerics untouched:
reference_data drift closure: the test files that duplicated oracle
values as literals now import the shared constants (11 files), with a
rewritten module docstring describing the actual regime; the EN 12354-6
Annex E surfaces move into reference_data (test + conformance import
them); all 8 previously-dead constants are wired into asserts instead
of deleted.
Conformance: 90 -> 95 checks across 22 domains, all passing and
byte-stable. New: ECMA-418-1 critical-band and proximity anchors (new
Prominent discrete tones domain), an ISO 10534-2 synthesize->recover
identity, the ISO 717-2 Annex C.1 impact rating, and an ISO 18233 sweep
deconvolution vs freqz check - each with a tolerance-rationale comment
(new convention). The STI domain is now Speech transmission
(IEC 60268-16), the ISO 9613-1 checks live under outdoor propagation,
and the registry floors are realistic.
match= backfill: 98 bare pytest.raises(ValueError) sites now pin the
actual message; one test found passing via the wrong validation path is
fixed to exercise the intended error.
Performance: the eight heavy files drop from 203 s to 148 s and the
full suite from ~360 s to 288 s (1785 tests) - module-scoped STIPA
fixture, shortened property-test signals with measured-margin comments
(exact anchors kept at the length their tolerance needs, with the
measurements documented), and the zero-margin cached<uncached
performance assert gains a 1.5x margin (it flaked on CI by 0.06 ms).
Small gaps: 13 zips gain strict=True and 3 become itertools.pairwise;
StatefulWeightingFilter gains its three missing invalid-input tests;
utils.py verified as having no raising paths.
* test: show both chained values in the ISO 10534-1 check display
A |r| mismatch used to fail the check while displaying only matching
absorption values; the expected/computed strings now carry both, like
the ECMA-418-1 check does (CodeRabbit).
* test: escape the pipes in the ISO 10534-1 display strings
Unescaped | inside GFM table cells broke the CONFORMANCE.md rendering
(CodeRabbit); regenerated byte-stable.
* docs: traceability fixes and the target taxonomy
Approved conclusions of the standards-traceability report:
Traceability (P1):
- the room-acoustics footer claimed ISO 3382-3:2022 while the code
implements and verifies :2012 - the footer now tells the truth
- CONFORMANCE.md becomes visible: a Reference page on the site (EN/ES)
explains the report and links the generated table (copying 300
auto-generated lines would drift), docs/README.md indexes it, and
why-phonometry links it after its comparison table
- nine citation patches with honest scoping: the ISO 1996-2 facade
claim reworded as measurement context; ECMA-74 out of the
tone-prominence footer (context note added); ISO 9613-1 declared in
surface-scattering with clause delimitation; materials cites the
BS EN ISO 10534:2001 editions the code cites; the EN 12354 typo;
ISO 266 and ISO 5725 added with narrow scope; IEC 61252 designations
unified
- class_limits exported (it was public-but-unimportable), documented in
the API reference and surfaced in filter-banks section 6
Taxonomy (approved target, sidebar moves only - zero URL changes):
- renamed: Signal processing & filters
- new groups: Instrumentation, calibration & compliance; Materials &
surfaces
- Perception & speech splits into Psychoacoustics (+tone-prominence),
Speech & intelligibility, and Hearing & occupational exposure
(+occupational-exposure)
- docs/README.md mirrors the new order; Conformance joins Reference
* docs: terminology polish on the new conformance pages
key differentiator (not 'differential asset'), governing band/frequency
(not 'binding'), pasa/no pasa, expresiones en forma cerrada, 'en la que
se basan las etiquetas nominales', and air-attenuation relations in
both languages (Gemini)
* docs: retrofit the house conventions onto the legacy guides
Audit batch 8 - the pre-convention guides catch up with the style the
newer ones follow, on all three surfaces (repo, site EN, site ES):
- Standards footers for the ten guides that cited clauses inline but
never closed with the normative scope (calibration, filter-banks,
intensity, levels, time-weighting, weighting, outdoor-propagation,
sound-power, psychoacoustics, plus materials synced from the site
style and a new room-acoustics footer); block-processing,
multichannel and getting-started verified as having no normative
citations to footer
- every touched snippet is now self-contained or carries an explicit
reuse annotation, and ~20 prints gained expected outputs EXECUTED
against the library (levels LN values re-verified, materials
# 222956, human-vibration VDV/MTVV/crest, getting-started ~91 dB
bands, Zwicker sone/phon values, enclosed-space RT spectrum)
- 12 figure-reproduction <details> blocks added (materials x3,
outdoor-propagation x2, time-weighting x2, weighting x3,
speech-intelligibility, getting-started - with an honest note where
the committed figure overlays extra context), all verified runnable
- structural reorders, content-preserving: the exponential detector now
precedes the F/S/I modes; the filter parameter table follows the
comparison it references; weighting opens with 'Where the curves come
from'; block-processing gains a motivating opening and drops its
duplicated constraints list; the raw LaTeX in human-vibration code
spans becomes proper math; F2/F3/F4 defined in prose in intensity
- sound-power SS1-3 gain result figures: three new deterministic
generators (12 PNG variants, ES translations, visually verified)
embedded with captions and <details> using each result's .plot()
* docs: address the review findings on the legacy-conventions batch
- NullFormatter imported from matplotlib.ticker in the SII snippet
(plt.NullFormatter does not exist) (Copilot)
- eps floor inside the log10 of the three FFT-magnitude plots in the
weighting snippets, verified warning-free with -W error (Copilot)
- the getting-started figure snippet now reproduces the committed
figure including the gray PSD overlay, note removed (CodeRabbit)
- 8-tau settling is ~99.97 %, not 99.8 % (CodeRabbit)
- the intensity result generator drops its blanket warning suppression
(this data fires none) and masks non-finite bands instead of
nan_to_num zero bars; regenerated variants are byte-identical
(Gemini, CodeRabbit)
- the outdoor attenuation stacks use separate positive/negative
baselines - the 63 Hz ground term (-4.65 dB) now hangs below zero in
BOTH the snippet and the committed generator figure, whose four
variants regenerate (CodeRabbit, Major)
- the IEC verification claim narrows to what is true: the example
verifies the 200 ms Fast row; CI covers Table 4 down to 1 ms for F
and 2 ms for S (CodeRabbit)
- freqs = res.frequencies added to the three sound-power by-hand blocks
(CodeRabbit)
* docs: bilingual EN/ES consistency batch
Audit batch 7 - the Spanish side catches up with the English one:
Content drift: the five per-architecture filter figures join the ES
filter-banks guide; the missing outdoor-propagation card joins the ES
index (7 cards on both sides); the sound-power card says five methods
in both languages (it said three since before ISO 3745/9614-3).
Decimals: the four prose dot-decimals and four code-comment
comma-decimals fixed per convention, and the systematic LaTeX {,}
migration lands - 146 conversions across 16 ES files, every hit
manually reviewed (clause numbers, standard designations, def.:
literals and code identifiers deliberately untouched; zero \d.\d
remain in ES math). The theory band-edge table keeps points (it
mirrors a code snippet's printed output); the why-phonometry report
tables convert to commas (prose).
Terminology unified: fonios, diezmado (including the divergent
multirate alt pair), umbral de audicion, banda atenuada, nivel de
evaluacion (including the rendered NT ACOU 112 ES diagram - the
generator string is fixed and only that SVG pair regenerated),
sharpness (agudeza), and clausula/apartado unified per file majority
(theory: clausula; api: apartado).
Code comments: 22 English comments translated across gum-uncertainty,
hearing-threshold, noise-induced-hearing-loss, room-noise and
occupational-exposure ES (the occupational f-string outputs were
re-executed and match the echoed comments byte-for-byte).
* docs: 'atenuación en banda atenuada' in the ES filter guide
The glossary fix produced a clumsy repetition ('atenuación de banda
atenuada'); the natural phrasing for stopband attenuation is 'en'
(Copilot).
* docs: the sound-power cards enumerate the guide's five routes exactly
The card grouped ISO 9614-2/3 into one item, making 'five routes' read
as four groups (or six standards); it now mirrors the guide's canonical
five-item enumeration, in both languages (CodeRabbit).
* docs: split the three overloaded guides
Audit batch 6 - reorganization with full content preservation (verified
line-by-line against HEAD across the three surfaces):
- room-acoustics (1096 lines) keeps ISO 18233 + ISO 3382-1/2/3 + ISO 354
measurement; the building half (ISO 16283 + ISO 717, ISO 10140,
EN 12354-1/2, ISO 12999-1) moves to a new docs/building-acoustics.md
built from the site's existing split, resolving the repo/site
asymmetry. The flanking figure is reunited with its <details> code and
flanking_path is introduced before it is referenced (audit fixes), on
all three surfaces.
- psychoacoustics extracts STI/STIPA to speech-transmission.md
(paralleling speech-intelligibility.md) with STI-vs-SII disambiguation
boxes on the three pages involved; ISO 226 moves into psychoacoustics
where its perception content belongs, fixing its stale 'upcoming
feature' sentence.
- levels extracts occupational-exposure.md (ISO 9612) and
tone-prominence.md (ECMA-418-1), keeping Leq/LN/peak/SEL/Lden and the
spectrogram.
Indexes, sidebar, theory/guide cross-links (~40 edits in 19 files) and
the llms.txt generator PAGES list follow the moves; the four extracted
topics now survive into llms-full.txt instead of dropping out.
* docs: Spanish grammar fixes in the split guides
- 'función de conveniencia' instead of the false friend 'comodidad'
for convenience function (Gemini)
- plural agreement 'estrategias basadas' at the two flagged sites
(Gemini); the remaining 'basada' occurrences are correct singular
agreements ('medición basada en tareas', 'la estrategia basada...')
* docs: address second-round review on the splits
- flanking_path() gains its own parameter table (with the kij_min
Formula E.4 floor contract) in the building guide, all three surfaces
(CodeRabbit)
- the flanking figure snippet explicitly annotates its reuse of paths/
res from the snippet above; the speech-transmission figure snippet is
now fully self-contained (imports + fs) on all surfaces (CodeRabbit)
- ISO 10140-3 joins the laboratory standards footer (CodeRabbit)
- the Speech Intelligibility guide joins llms.txt (regenerated) and the
root README table (CodeRabbit)
* docs: complete API reference and theory coverage
Audit batch 5 — the reference layer catches up with everything merged
since PR ~#95:
API reference (repo + site EN/ES): from 152 to 351 documented names,
verified complete against phonometry.__all__ (the six callable aliases
and four PEP 562 renamed names live in a deprecated-aliases note
instead of rows). All quoted example outputs are computed, not written
by hand. Also deduplicates two warning rows in the site mirror, updates
the .plot() availability note (38 result classes) and escapes the
math | characters that broke GFM table rendering.
Theory (repo + site EN/ES): sections for the sixteen standards the
document did not cover - NT ACOU 112, ANSI S3.5 SII, ISO 389-7/7029,
ISO 1999, ANSI S12.2, ISO 17497-1/2, ISO 13472-1/2, ISO 11654,
ISO 9053-1/2, ISO 10534-1/2 + ASTM E2611, ISO 8041-1 + ISO 2631-1/2 +
ISO 5349 + Directive 2002/44/EC, ISO 2631-5, EN 12354-6, ISO 3745,
ISO 9614-3, DIN 45692 and the GUM + Supplement 1 - each with its
formulas, normative constants taken from the code, design decisions and
validation anchors, cross-linked to its guide. EN site mirror verified
byte-identical to the repo file modulo frontmatter; ES translated with
the site's conventions.
* docs: Spanish decimal fixes in the new reference content
- the described |H| output list in the ES API reference uses comma
decimals like the surrounding prose (Gemini)
- the two LaTeX exponents in the new ES theory sections use the {,}
convention (Gemini)
The def.: literals keep points deliberately - they are Python default
literals and the file's ~150 existing rows already follow that style;
the legacy point-decimals in pre-existing ES theory LaTeX belong to the
systematic {,} migration scheduled in the bilingual batch.
* refactor: final identifier sweep against the naming convention
Audit batch 4c — the remaining non-conforming identifiers, closing the
library-wide rename batch:
- public constants drop the unit suffix (units belong in docstrings):
OCTAVE_BANDS, THIRD_OCTAVE_BANDS (absorption_rating) and
BASE_PLATE_BANDS (scattering_diffusion), with PEP 562 __getattr__
shims in the home modules and at the package root; plain import stays
warning-free (verified with -W error)
- sii.BAND_CENTRES -> BAND_CENTERS (American spelling, module shim);
the private road_absorption centres constant renames directly
- ExposureWarning -> OccupationalExposureWarning; the module
__getattr__ returns the same class object so isinstance/except and
warning filters via the old name keep matching
- multiple_shock_vibration reuses hearing.SEXES instead of a private
duplicate
- tests/reference_data.py families gain their standard designations
(ANSIS3_5_*, ANSIS12_2_*, NTACOU112_*, ISO7029_*, ISO389_7_*)
- misleading test files renamed: test_parametrized_signals.py (it never
tested parametric_filters) and test_loudness_contours.py (1:1 with
its module); scripts/gen_llms.py -> generate_llms.py (workflow and
Makefile updated)
- conformance internals catch up (_chk_impulse_*) and an internal use
of the deprecated expanded_uncertainty alias is migrated
Flagged and deliberately left: REFERENCE_DURATION_S (the mixed-units
carve-out applies - seconds and hours coexist in the exposure API) and
the private British 'unfavourable' constants (ISO 717/11654 normative
wording; private tables are named after the standard).
Six new alias tests (root and module access, class identity, the
AttributeError path).
* test: anchor the renamed band tables value-by-value
The tests around the renamed constants asserted lengths or spot values;
they now pin every element (nominal one-third-octave and base-plate
bands, the SII band centers, and the per-impulse Formula 1 values),
verified against the code, so an accidental table edit cannot pass
(Gemini).
* refactor: deprecation-cycle renames of published API
Audit batch 4b — the published names that violate the naming convention
gain canonical replacements, with the old names working for one cycle
(NEP 23 DeprecationWarning: deprecated since 3.1, removal in 4.0):
- module loudness -> loudness_zwicker, with a PEP 562 __getattr__ shim
(plain 'import phonometry' emits no warning)
- renamed keywords via the sklearn sentinel, positional compatibility
preserved: road_absorption sample_rate -> fs; outdoor_propagation
humidity -> relative_humidity; sound_power room_volume -> volume
- legacy PyOctaveBand names normalized: octave_filter,
nominal_frequencies, normalized_frequencies and sensitivity are the
canonical implementations; octavefilter, getansifrequencies,
normalizedfreq and calculate_sensitivity delegate with a warning via
the shared _warn_renamed helper
- public string enums annotated with Literal (sex, field, presentation,
method) - annotation only, runtime validation untouched
Tests, scripts, docs and site sweep to the canonical names (~310
occurrences in 46 files); tests/test_deprecated_aliases.py pins every
alias with pytest.warns plus delegation equality and the both-given /
missing-required error paths.
* fix: address review feedback on the deprecation batch
- an explicit room_volume=None (the old default) no longer trips the
deprecation warning; only a real value through the alias warns, with
a regression test (Copilot)
- the calibration snippets stop shadowing the imported sensitivity()
with their result variable, in the repo doc and both site languages
(Copilot)
FutureWarning declined again with the rationale posted on the PR:
DeprecationWarning is the ecosystem norm for renames (NEP 23, scipy),
now codified in CONTRIBUTING.
* fix: second-pass review feedback on the deprecation batch
- loudness_zwicker validates calibration_factor through the shared
require_positive, closing the NaN/inf-permeable check (CodeRabbit)
- test names catch up with the octave_filter and sensitivity renames
(CodeRabbit)
- markdownlint MD022 blank line before the ES calibration heading
(CodeRabbit)
* refactor: concept-based names for the four unpublished modules
Audit batch 4a — the four modules named after standard numbers were all
merged after v3.0.0 and never published, so they rename cleanly (no
deprecation shims), aligned with their documentation pages:
- iso1999 -> noise_induced_hearing_loss
- iso2631_5 -> multiple_shock_vibration
- ntacou112 -> impulse_prominence
- en12354_6 -> enclosed_space_absorption
- class ImpulseProminence -> ImpulseProminenceResult
Everything that referenced the old module paths moves with them: package
imports/__all__, the plotting layer (including plot_en12354_6 ->
plot_enclosed_space_absorption), the test files (1:1 names, history
preserved), the conformance checks and the figure/diagram generator
functions (generate_nihl -> generate_noise_induced_hearing_loss for
consistency). Committed image basenames stay unchanged (published docs
link them).
CONTRIBUTING.md gains the validated Naming Conventions section (per-group
table + deprecation mechanics: NEP 23-format DeprecationWarning, PEP 562
module __getattr__ shims, the sklearn deprecated-kwarg sentinel, removal
only in majors) so the drift that produced the standard-number names
cannot recur.
* docs: align the deprecation policy with the implemented shims
- CONTRIBUTING: the stacklevel rule now says what it is for (point at
the caller's line: 2 direct, 3 via a shared helper) instead of a
literal that contradicted the existing helper-based shims (CodeRabbit)
- the insulation_* deprecation messages carry the NEP 23 version
metadata (deprecated since 3.1, removal in 4.0)
Gemini's FutureWarning suggestion is declined again: the ecosystem
research (NEP 23, scipy) settles DeprecationWarning for renames;
FutureWarning is reserved for behavior changes.
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
* fix: reject multichannel signals and resolve the uncertainty shadowing
Multichannel safety (audit finding): time-series inputs that were
silently flattened with ravel() — concatenating the channels into one
wrong series — now raise a clear ValueError telling the caller to
process channels separately:
- loudness_ecma / tonality_ecma / roughness_ecma (ECMA-418-2)
- loudness_moore_glasberg (stationary); the time-varying sibling keeps
its explicit mono/two-channel support and now rejects ndim >= 3
instead of flattening
- iso2631_5.response_peaks (a flattened 2-D response created a false
zero crossing at the channel seam)
ravel() over per-item value collections (response peaks, dose
conditions, NT ACOU impulses) is intentional normalization of scalar
lists and stays.
Uncertainty shadowing (audit finding): coverage_factor and
expanded_uncertainty were defined by both the GUM module and the
ISO 12999-1 module, and the package root silently exported the
ISO 12999-1 pair. The ISO 12999-1 functions are now canonically
insulation_coverage_factor / insulation_expanded_uncertainty (Table 8);
the bare names remain as deprecated aliases that point to both the new
name and the GUM counterpart, and the API reference and figure
generator use the new names.
Adds 7 tests (stereo/3-D rejection per module, 2-D response_peaks,
deprecation delegation).
* refactor: deduplicate the new validation and tests (quality gate)
- introduce the shared private _validation.require_1d_signal helper
(seeding the audit's shared-validation batch) and use it in the ECMA
trio, the stationary Moore-Glasberg model and response_peaks
- the two deprecation wrappers share one _warn_renamed emitter
- consolidate the per-module stereo-rejection tests into a single
parametrized test_multichannel_rejection.py
* fix: first pass of audit findings (docs, figures, plotting)
Documentation:
- fix the Spanish filter-figure regression: _showfilter gains a close
flag and the generator saves through themed_path after drawing, so the
translation pass runs on the finished figure; restore the two
_ES_EXACT keys removed in #82 (they are emitted by filter_design, not
by the script)
- repair the broken building-acoustics.md link in the enclosed-space
guide (the repo split lives in room-acoustics.md)
- make the multiple-shock and intensity snippets self-contained: define
az/fs with a synthetic shock train (verified outputs 20.94 m/s2,
R=0.46, probability 0.03) and import numpy with stated outputs
(F2=3.41, Ld=8.0), in the repo doc and both site languages
- decimal points in the English Formula texts (1.07, 55.3) that had
kept the Spanish comma
Figures:
- stop drawing data series in the grid colour (invisible on light
backgrounds): time-weighting input burst, hearing-threshold 20 yr and
NIPTS 10 yr curves, and the unweighted/partial bars now use a visible
neutral grey; regenerate the 15 affected figure sets
Plotting layer:
- plot_facade_insulation forwards user kwargs to the primary D2m,nT
curve only, so label=/color= no longer raise or merge the four curves;
the kwargs-forwarding test now covers the facade plot
- harden three edge cases: all-masked SII (zero contribution no longer
divides 0/0), empty per-impulse prominence, and nearest-band lookup
for the NC governing marker instead of float equality
- sharpness normalization sanity guards raise RuntimeError instead of
AssertionError
* refactor: address review feedback on the NC marker and facade test
- plot_noise_criterion: pick the nearest valid (non-NaN) band and place
the governing marker on that band's frequency so x and y stay paired;
skip the marker if every level is NaN (Copilot)
- add an explicit regression test for plot_facade_insulation(label=...),
which used to raise TypeError (Copilot)
* feat: EN 12354-6 sound absorption in enclosed spaces
Add the EN 12354-6:2003 prediction of a room's total equivalent sound
absorption area and reverberation time from the absorption of its
surfaces and objects (the normative Clause 4 model).
The absorption area sums the surface contributions, the object areas and
the air absorption (Formula 1), with the air term Aair = 4*m*V*(1 - psi)
(Formula 2, Table 1), the object fraction psi = sum(Vobj)/V (Formula 3)
and the empirical hard-object area Aobj = Vobj^(2/3) (Formula 4). The
reverberation time follows as T = 55.3/c0 * V*(1 - psi)/A (Formula 5,
the 0.16 factor at c0 = 345.6 m/s). The informative Annex D method for
irregular spaces is out of scope.
New module phonometry.en12354_6 with equivalent_absorption_area,
object_fraction, hard_object_absorption, air_absorption_area,
reverberation_time and the per-band enclosed_space_reverberation
returning a ReverberationResult with .plot(). Conformance report gains
an enclosed-space absorption domain (2 checks, 90 total). Adds the
enclosed-space-absorption guide (repo + EN/ES site) with a figure and a
flow diagram.
Validated digit-exact against the three worked cases of Annex E
(A = 2.26/5.03/10.21 m2, T = 2.1/0.9/0.5 s, and the air-absorption
note Aair = 0.12 m2 -> T = 2.0 s).
* refactor: address review feedback on EN 12354-6
- validation helpers _require_positive/_require_fraction: single error
literal and no negated comparison, still rejecting NaN (SonarCloud
S1192/S1940)
- object_fraction: reject negative object volumes and a fraction >= 1
(Gemini)
- air_absorption_area: validate the object fraction range and a
non-negative attenuation coefficient (Gemini)
- equivalent_absorption_area: reject negative air_area, absorption
coefficients and object areas (Gemini)
- reverberation_time: validate the object fraction range (Gemini)
- enclosed_space_reverberation: clear error when a built-in
air_condition is combined with non-standard frequencies (Gemini)
- diagram + alt text: the speed-of-sound symbol is c0, not co
(CodeRabbit); the diagram now renders it as a proper subscript
- tests: cover the speed_of_sound guard (CodeRabbit), NaN volume, the
physical-range validations and the standard-bands guard
* docs: label the object-array sum in the Formula 1 prose
The third summand of Formula 1 is the standard's object arrays k
(groups of identical objects treated as an absorbing surface), not a
duplicated surface term; name all three indices in the surrounding
prose so the equation reads unambiguously (CodeRabbit).
* feat: ISO 2631-5 multiple-shock whole-body vibration
Add the ISO 2631-5:2018 spinal-response model for whole-body vibration
containing multiple shocks: the normative Clause 5 dose and the Annex C
injury assessment (the vertical z-axis).
A seat-to-spine transfer function (clause 5.2, Formula 1) maps the
measured seat acceleration to the spinal response Az(t); the acceleration
dose Dz = 1.07*(sum Az,i^6)^(1/6) combines the positive response peaks
(Formula 3), scaled to a daily dose (Formula 4/5). Annex C turns the
daily dose into the compressive stress Sd = mz*Dzd (C.1), the
age-cumulated stress variable R (C.3/C.4) and the Weibull probability of
lumbar injury (C.5).
New module phonometry.iso2631_5 with seat_to_spine_transfer,
spinal_response, acceleration_dose, daily_dose(_multi), compression_dose,
injury_risk, injury_probability and the multiple_shock_assessment
convenience returning a MultipleShockResult with .plot(). Conformance
report gains a multiple-shock domain (3 checks, 88 total). Adds the
multiple-shock-vibration guide (repo + EN/ES site) with a figure and a
flow diagram.
Validated against the standard: the seat-to-spine transfer function
reproduces the Annex D 256 Hz digital realization to within the clause
5.2 tolerance, and the Annex C worked example (5 x 40 m/s2, 82 kg male)
gives Dzd=55.97 m/s2, R=1.22 and injury probability 0.37.
* refactor: address review feedback on ISO 2631-5
- response_peaks: vectorize the positive-peak search with NumPy instead
of a Python loop (Gemini, CodeRabbit)
- injury_probability: accept array-like R and return an array or float,
clamping negative R to zero (Gemini)
- multiple_shock_assessment: raise if only one of exposure_time /
measurement_time is given instead of silently skipping the daily-dose
scaling (Gemini, CodeRabbit)
- spinal_response: reject multi-dimensional input with a clear error
(CodeRabbit)
- export static_stress alongside the other Annex C helpers (CodeRabbit)
- figure/plot: use the vectorized injury_probability directly
- tests: assert_allclose over the transfer-function band, use MZ_MALE
and strict zip, cover array probability and the partial-argument guard
- es guide: consistent decimal style in the code comment (CodeRabbit)
* fix: Spanish decimal comma in mathtext figure legends
The decimal-comma pass skips any label containing mathtext ($...$), so
the ISO 2631-5 'Ejemplo $R$ = 1.22' and NT ACOU 112 'Determinante
$K_I$ = 13.0 dB' legends kept a dot in Spanish. Fold the decimal
conversion into the _ES_PATTERNS rules for those labels (CodeRabbit).
* feat: NT ACOU 112 impulsive-sound prominence and LAeq adjustment
Add the Nordtest NT ACOU 112:2002 method for the prominence of
impulsive sounds: the predicted prominence P = 3 lg(OR) + 2 lg(LD)
from each impulse's onset rate and level difference (clause 7,
Formula 1), the graduated adjustment KI = 1.8 (P - 5) dB for P > 5
(clause 8, Formula 2), and the rating level over a reference time
interval (clause 8, Note 1).
New module phonometry.ntacou112 with predicted_prominence,
impulse_adjustment, impulse_prominence (ImpulseProminence result with
.plot()) and rating_level. Conformance report gains an impulsive-sound
domain (2 checks). Adds the impulse-prominence guide (repo + EN/ES
site) with figure and flow diagram.
* refactor: address review feedback on NT ACOU 112
- impulse_prominence: ravel inputs so multi-dimensional arrays flatten
to a 1-D impulse sequence (Gemini)
- figure: use a distinct neutral for the threshold line instead of the
grid color, matching plot_impulse_prominence (Gemini)
- tests: use the shared NTACOU_PROMINENCE / NTACOU_ADJUSTMENT_P10
reference constants instead of hardcoded literals (CodeRabbit)
* refactor: second-pass review polish for NT ACOU 112
- figure: give the impulse markers a distinct light blue (#aec7e8),
matching plot_impulse_prominence, so they read apart from the neutral
threshold line (CodeRabbit)
- tests: split the chained magnitude/approx assertion into two clearer
assertions (CodeRabbit)
Completes the ANSI S3.5-1997 Table 3 standard speech spectra in the SII module (#97), which shipped with only the normal-effort spectrum. Adds raised, loud and shout so `standard_speech_spectrum` and `speech_intelligibility_index` accept all four vocal-effort names.
The three 18-band Table 3 arrays were cross-verified digit-exact against independent reference implementations (Google speech_intelligibility_index, R CRAN SII) and their reconstructed overall levels match the known ANSI vocal-effort overalls (normal 62.35, raised 68.3, loud 74.86, shout 82.36 dB SPL), strictly increasing. Speaking louder raises the index in a fixed noise (0.12 -> 0.79).
Conformance 83/83; full suite 1691 passed. Adds a vocal-effort figure (EN/ES, light/dark) and a "vocal effort" section in the repo and site SII guides.
Adds a `phonometry.iso1999` module estimating occupational noise-induced hearing loss, extending the ISO 7029 age-related threshold with a noise component.
* `nipts(l_ex, years, fractile)` — noise-induced permanent threshold shift over 500-6000 Hz: median N50 (clause 6.3.1, Formula 2/3, Table 1) and population fractiles from the half-Gaussian spreads du/dl (clause 6.3.2, Formulae 4-7, Tables 2-3), clamped at zero.
* `htlan(age, sex, l_ex, years, fractile)` — hearing threshold level associated with age and noise, combining the age component (HTLA = ISO 7029) with the NIPTS by H' = H + N - H*N/120 (clause 6.1, Formula 1).
Validated against ISO 1999 Annex D (Tables D.1-D.4, 85-100 dB, 10-40 years): reproduces every tabulated dB value. Conformance 82/82; full suite 1686 passed. Figure + two-lane NIHL diagram (EN/ES, light/dark), repo doc and EN/ES site guides.
Adds a `phonometry.uncertainty` module implementing the two propagation methods of the GUM:
* Law of propagation of uncertainty (ISO/IEC Guide 98-3:2008, clause 5) — `combine_uncertainty` builds the combined standard uncertainty from central-difference sensitivity coefficients, with optional input correlations (validated for shape, symmetry, unit diagonal and positive-semidefiniteness), Welch-Satterthwaite effective degrees of freedom (Annex G.4) and the expanded uncertainty U = k*uc (clause 6).
* Monte Carlo method (ISO/IEC Guide 98-3-1:2008, Supplement 1, clause 7) — `monte_carlo` propagates the input PDFs and returns the estimate, its standard uncertainty and the probabilistically symmetric coverage interval (clause 7.7).
Type B quantities from a half-width: `rectangular` (a/sqrt3), `triangular` (a/sqrt6), `u_shaped` (a/sqrt2). Results expose `.expanded()` and `.plot()` (uncertainty budget).
Validated against the Guides' worked examples: additive uc=2.0 (Suppl. 1, 9.2), coverage factor k=2.92 at p=0.99/v=16 (Table G.2), Welch-Satterthwaite v_eff=40 (Annex G.4), Monte Carlo interval [-3.88, 3.88] (Suppl. 1, 9.2.3). Conformance 79/79.
Includes the budget/Monte-Carlo figure, the two-lane GUM-vs-Monte-Carlo diagram (EN/ES, light/dark), the repo doc and the EN/ES site guides under a new "Metrology & uncertainty" section.
Add phonometry.hearing: age_threshold (ISO 7029:2017 statistical age distribution — median, spreads, population fractile) and reference_threshold (ISO 389-7:2006 free/diffuse-field audiometric zero) over 125 Hz - 8000 Hz. Coefficients parsed digit-exact from the standard. AgeThresholdResult with .plot(); 11 tests; 3 conformance checks (76/76); figure + diagram (EN/ES, light/dark); repo doc + site guides EN/ES.
Add `phonometry.room_noise`: the NC tangency method (Table 1) and RC Mark II rating + rumble/hiss/neutral spectral tag (Annex D). Frozen result dataclasses with .plot(); RC curves reproduce Table D.1 digit-exact. 22 tests, 3 conformance checks (73/73), figure + diagram (EN/ES, light/dark), repo doc + site guides EN/ES.
impulse_response / mls_impulse_response now return an ImpulseResponseResult
with .plot() (waveform + log-magnitude envelope + Schroeder decay), staying a
drop-in for the raw IR array via __array__ so existing callers are unchanged.
Adds plot_excitation() for the sweep and MLS excitation signals, and an ISO 3382
measurement-setup diagram (source/microphone positions and ISO 3382-1 placement
rules, plus the ISO 3382-2 minimum-position grades). Room-acoustics guide §1
(repo + EN/ES site) shows the new signal/IR plots and the diagram.
Adds the ANSI S3.5-1997 (R2017) Speech Intelligibility Index over the 18
one-third-octave bands (160 Hz - 8000 Hz): speech_intelligibility_index() and
standard_speech_spectrum(), with SIIResult.plot(). The self-speech and upward
spread of masking, equivalent disturbance, level-distortion factor and
band-audibility function are weighted by the Table 3 band-importance function
(clause 6).
Constants are the standard's tabulated values; the procedure reproduces the
standard's masking intermediates (~1e-14) and gives SII = 0.996 for standard
speech in quiet with normal hearing. Includes a conformance domain (70/70),
a band-audibility figure, a computation-flow diagram, a reference page and a
bilingual site guide.
Bring the Astro/Starlight docs site to parity with the library's implemented
functionality and reorganize the sidebar into thematic groups (docs-only).
- New bilingual guides (EN + ES): Human Vibration (ISO 8041-1 / 2631 / 5349 /
Directive 2002/44/EC), Acoustic Materials (ISO 11654 / 9053 / 10534 /
ASTM E2611), Surface Scattering (ISO 17497 / 13472).
- Split Room & Building Acoustics into 'Room Acoustics' (ISO 18233 / 3382 / 354)
and a new 'Building Acoustics & Sound Insulation' guide (ISO 16283 / 717 /
10140, EN 12354, ISO 12999-1).
- Sound Power extended to five methods (added ISO 3745 anechoic and ISO 9614-3
precision-intensity-scanning sections).
- Sidebar reorganized from a flat 12-item list into eight thematic groups,
translated in Spanish.
Site build, internal-link validation and html-validate all pass.
New `human_vibration` module implementing the ISO 8041-1:2017 master
weighting cascade (all nine weightings Wb/Wc/Wd/We/Wf/Wh/Wj/Wk/Wm),
ISO 2631-1/-2/-4 whole-body metrics (running RMS, MTVV, VDV, MSDV, crest
factor, vibration total value, energy-equivalent acceleration), the
ISO 5349-1/-2 hand-arm A(8) arithmetic and vibration-white-finger latency,
and the Directive 2002/44/EC EAV/ELV assessment. Every result object exposes
.plot(); validated to <0.05% against the ISO 8041-1 Annex B tables and the
ISO 5349-2 Annex E worked examples. Includes docs page, figures, setup
diagram, and 7 conformance checks (100%).
Three <img> tags in the room-acoustics guide carried a percentage width
attribute (width="92%"/"80%"), which is invalid HTML5 (the width attribute
must be an integer pixel count). html-validate's attribute-allowed-values
rule failed the Deploy Docs build, blocking site deployment. Switch them to
the inline style form (style="width:NN%") already used by every other figure
on the site, matching site/src/styles/theme-images.css responsive rules.
Claude-Session: https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm
Clean-room ISO 17497-1/-2 (scattering & diffusion), ISO 13472-1/-2 (in-situ road absorption) and ISO 3745 / 9614-3 (precision sound power). Plottable result objects with .plot(), single-concept documentation figures with dual-snippet <details>, 12 new conformance checks (60/60), and 6 experimental-setup diagrams. All subagent-reviewed and visually validated.
https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm