Commits
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
* feat: impedance tube, airflow resistance and ISO 11654 absorption rating
Clean-room from the standards (no third-party tool referenced):
- impedance_tube.py: ISO 10534-2 two-microphone transfer-function method
(reflection factor, surface impedance/admittance, absorption), ISO 10534-1
standing-wave-ratio method, and ASTM E2611 four-microphone transfer-matrix
method (wave decomposition, two-load/one-load T-matrix, transmission loss,
material wavenumber and characteristic impedance). Kelvin (ISO) vs Celsius
(ASTM) speed-of-sound helpers keep the two sign/unit conventions apart.
- airflow_resistance.py: ISO 9053-1 static method (through-origin quadratic
fit) and ISO 9053-2 alternating method (Formula 2), with R_s in Pa·s/m and
resistivity in Pa·s/m².
- absorption_rating.py: ISO 11654 weighted absorption alpha_w (0.05-grid
reference-curve shift, L/M/H shape indicators, absorption classes A-E),
verified against the Annex A.1/A.2 worked examples.
103 module tests plus public-export checks; physics-identity anchors for the
tube (rigid wall alpha=0, perfect absorber r=0/Z=rhoc, det(T)=1, air-layer
recovery to <1e-9) since neither tube standard prints a numeric example.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* feat: ISO 9053-2 Annex A kappa', materials docs, figures & conformance
Address per-module review of the impedance-tube / materials modules:
- ISO 9053-2:2020 Annex A (normative): implement the heat-conduction-corrected
effective ratio of specific heats kappa' (Formula (A.7)) and the thermal
boundary-layer thickness b (A.4/A.5) as effective_kappa() and
thermal_boundary_layer_thickness(). The Annex A.3 worked example is a
digit-exact oracle (b = 1.83e-3 m, kappa' = 1.370). Fix the misleading
_ADIABATIC_KAPPA citation: 1.4 is the uncorrected adiabatic fallback, not the
Annex A output.
- absorption_rating: delegate .plot() to _plotting.plot_weighted_absorption,
matching the repo idiom (reuses _unfavourable_mask / _new_axes).
- docs/materials.md: new guide covering ISO 11654, ISO 9053-1/-2 and
ISO 10534-1/-2 + ASTM E2611, with runnable examples and three figures
(EN/ES, light/dark). Linked from the docs index.
- Conformance report: five materials checks (ISO 11654 Annex A.1/A.2,
ISO 9053-2 Annex A.3 b + kappa', ISO 10534-1 SWR); shared oracles in
reference_data.py. 48/48 checks pass.
Impedance-tube module review returned Approved (no code changes needed;
the Eq (20) phase citation was verified correct against ISO 10534-1 p.5).
Claude-Session: https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm
* docs: experimental-setup diagrams for the materials methods
Add hand-drawn SVG setup diagrams (EN/ES, light/dark) for the new methods,
mirroring the didactic style of the room-acoustics / outdoor diagrams, and
embed them in docs/materials.md:
- Impedance tube (ISO 10534-2): loudspeaker, tube, two flush microphones at
spacing s and distance x1 from the specimen face, rigid backing, incident/
reflected plane waves and the Eq. (17)-(19) relations.
- Four-microphone transmission-loss tube (ASTM E2611): source, two upstream
and two downstream microphones, adjustable two-load termination, the A/B/C/D
travelling waves, and s1/s2/l1/l2/d labelled to match the library's
transfer_matrix_two_load / wave_decomposition parameters (l1, l2 measured
from the specimen front reference plane).
- Airflow resistance (ISO 9053-1/-2): the static rig (specimen, laminar flow
q_v, differential manometer Δp) and the alternating rig (piston 1-4 Hz,
cavity V, specimen/airtight termination, cavity level L_p).
Also apply the whole-branch review's two before-merge items:
- airflow_resistance.py: add the module-level __all__ it was missing.
- impedance_tube.face_quantities: add functional tests (progressive/backward
wave impedance = ±rho*c) so the exported ASTM Eq. (21) helper is covered.
SVG text() now XML-escapes &, < and > so labels may contain them literally.
Diagrams validated by subagent against the standards' figures and the
library's public-API variable names; the ASTM l2 origin was corrected from
the rear to the front specimen face to match the library convention.
Claude-Session: https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm
* fix: harden ISO 11654 input coercion (bot review)
_coerce now rejects multi-dimensional array input instead of silently
flattening it (CodeRabbit) and rejects non-finite values with a clear message
instead of a cryptic downstream integer-conversion error (Gemini). Adds tests
for both.
Claude-Session: https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm
* fix: flag ill-conditioned transfer-matrix solves (bot review)
transfer_matrix_two_load / transfer_matrix_one_load now emit an
ImpedanceTubeWarning when the solve denominator is near-singular relative to
its operands — two insufficiently-different loads (two-load) or a near-resonant
geometry (one-load) — instead of silently propagating inf/nan (CodeRabbit).
Adds tests for both the singular and the well-conditioned case.
Claude-Session: https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm
* test: run the full ISO 532-1 Annex B validation from in-repo signals
Commit the ISO 532-1:2017 electronic-attachment validation data under
tests/data/iso532_1/ with a README documenting provenance, ISO copyright,
the source URL and a removal policy. This turns the twelve previously
skipped Annex B.5 technical-signal tests into always-on checks:
- Add the twelve recorded B.5 technical signals (byte-identical to the ISO
attachment) and gather the existing tone-pulse/level/expected fixtures
into the same folder.
- Read the WAVs with the stdlib `wave` module (no soundfile dependency) and
scale per Annex B.1 (full scale = 100 dB SPL -> 2*sqrt(2) Pa peak).
- Validate each signal in the sound field its ISO results workbook was
computed in; signal 15 (vehicle interior) is diffuse, the rest free. This
was the sole failing case and it is a field mismatch, not an algorithm
error. Nmax now reproduces the workbook to < 0.01 % on all twelve, so the
Nmax tolerance is tightened from 5 % to 0.1 %.
- Add two Annex B.5 rows (free + diffuse technical recording) to the
numerical conformance report; docs/CONFORMANCE.md regenerated (34/34).
Also clean the pre-existing mypy --strict debt in scripts/ (13 findings in
conformance_report.py, generate_graphs.py and generate_diagrams.py) and
extend the lint gate and CI to type-check scripts/ alongside src/ so it
cannot regress.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* test: harden ISO 532-1 WAV loading and graceful data absence (PR review)
Addresses the PR #90 bot review:
- Read WAVs with the little-endian dtype '<i2' (RIFF is little-endian, not
host-native) and keep only channel 0 of any multi-channel file, in both
the test reader and the conformance helper.
- Load the Annex B expected-values JSON defensively: the module no longer
crashes on import when the ISO data folder is absent; the six data-backed
tests carry a `requires_iso_data` skip marker instead, honouring the
README's removal policy while the synthetic-signal tests keep running.
- Raise a clear FileNotFoundError (not an IndexError) in the conformance
helper when no Annex B.5 WAV matches.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* test: unify the ISO 532-1 data root behind ISO532_1_TESTDATA (PR review)
CodeRabbit noted the presence gate and expected-values JSON were tied to
the hard-coded in-repo path while the Annex B.5 lookup honoured the
ISO532_1_TESTDATA override, so a valid override with the packaged JSON
absent would still skip. Derive the single DATA root from the override
(defaulting to the in-repo fixtures) and read the JSON, the presence gate
and the B.5 recordings all from it; drop the separate ISO_DIR.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* fix: mypy --strict on PR-C script code surfaced by the merged gate
Merging main (PR #89) brought the ISO 9613-2/9612 figure and conformance
helpers under this branch's extended `mypy src scripts` gate for the first
time: annotate the ISO 9612 Annex D task builder's return type and cast a
numpy int bar-label position to float.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* feat: ISO 9613-1 atmospheric sound absorption
Full alpha(f, T, RH, p) model per ISO 9613-1:1993 clauses 6.2-6.4:
oxygen/nitrogen relaxation frequencies, water-vapour molar
concentration from relative humidity, classical + rotational +
vibrational terms. Validated against a 13-point Table 1 grid
(worst deviation 0.39 %, rounding-limited, vs the standard's
stated +/-10 % accuracy) with the exact midband convention of
Eq. (6) Note 5. air_attenuation_m() feeds the ISO 354 m parameter
directly, closing the user-supplied gap. Two conformance checks
added (34/34).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* refactor: C1 review nits — test name and comment direction
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* test: extend ISO 9613-1 Table 1 oracle against the full standard text
The full 37-page ISO 9613-1:1993 text (now in hand) confirms every
implemented constant of Eqs. (3)-(6), the Annex B psychrometric
conversion and the clause 4.2 reference conditions exactly as coded.
Oracle extended from 13 to 23 Table 1 points, adding the 20-50 degC
sub-tables 1(i)-1(p) unavailable in the interim source. Worst
deviation unchanged at 0.385 % (rounding-limited).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 9613-2 outdoor sound propagation
Implement the ISO 9613-2:1996 general method for octave-band outdoor sound
attenuation: geometrical divergence (Eq. 7), atmospheric absorption (Eq. 8,
via ISO 9613-1 air_absorption), ground effect (general per-region method of
7.3.1 with the Table 3 a'/b'/c'/d' functions, Eq. 9, plus the alternative
method of 7.3.2, Eq. 10, and the DOmega index Eq. 11), and screening (Eq. 12-18
with the C2/C3 factors, single/double diffraction, Kmet and the 20/25 dB caps).
Adds the Cmet meteorological correction (Eq. 21/22) and the LfT(DW)=Lw+Dc-A
receiver-level composition (Eq. 3/6). Frozen dataclass result exposes the
per-term breakdown (Adiv/Aatm/Agr/Abar). New module, tests and exports.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 9612 occupational noise exposure strategies and uncertainty
Add phonometry.occupational_exposure implementing the three ISO 9612:2009
measurement strategies and the normative Annex C uncertainty budget:
- task_based_exposure (Clause 9): per-task energy average (Eq 7), task
contributions (Eq 8), daily level (Eq 9/10); uncertainty Eq C.3 with
sampling u1a (Eq C.6), duration u1b (Eq C.7) and sensitivity coefficients
c1a (Eq C.4) / c1b (Eq C.5).
- job_based_exposure (Clause 10): effective-day energy average (Eq 11),
daily level (Eq 12); Table 1 minimum-duration check; uncertainty Eq C.9.
- full_day_exposure (Clause 11): whole-day averaging (Eq 11/13) with the
Clause 11.3 three-measurement / 3 dB spread rule.
- Table C.4 (job/full-day c1*u1, bilinear interpolation), Table C.5 (u2),
u3 = 1.0 dB, coverage factor k = 1.65 (one-sided 95 %).
Frozen dataclasses (Task, TaskContribution, ExposureResult), ExposureWarning
advisories for the sampling rules, and exports appended to __init__.
Tests reproduce the Annex D (task), E (job) and F (full-day) worked examples
to the standard's precision, plus closed-form oracles (single 85 dB/8 h task
= 85; equal-level tasks; -3.01 dB per halved duration; k = 1.65) and the
Table C.4 / Table 1 lookups. make check green: 1167 passed, 12 skipped.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 9612 review corrections — Table C.4 digits and Eq C.7 denominator
Two Table C.4 cells verified against the page image at 200 dpi and
corrected (N=6/u1=3.5: 3.4 -> 3.3; N=20/u1=2.0: 0.6 -> 0.5), the test
that echoed the wrong N=6 cell now pins the printed value plus the
corrected N=20 cell. The duration_samples branch of Eq C.7 divides by
J(J-1) like Eq C.6 (standard error of the mean), with a regression test
pinning u1b = 1.0 h for samples (4, 6) h. Module docstring now states
the Lp,Cpeak uncertainty scope cut (Table C.5, NOTE 1).
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* test: ISO 9613-2 and ISO 9612 conformance checks
Add a new "Outdoor propagation & occupational exposure" conformance domain
with seven closed-form / worked-example checks backed by shared constants in
tests/reference_data.py:
- ISO 9613-2:1996 Eq. (7) geometrical divergence Adiv = 51 dB at 100 m.
- ISO 9613-2:1996 Table 3 ground limit b'(0) = 10,1 -> Agr(250 Hz) = 17,2 dB
(porous ground, hs = hr = 0, dp -> inf).
- ISO 9613-2:1996 clause 7.4 single/double diffraction 20/25 dB caps.
- ISO 9612:2009 Annex D/E/F daily exposure LEX,8h and expanded uncertainty U
(84,3/2,7, 88,1/3,8, 90,1/3,4 dB), the Task objects rebuilt from the shared
input table.
Registry grows 34 -> 41 checks across 8 domains; docs/CONFORMANCE.md
regenerated via make conformance. Registration and reference-data consistency
tests pin the constants to the published values.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* docs: figures for outdoor propagation and occupational exposure
Add four documentation figures (each EN/ES x light/dark) plus a setup diagram:
- air_absorption_alpha: ISO 9613-1 alpha(f) over frequency at four
temperature/humidity combinations (relaxation roll-off, f^2 growth).
- outdoor_attenuation_breakdown: ISO 9613-2 per-term stacked bar (Adiv, Aatm,
Agr, Abar) with the total A, for a 200 m path over porous ground with a 4 m
barrier.
- exposure_uncertainty: ISO 9612 Annex D task contributions with the daily
LEX,8h and the one-sided 95 % upper limit LEX,8h + U.
- diagram_outdoor_geometry: source-barrier-receiver geometry (dss/dsr/z, the
blocked direct ray and the diffracted ray over the top edge).
Spanish translations added to the generate_graphs / generate_diagrams
dictionaries (decimal commas, UNE terminology). Every image visually audited.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* docs: ISO 9613-1/2 outdoor propagation guide and ISO 9612 exposure (EN)
Add the didactic Outdoor Sound Propagation guide (docs/outdoor-propagation.md)
covering ISO 9613-1 atmospheric absorption and the ISO 9613-2 general method
(divergence, atmospheric, ground and barrier terms, the OutdoorAttenuation
breakdown, the alternative 7.3.2 ground method, Cmet), with executed snippets
and honest scope notes (Amisc/reflections not implemented, 7.3.2 not
auto-wired, per-band Cmet a convenience). Extend the Levels guide with the
ISO 9612 occupational-exposure section (task/job/full-day strategies, the
C.6 I(I-1) vs C.12 N-1 asymmetry, Table C.4, k = 1,65, LEX,8h + U upper limit,
Lp,Cpeak uncertainty out of scope), reproducing the Annex D/E/F worked
examples.
Add the matching theory sections, API-reference rows for every new public
name, README feature list + docs table, docs index and CHANGELOG entries.
llms-full.txt / llms.txt regenerated (make llms) with the new page wired into
gen_llms.py. GitHub-safe math throughout; every snippet executed with pasted
outputs.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* docs: outdoor propagation and exposure site twins (EN + ES)
Add the English and Spanish Starlight twins of the new content:
- guides/outdoor-propagation.md (EN + ES) — code-identical snippets to the
docs page; Spanish prose uses UNE terminology ("ponderación temporal"),
decimal commas outside math and code, and the _es figure variants.
- The ISO 9612 section added to guides/levels.md (EN + ES).
- reference/theory.md and reference/api.md rows (EN + ES).
- Landing cards and standards list bumped 34 -> 37 (index.mdx EN + ES);
guides/outdoor-propagation added to the sidebar.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* fix: final branch review — API rows, precision phrasing and ES links
Adds the five missing public-symbol rows (TaskContribution,
table_c4_contribution, minimum_cumulative_duration_hours, COVERAGE_FACTOR,
INSTRUMENT_U2) to the three API references, states the ISO 9613-2 default
atmosphere (20 °C / 70 %, a Table 2 reference atmosphere) in the API rows
and guides, and softens "digit-for-digit" to "the standard's printed
precision" wherever it covered Annex E, whose final level differs only by
the standard's own pre-rounding. ES pages now use the /es/ link prefix
consistently. Code nits from the review: dead arg > 1 guard removed
(z > 0 implies arg >= 3), _with_advisory uses dataclasses.replace, and the
float-keyed _PRIME_BY_BAND gets a safety comment. llms files regenerated.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* fix: validate propagation/exposure inputs and docs lint (PR review)
Addresses the PR #89 bot review:
- Barrier.__post_init__ rejects negative edge distances and a zero
edge_separation (which previously divided by zero in the C3 double-
diffraction factor); Task.__post_init__ rejects empty samples and a
non-positive duration.
- ground_attenuation and barrier_attenuation reject non-positive
frequencies (a zero frequency gave an infinite wavelength / division by
zero); ground_attenuation also checks a positive distance and
non-negative heights. job_based_exposure rejects a non-positive
sample_duration_hours.
- Rewrite the ISO 9613-2 7.3.2 closed form in the guides and API tables so
Markdownlint no longer parses `(...)[...]` as a reversed link (MD011).
- Regression tests for every new guard; docstrings note the new raises.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* feat: ISO 16283-3 facade sound insulation
Add facade_insulation() and FacadeInsulationResult for field facade sound
insulation per ISO 16283-3: the global-method level difference D2m and its
standardized (D2m,nT) and normalized (D2m,n) forms, plus the element-method
apparent sound reduction index R'45 (loudspeaker, -1,5 dB) / R'tr,s (road
traffic, -3 dB). Reuses the ISO 16283-1 energy-averaging helpers, the
Sabine absorption area, and the ISO 717-1 airborne weighted_rating engine
unchanged for the single-number rating. Adds a per-band .plot() profile and
exports.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 16283-3 draft-edition caveat and symmetric R' validation
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 10140 laboratory airborne and impact sound insulation
Add src/phonometry/lab_insulation.py, the laboratory counterpart of the
field ISO 16283 family:
- lab_airborne_insulation: R = L1 - L2 + 10 lg(S/A) with A = 0,16 V/T
(ISO 10140-2:2010 Formula (2); ISO 10140-4:2010 Formula (5)).
- lab_impact_insulation: Ln = Li + 10 lg(A/A0), A0 = 10 m²
(ISO 10140-3:2010 Formula (1)).
- background_correction: ISO 10140-4:2010 Clause 4.3 Formula (4) with the
6/15 dB criteria and the 1,3 dB limit-of-measurement cap.
Single-number Rw / Ln,w with C, Ctr, CI reuse the verified ISO 717-1/2
weighted_rating / weighted_impact_rating engines; position energy
averaging reuses _as_band_levels. 29 new closed-form / identity tests.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: reconcile facade citations with published ISO 16283-3:2016
The facade module was drafted from ISO/DIS 16283-3:2014; its docstrings
cited draft formula numbers with a caveat to reconcile against the
published edition. Reconciled against ISO 16283-3:2016(E) (First edition
2016-02-01):
- Clause numbers are edition-stable: the field-quantity definitions keep
3.12 (R'45deg), 3.13 (R'tr,s), 3.14 (D2m), 3.15 (D2m,nT), 3.16 (D2m,n),
3.17 (A=0,16V/T), and 9.5.1 (surface-level averaging).
- Formula numbers differ: in the 2016 edition the Clause 3 defining
formulas are UNNUMBERED (inline in the term definitions); the numbered
formulas (1)-(21) live in the procedural clauses. The surface-level
energy-average is Formula (7) in 2016 (was Formula (20) in the DIS).
- Dropped the invalid draft Formula (2)-(7) citations from the field-
quantity docstrings and the draft-edition reconciliation caveat.
- Constants and formula bodies are unchanged (-1,5 dB / -3 dB, A0=10 m2,
T0=0,5 s, A=0,16V/T, band ranges): no numeric change; 22 facade tests
green.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: EN 12354-1/2 building acoustic performance prediction with flanking
Implement the EN 12354:2000 simplified single-number prediction model for
apparent airborne (R'w) and impact (L'n,w) sound insulation, including direct
plus flanking transmission (Ff/Df/Fd paths) and the Annex E vibration-reduction
index Kij for rigid cross, rigid T, flexible-interlayer and lightweight-facade
junctions.
Airborne Formula (26)/(27)/(28a) with l0=1 m, Kij,min (29), lining composition
(30)/(31); impact Formula (21) with bare-floor Ln,w,eq (164-35 lg m'), Table 1
flanking correction K, and standardized L'nT,w (3). Results expose per-path
energy contributions so the dominant flanking path is visible.
Validated against the standards' worked examples: Part 1 Annex H.3 airborne
(R'w = 52 dB, 13 paths; +floating-floor 53 dB) and Part 2 Annex E.3 impact
(L'n,w = 45 dB, L'nT,w = 43 dB); all four Annex H junctions reproduce Kij.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 12999-1 building acoustics measurement uncertainty
Add building_uncertainty module implementing ISO 12999-1:2020 standard
uncertainties for sound-insulation quantities: per-band and single-number
values for airborne (Tables 2/3), impact (Tables 4/5) and floor-covering
reduction (Tables 6/7), across measurement situations A/B/C (Clause 5.2),
plus the Annex D sigma_R95 upper limits, Table 1 max repeatability and
Table 8 coverage factors.
Expansion U = k*u with k >= 1 (Clause 8), one-sided factors for conformity
checks (Formulae 4/5), and combination rules from Annexes A/B/C (prediction
input A.1, quadrature A.2/C.2, m-measurement reduction A.7, uncorrelated
single-number B.2). Composable UncertainValue attaches value +- U without
touching the rating dataclasses.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: numpy-stub-agnostic annotations for the two env-dependent mypy sites
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* refactor: PR-B review minors — anchors, Kij floor, uncertainty polish
- Table 3 r_w+ctr_50_5000 σsitu(B)=1.0 verified digit-by-digit against the
ISO 12999-1:2020(E) page image; anomalous but normative (comment added).
- Anchor the ISO 10140 rating tests to independent literals (Rw=54, Ln,w=58 —
reference-curve shape + the 32 dB / 16-band = 2 dB ISO 717 shift).
- background_correction: cross-ref sound_power.background_noise_correction and
document the negative-margin (Lb > Lsb) cap at Lsb−1.3.
- flanking_path: optional kij_min clamp for EN 12354-1 Clause 4.4.2 (Kij≥Kij,min),
documented on flanking_element; tested clamped vs unclamped (H.3 unaffected).
- impact_flanking_correction: comment on nearest-neighbour + tie-to-lower.
- Export COVERAGE_FACTORS (Table 8) as public API.
- single_number_uncertainty: note impact situation A is an estimate (Table 5 fn a).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: conformance checks for facade, lab insulation, EN 12354 and ISO 12999
Add six numerical conformance checks for the PR-B building-acoustics
standards, following the established registry pattern (single source of
truth in tests/reference_data.py, clause citations, honest tolerances):
- ISO 16283-3:2016 Clause 3.12: facade R'45 isolates the -1.5 dB
oblique-incidence correction on an S=A constructed case (exact).
- ISO 10140-2:2021 Formula (2): lab airborne R laid on the ISO 717-1
reference shape (S=A) -> Rw = 54 (the +2-shift analytic anchor).
- EN 12354-1:2000 Annex H.3: airborne prediction R'w = 52 from the 13
transmission paths (direct + 12 flanking), the branch's strongest oracle.
- EN 12354-2:2000 Annex E.3: impact prediction L'n,w = 45 (76-33+2).
- ISO 12999-1:2020 Table 2: airborne band uncertainty, situation A @
1 kHz = 1.8 dB (digit-exact), and Clause 8/Table 8 expanded
uncertainty U = 1.96 u = 2.352 dB (exact k=1.96 arithmetic).
Facade and lab checks join "Room & building acoustics"; the EN 12354 /
ISO 12999 checks form a new "Building prediction & uncertainty" domain.
Registry grows 26 -> 32 checks across 7 domains. Shared expected values
and the Annex H.3 input table move into reference_data.py so the report
and tests cannot drift; a consistency test pins them to the published
worked-example results. docs/CONFORMANCE.md regenerated via make conformance.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: building acoustics II — facade, laboratory, prediction and uncertainty (EN)
Extend the Room & Building Acoustics guide with facade insulation
(ISO 16283-3), laboratory characterisation (ISO 10140), flanking-transmission
performance prediction (EN 12354-1/2) and measurement uncertainty
(ISO 12999-1), with executed snippets reproducing the EN 12354-1 Annex H.3
(R'w = 52 dB) and Annex E.3 (L'n,w = 45 dB) worked examples. Add the matching
theory subsections, API-reference rows for every new public name, README and
landing-page entries (29 -> 33 standards), and CHANGELOG. EN docs and site
twins keep byte-identical code blocks.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: flanking-paths diagram, prediction and uncertainty figures
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: gemelas ES de edificación II (fachadas, laboratorio, predicción, incertidumbre)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: PR-B final-review fixes — count, GitHub-safe math, edition alignment
Landing count corrected to 34 standards (slash-part convention); the
GitHub-unsafe math spacing tokens reintroduced by the new building
sections are scrubbed from the docs/ tree and its site twins; the whole
ISO 10140-2 citation chain aligned to the verified :2010 edition; plus
the six minor documentation-precision fixes from the final review.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 88 review round 1 — isfinite idiom, heading levels, frequencies validation
- building_prediction._check_finite: replace 'v != v or v in (inf,-inf)'
NaN idiom with math.isfinite for SonarCloud (same behavior, cleaner).
- room-acoustics docs (EN docs + site, ES site): relevel the three
parameter-table headings that skipped H2->H4 to H3 (MD001).
- insulation.facade_insulation: validate 'frequencies' length against the
band count, raising a clear ValueError instead of deferring a matplotlib
shape error to plot(). lab_insulation/building_uncertainty have no such
gap (no user-supplied frequencies stored). Regenerated llms-full.txt.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ECMA-418-2 Sottek Hearing Model loudness and auditory front-end
Implement the ECMA-418-2:2025 (4th ed.) psychoacoustic loudness metric and
its shared auditory front-end (Sottek Hearing Model):
- Clause 5 front-end: outer/middle-ear cascade (5.1.3, Table 1), 53-band
gammatone-like auditory filter bank (5.1.4), band-dependent segmentation
(5.1.5, Table 4), rectification/RMS (5.1.6-5.1.7), compressive nonlinearity
(5.1.8, Formula 23, Table 2) and threshold in quiet (5.1.9, Table 3).
- Clause 6.2.2-6.2.7 ACF-based tonal/noise specific loudness (reusable by the
later tonality/roughness metrics).
- Clause 8 assembly: tonal/noise power average, average specific loudness,
time-dependent and single representative loudness value.
The front-end helpers are factored for reuse. Calibration verified: a 1 kHz
40 dB SPL sinusoid yields 0.996 sone_HMS (target 1.0). Adds a frozen
EcmaLoudness result with lazy .plot(), exports, and 12 tests.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ECMA-418-2 tonality (Sottek Hearing Model)
Implements the psychoacoustic tonality metric of ECMA-418-2:2025
(Clause 6.2.8-6.2.11) on top of the committed Sottek front-end and
ACF tonal/noise decomposition.
- tonality_ecma() -> EcmaTonality with the single value T (Formula 63),
average specific tonality T'(z) (Formula 53), tonal frequencies
f_ton,z(z) (Formula 55), time-dependent tonality T(l) (Formula 61)
and its frequency f_ton(l) (Formula 62); optional user band [f_low,f_high].
- Full Clause 6.2.3 band averaging with cross-block-size-group ACF
recomputation (loudness keeps its simplified, valid-for-loudness path;
boundary marker added there). Loudness 0.996 sone guard unchanged.
- _tonal_estimate now also returns the DFT-peak tonal frequency, with the
argmax restricted to the lower DFT half to avoid Nyquist-mirror aliasing.
- .plot() via _plotting.plot_ecma_tonality; exports in __init__.
- 13 conformance tests: 1 kHz/40 dB -> 1 tu_HMS calibration, pure tone
>> noise, f_ton tracks the tone, silence -> 0, band restriction.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ECMA-418-2 roughness (Sottek Hearing Model)
Implement the ECMA-418-2:2025 (4th ed.) psychoacoustic roughness metric
(Clause 7), the third Sottek Hearing Model output and new library
functionality. Reuses the Clause 5 auditory front-end from loudness_ecma
(ear filter, 53-band gammatone bank, specific basis loudness) and adds the
roughness-specific chain: start-only zero-padding and fixed 16384/4096
segmentation (5.1.2.2, 5.1.5.2, 7.1.1), the Hilbert envelope with x32
downsampling to 1500 Hz (7.1.2), the scaled envelope power spectrum
(7.1.3), two-step noise reduction (7.1.4), the four-stage spectral
weighting with quadratic-fit modulation-rate refinement, bias correction,
high/low modulation-rate weighting and fundamental-rate estimation
(7.1.5), interpolation to 50 Hz with the distribution-dependent nonlinear
transform, tabulated calibration c_R and asymmetric smoothing (7.1.7), and
the R'(z) / R(l50) / 90th-percentile aggregation (7.1.8-7.1.10).
Uses the standard's tabulated c_R = 0.0180685 (Formula 104), not
reverse-fit: the 1 kHz / 70 Hz / m=1 / 60 dB calibration signal yields
R = 1.0735 asper (near-but-not-exactly 1). OCR-ambiguous formulae
(66, 71, 78-81) cross-checked against the SQAT MATLAB reference; the
optional entropy weighting (7.1.6, needs RPM telemetry) is documented but
not implemented.
Adds roughness_ecma()/EcmaRoughness, a two-panel .plot(), exports, a
10-test TDD suite and plan/notes-ecma418-2-roughness.md. Full suite
752 passed / 12 skipped; loudness (0.996) and tonality (0.99993) oracles
unchanged (both modules untouched).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* test: pin ECMA-418-2 roughness calibration value and document clean-room variance
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 532-2 Moore-Glasberg loudness
Add the stationary Moore-Glasberg loudness model of ISO 532-2:2017 as a
third loudness method alongside the Zwicker (ISO 532-1) and Sottek
(ECMA-418-2) implementations. The full Clause 7 chain is implemented:
fixed outer/middle-ear transfer (Table 1), the level-dependent roex
auditory filter bank and excitation pattern on the ERB-number scale
(Formulae 1-6), the compressive specific-loudness transform (Formulae
7-9, Tables 2-4, C=0.0617 sone/Cam), binaural inhibition (Formulae
10-13) and integration to total loudness with the phon mapping of
Table 5.
Public API: loudness_moore_glasberg_from_spectrum (exact 5.2/5.4),
loudness_moore_glasberg_from_third_octave (29 bands, 5.5) and
loudness_moore_glasberg (signal wrapper), returning a frozen
MooreGlasbergLoudness with .plot() over the ERB-number scale.
Validated against the Annex B reference signals (tones, white/pink
noise, multi-tone complexes, tone-in-noise) to ~1-4%, well inside the
standard's 2.8 phon expanded uncertainty; the definitional anchor
(1 kHz/40 dB/free/binaural = 1.000 sone) holds end-to-end from the
tabulated constant. 50 new tests; full suite 802 passed / 12 skipped.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 532-3 Moore-Glasberg-Schlittenlacher time-varying loudness
Add loudness_moore_glasberg_time() implementing the ISO 532-3:2023 method:
a six-FFT multi-resolution running short-term spectrum (clause 7.3) feeding
the ISO 532-2 excitation and specific-loudness model on the 0.25 Cam grid
with the 532-3 constants (C=0.063, Tables 2/4, E_THRQ/E0=2.307), integrated
by asymmetric attack/release smoothing into short-term (clauses 7.6-7.8) and
long-term (clause 7.9) loudness traces, with peak long-term loudness,
percentiles and a lazy STL/LTL .plot().
Validated against the Annex C.1 tone table (1 kHz 10-80 dB, 3 kHz, 4 kHz,
100 Hz and monaural earphone) to within 0.4 phon, well inside the 2.8 phon
expanded uncertainty; the 1 kHz/40 dB anchor is exact at 1.000 sone. The
ISO 532-2 and ECMA-418-2 oracles are unchanged (831 passed / 12 skipped).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 532-3 per-ear LTL and Formula-14 kernel width (dichotic path)
Two transcription errors in the ISO 532-3:2023 dichotic (per-ear) binaural
path of the time-varying Moore-Glasberg-Schlittenlacher module:
1. Clause 7.9: the long-term loudness must be computed per ear by the
attack/release averager (Formulae 18-21) and then summed. The code ran a
single AGC on the binaural sum S'_L + S'_R; because the attack/release
branch is nonlinear, AGC(S'_L) + AGC(S'_R) != AGC(S'_L + S'_R) for dichotic
input. Keep two LTL states and sum the two long-term loudnesses. For
diotic/monaural the averager is linear in the (scaled) input so the result
is bit-for-bit identical to the old sum-then-AGC.
2. Formula (14)/(15): the smoothing weight is exp(-(B*Di)^2) with B = 0.08 and
Di in Cam. The code evaluated exp(-((0.08 * Di/0.1)^2)) = exp(-(0.8*Di)^2),
a Gaussian ~10x too narrow, and truncated the taps at +/-1.75 Cam. Remove
the spurious /0.1 and widen the kernel to the standard's +/-18 Cam (145 taps
on the 0.25-Cam grid). For diotic/monaural the L/R excitation ratio is 1
regardless of the kernel, so all oracles are unaffected.
No current validation signal exercises the dichotic path (Annex C signals are
diotic/monaural), so the 532-3 tone oracle and every other oracle remain
byte-identical; added focused dichotic tests plus a byte-identical anchor pin.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: numerical conformance report in CI PR comment
Reframe the PR comment into a persistent numerical conformance report:
each implemented standard -> normative expected value/range -> computed
result -> pass/fail with deviation, grouped by domain.
- scripts/conformance_report.py: registry of 21 checks across 17 standards
and 6 domains (filters/weightings, levels/dosimetry, psychoacoustics,
speech, intensity/power, room/building). Emits a headline summary, a
"Numerical validation - filters & weightings" section (per-architecture
IEC 61260-1 class margins + A/C/G weighting deviation vs the normative
curve) and one conformance table per domain. Deterministic, ~2.7 s.
- tests/reference_data.py: single source of truth for the shared normative
tables (IEC 61672-1 Table 3, ISO 7196 Table 2, ISO 717-1 Annex C R),
imported by both the tests and the report so they cannot drift.
- tests/test_conformance_report.py: smoke test asserting every registered
check passes and the Markdown is well-formed.
- comment_pr.py / workflow: conformance report leads the comment; the
test/coverage table is collapsed below. Retire benchmark_filters.py
(numerical content absorbed into the report).
make check green (740 passed, 12 skipped); ruff, mypy --strict, bandit clean.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* refactor: block-A review minors — docstring notes and tonality band-range
Four Minor findings from the plan-17 block-A psychoacoustics reviews. No
change to any validated numeric output; all five oracles stay byte-identical.
1. ISO 532-2 _source_levels: docstring note that clause 7.4 specifies a roex
weighting for X where the code uses a rectangular +/-ERB_n/2 window —
identical for isolated tones, immaterial for the Annex B broadband cases.
2. ISO 532-2 Formula-10 binaural smoothing kernel: the PDF (clause 8.1) states
"Di is changed in steps of 0,1" over -18..+18 Cam; the code confused the
0.1-Cam index offset with Di-in-Cam (the same /0.1 width bug just fixed in
532-3 Formula-14), making the Gaussian 10x too narrow and truncating to
+/-1.8 Cam. Widen to +/-18 Cam (361 taps) with Di = tap*_I_STEP. Provably
inert: all Annex B cases are diotic (ratio 1) or monaural (unit inhibition).
3. ECMA tonality _band_range: implement the exact Formulae 56-57 edge-midpoint
boundary conditions instead of a centre-frequency threshold; only affects
the optional user-band feature. Added a user-band edge test.
4. ECMA tonality reach: comment that the _CBF-1-band upper-edge term is a dead
defensive guard (N_B>0 only for low bands 0..24, so it never limits reach).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* chore: move conformance design doc out of repo root
* feat: clearer conformance report (value-vs-range) + block-A checks + audit fixes
Table 1 (filters): show, per architecture, the measured relative attenuation
at the binding band vs the class-1 limit it must clear (value + range, not
just a margin). Replace the "none" verdict (read as a failure) with a
"By design (ripple/soft rolloff)" label + footnote; Butterworth (default) and
Chebyshev-II are the only mask-compliant architectures. The measured value and
limit are re-derived with the public class_limits on the same designed SOS; a
smoke-test guard asserts the re-derived class-1 margin matches
verify_filter_class (single source of truth).
Table 2 (weightings): separate the informational max-deviation-from-nominal
(at a frequency extreme with wide, asymmetric tolerance) from the compliance
margin at the binding frequency, where the deviation, the +/- tolerance band
and the headroom are co-located so "value vs range" is unambiguous and in-spec.
Audit: verified all 21 checks (expected value/range vs the standard, real
library call, honest tolerance). One fix - ISO 226 anchored at 60 phon @ 2 kHz
-> 60 dB, a genuine Table B.1 value but coincidentally equal to the phon number
(reads as the trivial 1 kHz identity); re-anchored to the non-coincidental
60 phon @ 100 Hz -> 78.5 dB, sourced from reference_data. No library bug found.
Block-A psychoacoustics (+5, single-source expected in reference_data):
ECMA-418-2 loudness (1 kHz/40 dB -> 1 sone_HMS, c_N), tonality (-> 1 tu_HMS,
c_T), roughness (1 kHz/70 Hz/m=1/60 dB -> clean-room 1.0735 asper with a note
that the standard target is 1.0); ISO 532-2 (1 kHz/40 dB -> 1 sone, C=0.0617);
ISO 532-3 (steady 1 kHz/40 dB -> peak LTL 1 sone). 26/26 checks, 20 standards.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: versioned docs/CONFORMANCE.md with make target, CI staleness check and README link
Persist the numerical conformance report as a committed, versioned file so
every reader sees the library's per-standard validation (expected vs computed)
without running CI.
- docs/CONFORMANCE.md: committed report (26/26 checks, 6 domains, 20 standards)
with an auto-generated "do not hand-edit" header linking the standards docs.
- scripts/conformance_report.py: add a --file-header flag that emits that header
for the committed file (PR-comment body stays header-free); coarsen the
informational delta column to 3 decimals so BLAS/FFT-dependent sub-milli
residuals do not churn the file (status/computed columns keep regressions
visible).
- Makefile: `make conformance` regenerates the file; `make install-hooks`
installs the optional pre-commit hook.
- CI: new fast `conformance` job (runtime deps only) fails a PR if the committed
file is stale, telling the author to run `make conformance` and commit.
- hooks/pre-commit: opt-in convenience hook, scoped to src/scripts/reference
changes; CI is the enforcement.
- README + CONTRIBUTING: link and document the generated report.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: advanced psychoacoustics guide, theory, API (EN)
Document the plan-17 block-A advanced psychoacoustics models: Moore-Glasberg
loudness (ISO 532-2/532-3), Sottek Hearing Model loudness, tonality and
roughness (ECMA-418-2:2025).
- Psychoacoustics guide: new "Advanced loudness & sound-quality models"
section with a model-comparison table, executed snippets, parameter tables,
res.plot() one-liners and figure-code collapsibles (docs + site twin,
code blocks byte-identical).
- Theory: excitation-pattern/specific-loudness chain, Sottek front-end,
ACF tonality and envelope-modulation roughness (GitHub-safe math).
- API reference: rows for the 5 functions (+ 2 MG spectral entry points) and
5 result dataclasses with their .plot().
- README, EN landing (standards 26 -> 29: ISO 532-2, ISO 532-3, ECMA-418-2),
CHANGELOG Unreleased/Added, llms regen.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: advanced-psychoacoustics figures (loudness models, Sottek, tonality/roughness, time-varying)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: gemelas ES de psicoacústica avanzada (Moore-Glasberg, Sottek, tonalidad, aspereza)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 532-2 signal-input path builds a narrowband spectrum (pure-tone loudness)
The loudness_moore_glasberg(x, fs) wrapper routed the signal through the
one-third-octave method, which smeared a pure tone across a whole 1/3-octave
band and used a coherent-gain window normalisation that over-counted power by
1.76 dB. A calibrated 1 kHz tone at 40 dB SPL therefore returned 1.327 sone
instead of the definitional anchor of 1.000 sone.
ISO 532-2 is spectrum-based and its exact input (clauses 5.2/5.4) is a set of
discrete sinusoidal components; the excitation pattern (Formula 5) is computed
per component, so a tone must enter as a single line. The wrapper now forms the
signal's narrowband (FFT) line spectrum with a power-preserving (Parseval)
window normalisation and feeds it to loudness_moore_glasberg_from_spectrum. A
1 kHz/40 dB tone now yields 1.0001 sone / 40 phon and multi-tone signals match
the exact spectrum path. The validated spectrum and third-octave paths are
byte-identical.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: correct MG signal-path mechanism text and presentation default
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 87 review round 1 — float-equality guards, 532-3 FFT window length, tonality short-signal, report/figure fixes
- roughness_ecma: replace three float == 0.0 checks with abs>eps guards
(SonarCloud gate); roughness oracle byte-identical (1.0735).
- loudness_moore_glasberg_time: per-window FFT length (>= window length) so
the 64 ms window is not truncated at 44.1/48 kHz (was ~0.74 dB low in the
20-80 Hz band). 32 kHz anchor byte-identical (n_max 1.0000044713237626).
- tonality_ecma: short signals fall back to averaging all blocks (matches
loudness_ecma), not the final block only; validated anchor unaffected.
- conformance_report: document deliberate 273.0 (speed of sound) vs 273.15
(C1/C2) split matching the library; complete bind_side docstring.
- generate_graphs: roughness annotation uses computed peak fmod; Sottek
metrics use tu_HMS/sone_HMS units; ES translations + 8 figures regenerated.
- hooks/pre-commit: drop set -e so a benign regen failure does not block the
commit (CI staleness check is the gate).
- test_iec_weighting_table3 docstring, Makefile .PHONY completion.
All 5 psychoacoustics oracles byte-identical; make check 868 passed / 12
skipped; ruff + mypy + bandit clean; conformance 26/26.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: localize decimal commas in Sottek-unit figure labels (underscore guard)
The `_` guard in `_translate_figure` skipped decimal-comma localization for
any label containing an underscore. The new Sottek/HMS unit tokens
(`sone_HMS`, `tu_HMS`, `sonios_HMS`) carry underscores, so Spanish figures
regressed to dots (`8.0`, `0.42`) instead of commas (`8,0`, `0,42`).
Drop the underscore guard: the decimal-comma regex only rewrites a bare
`digit.digit` not adjacent to more digits/dots, so unit identifiers and
version numbers (5.3.3) stay intact while genuine decimals get commas.
Mathtext is still skipped via the `$` guard. Regenerated the affected ES
figures (sottek_specific_loudness, tonality_roughness_demo).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: responsive figure sizing — full-width on mobile, 92% plots on desktop (plan 16 T4)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: snippet clarity wave — recording/excitation convention (plan 16 T1)
Apply the recording/excitation naming convention from the P16 audit across
the calibration, intensity, block-processing, time-weighting, levels,
weighting and room-acoustics guides (docs/ + EN guides + ES guides).
- calibration: ref_signal->calibrator_recording, signal->recording (J1 cascade)
- intensity: probe-recording synth + swap-in disclosure on p1/p2
- block-processing / time-weighting: audio_blocks provenance comments
- levels / time-weighting / weighting: signal->recording, provenance upgraded to convention
- room-acoustics: play/record framing on sweep/recorded; synthetic-IR stand-in note
EN docs/guides kept code-identical; ES mirrored with UNE wording.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: one-line canonical plots on result objects (plan 16 T2)
Add a thin plot(ax=None, **kwargs) method to each result dataclass that
renders the essence of its guide's canonical figure in one line, following
the sklearn/statsmodels/librosa pattern. matplotlib stays a soft dependency:
importing phonometry and computing works without it; .plot() raises a clear
ImportError with install guidance. Rendering lives in a new lazy _plotting
module; dataclass methods are thin delegates.
Covers ZwickerLoudness, STIResult, WeightedRatingResult/ImpactRatingResult,
RoomAcousticsResult, SoundPowerResult/ReverberationSoundPowerResult/
SoundPowerIntensityResult, IntensityResult and a new frozen DecayCurve
(returned by decay_curve, tuple-unpackable for backward compat). Rating
results gain band_centers/measured/shifted_reference fields for the figure.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: plot kwargs forwarding, deterministic invalid-band test, octave-impact annotation
- _plot_rating now accepts **kwargs and forwards to the measured curve, so
result.plot(linewidth=2) no longer raises TypeError; parametrized test
covers every public .plot() forwarding a benign kwarg to its primary artist.
- Replace skip-if-all-valid room-acoustics invalid-band test with a directly
constructed RoomAcousticsResult (500 Hz band flagged invalid); assert exactly
the flagged bars are hatched and greyed.
- plot_impact_rating keeps the shifted-reference curve normatively honest
(ref - shift) and marks/annotates the 500 Hz read value with the ISO 717-2
Clause 4.3.2 -5 dB octave rule instead of distorting the curve.
- Move _bar_width next to its only consumer, plot_intensity.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: .plot() one-liners and figure-code collapsibles (plan 16 T3)
Add one-line res.plot() mentions to every guide section whose result type
gained .plot() in T2, and figure-code collapsibles (both the one-liner and
the manual matplotlib path) under the five .plot()-backed data figures.
Document .plot() availability and the DecayCurve return type in the API
reference and CHANGELOG. EN docs + EN/ES site; llms-full.txt regenerated.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: collapsibles for STI-vs-T60 and open-plan figures
Controller follow-up to plan 16 T3: the two figures are reproducible with
public API only. STI-vs-T60 sweeps sti_from_impulse_response over synthetic
exponential IRs at a 10-point T60 grid; the open-plan collapsible rebuilds
the D2,S regression and rD/rP crossings from the OpenPlanResult fields.
EN docs + EN/ES site; executed on Agg; llms-full.txt regenerated.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: final-review polish — result-field docs, comment hygiene, naming
Document band_centers/measured/shifted_reference on WeightedRatingResult
and ImpactRatingResult (CHANGELOG + EN/ES API tables), strip internal plan
tags from theme-images.css and test_result_plots.py, note that [plot] also
powers result .plot() methods, tighten the plot_zwicker_loudness docstring
to say the two-panel layout only occurs when ax is None, sentence-case the
intensity provenance comments, and rename the shadowed impact rating var to
res_imp on the room-acoustics pages.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 86 review round 1 — log-axis bars, style kwargs overrides, snippet physics and notation
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
The project Wikidata item (Q140451720) was deleted by a Wikidata admin
in a notability sweep on 2026-07-07, so the sameAs entry points at a
nonexistent entity.
Claude-Session: https://claude.ai/code/session_016KQnnNLPVouuFYJqLd6kzv
* feat: ISO 3744/3746 sound power from pressure measurements
Add sound_power.py: sound power level of a noise source from sound
pressure levels over an enveloping measurement surface, ISO 3744:2010
(engineering, grade 2) and ISO 3746:2010 (survey, grade 3).
- sound_power_pressure(): per-band LW and A-weighted LWA from an
(NM, NB) level array over a hemisphere (radius) or parallelepiped
(dimensions + distance); surface area from the standard's closed forms
for 1/2/3 reflecting planes; energy average (Eq. 12), background K1
(Eq. 16), environmental K2 (Eq. A.2), apparent directivity index
(Eq. 7), expanded uncertainty.
- measurement_positions(): normative Annex B coordinates
(Tables B.1/B.2/B.3), grade- and plane-dependent subsets.
- background_noise_correction(): K1 with engineering/survey criteria.
- environmental_correction(): K2 from A, Sabine T+V, or alpha*Sv.
- SoundPowerResult (frozen dataclass), SoundPowerWarning.
Tests: 24 cases incl. monopole-over-plane exact LW recovery, radius
independence, K1/K2 closed forms, LWA via Annex E, and validations.
make check green (543 passed, 12 skipped); ruff/bandit/mypy --strict clean.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: directivity index under background correction, citation hygiene
Broadband K1 for the apparent directivity index was collapsing to the
surface-area term 10*lg(S/S0) (~+22 dB inflation with background
correction). Compute the true energy-based broadband K1 (per-band Eq. 16
aggregated over bands) so DI is correct in both paths.
Also: multi-band sound_power_level_a without frequencies now returns NaN
(A-weighting needs band centres); dropped PDF page-number citations from
Annex B/E table comments; corrected the survey parallelepiped position
citation to ISO 3746 clause C.1 / Figure C.7.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 3741 reverberation-room sound power
Add the ISO 3741:2010 precision (grade 1) methods for determining sound
power in a reverberation test room, in a new sound_power_reverberation
module:
- sound_power_reverberation() — direct method (Eq. 20): LW from the mean
room SPL and the Sabine equivalent absorption area A = (55,26/c)(V/T60),
with the full normative bracket: 10 lg(A/A0), 4,34*(A/S), the Waterhouse
boundary term 10 lg(1 + S c/(8 V f)), the reference-quantity (C1) and
radiation-impedance (C2) meteorological corrections and the -6 dB
constant. Speed of sound c = 20,05*sqrt(273 + theta).
- sound_power_comparison() — comparison method (Eq. 21) with a reference
sound source: LW = LW(RSS) + (Lp(ST) - Lp(RSS) + C2).
- ReverberationSoundPowerResult frozen dataclass; frequency-dependent
background correction K1 (Eq. 14, same formula as ISO 3744) with the
precision-grade 6/10 dB criteria; A-weighted total via Annex F.
Reuses the ISO 3744 energy-average and A-weighting helpers. Tests cover
exact inversion of Eq. (20), the Waterhouse term vanishing at high
frequency, the comparison method exact by construction, and synergy with
room_parameters T30 feeding the direct method.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 3741 room-qualification advisories and validation ordering
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 9614-2 sound power by intensity scanning
Add sound_power_intensity() computing per-band LW = 10lg(sum Pi/P0) from
per-segment signed normal intensity and areas (partial power Pi = <In,i>*Si),
the scanning-method field indicators FpI (Eq. A.1) and F+/- (Eq. A.2), the
repeatability criterion 3, and the achieved engineering/survey grade per band
(Annex B criteria 1-3). Negative total power flags bands as non-determinable
(clause 9.2); negative partial power raises SoundPowerWarning. Reuses
dynamic_capability_index for Ld = dpI0 - K (Table 1).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 16283-2 impact sound insulation and ISO 717-2 ratings
Add field impact sound insulation (ISO 16283-2, tapping machine) and the
ISO 717-2 single-number weighted impact rating with the CI spectrum
adaptation term, reusing the verified reference-curve shift engine.
- impact_insulation(li, t2, *, volume=None, t0=0.5): per-band
L'nT = Li - 10 lg(T/T0) (Formula (1)) and L'n = Li + 10 lg(A/A0) with
A = 0,16 V/T, A0 = 10 m2 (Formula (2)); positions energy-averaged.
- weighted_impact_rating(values_by_band, bands=None): Ln,w / L'n,w /
L'nT,w (Clause 4.3, octave -5 dB rule) and CI (Clause A.2.1, energetic
sum 100-2500 / 125-2000). Impact unfavourable deviations (measurement
exceeds reference, sign opposite to airborne) reduce to the airborne
search on negated curves, so _best_shift is reused verbatim.
- _best_shift: tolerance absorbs float noise at the exact bound (sums are
true multiples of 0,1 dB); airborne behaviour unchanged.
Verified against ISO 717-2 Annex C worked examples (Table C.1 Ln,w=79,
CI=-11; Table C.3 octave Ln,w=54, CI=0) and an independent brute-force
shift search on 10 000 random curves per band set. 2013 and 2020 editions
are identical for all implemented maths (2020 only adds Annex D rubber
ball, out of scope).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 354 sound absorption in a reverberation room
Add phonometry.sound_absorption implementing BS EN ISO 354:2003:
- absorption_area(t60, volume, ...): equivalent sound absorption area
A = 55,3*V/(c*T) - 4*V*m (Eq. 5/7), speed of sound from Eq. (6).
- absorption_coefficient(t1, t2, volume, sample_area, ...): alpha_s =
(A2-A1)/S built from Eq. (8)/(9), per-measurement c1/c2, unclamped
(alpha_s > 1 valid, clause 3.7 NOTE 2); warns when T2 >= T1.
- attenuation_from_alpha: ISO-354 conversion m = alpha/(10 lg e) from an
ISO 9613-1 attenuation coefficient (m deferred to ISO 9613-1, default 0).
Composes directly with room_parameters() T20/T30 outputs. 25 tests
(exact T->A->T inversion, synthetic-alpha recovery, zero-m air term,
two-temperature Eq. (8), range validation, room_parameters synergy).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 354 room and sample-size advisories
Add advisory AbsorptionWarning (never raising; result still returned) for
setup conditions outside ISO 354:2003 limits:
- Room volume below the 150 m3 minimum of clause 6.1.1, in both
absorption_area and absorption_coefficient.
- Sample area outside the clause 6.2.1.1 range 10 <= S <= 12 m2, with the
upper limit scaled by (V/200)^(2/3) when V > 200 m3, in
absorption_coefficient.
The volume advisory is emitted once by absorption_coefficient and suppressed
in its internal absorption_area calls to avoid duplicate warnings.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: deferred review minors — airborne brute-force net, advisory consistency, labels
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: sound power guide, impact and absorption sections, theory and API (EN)
Add a "Sound Power" guide covering the three LW routes (ISO 3744/3746
enveloping surface, ISO 3741 reverberation room with Waterhouse/C1/C2,
ISO 9614-2 intensity scanning), with a method-comparison table, executed
snippets and parameter tables. Extend the Room & Building Acoustics guide
with impact sound insulation (ISO 16283-2 + ISO 717-2, L'nT/L'n, tapping
machine, CI) and sound absorption in a reverberation room (ISO 354).
Add the matching theory sections (K1/K2, Waterhouse, C1/C2 derivations;
impact sign conventions; Sabine absorption) and API-reference rows for all
new public names. Update the landing standards count to 26, the sidebar,
README highlights/table, CHANGELOG and llms-full.txt.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: sound-power and impact figures and diagrams
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: include the sound-power guide in llms coverage
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: reposition Ln,w annotation in the impact figure
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: gemelas ES de potencia sonora, impacto y absorción
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: final-review findings — comparison K1 reporting, citations, knee unification, docs attribution
- sound_power_comparison now reports the test-source K1 actually applied
per band (zero when no background), matching the dataclass docstring;
LW unchanged.
- Replace gitignored notes-iso3744-3746.md citation in the DI comment with
the standard reference (ISO 3744:2010, 3.24 / Eq. 7 context).
- Unify the K1 zeroing knee to the >= form (ΔLp >= 15 -> no correction).
- Broaden SoundPowerWarning docstring to name all emitters (ISO 3744/3746,
ISO 3741, ISO 9614-2).
- Fix docs misattributing K2 to ISO 3741 (ISO 3744 K2 vs ISO 3741 absorption
term) across EN/ES room-acoustics guides.
- EN impact-setup diagram uses dot decimals (0.16); regenerate the 4 variants.
- CHANGELOG: label the ISO 717-2 Annex C value Ln,w = 79.
- Warn-after-validate in sound_power_comparison; "is raised" -> "is emitted".
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 84 review round 1 — per-band DI and K2, sweep-reversal repeatability, validation and broadcast ergonomics
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 84 round 2 — partial room-data validation, result-field docs
Raise a clear ValueError in environmental_correction (and its
sound_power_pressure pass-through) when exactly one member of a
room-data pair (reverberation_time/room_volume or
mean_absorption_coefficient/room_surface) is supplied, instead of
silently returning K2=0 as if free field. All-None remains the
legitimate free-field K2=0 path.
Document frequencies on the ReverberationSoundPowerResult row and
negative_band as a standalone per-band boolean field on the
SoundPowerIntensityResult row across docs/api-reference.md and the
EN/ES site twins.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: audit wave A — inter-sample LCpeak, cheby2 class-1 default, 144 kHz weighting target
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: audit wave A — decay validity thresholds, variant sharpness anchors, N5 phase, STIPA warning, Farina guard
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: audit wave B — self-contained snippets, tables, sharpness/open-plan figures, orphans
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: audit wave C — remaining minors and optimizations across the library
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: document the audit-wave parameters across guides and API tables
Cover the public parameters added in the audit fix waves across all three
documentation trees (docs/, site EN, site ES):
- lc_peak(..., oversample=8): inter-sample peak recovery (levels guide + API)
- calculate_sensitivity(..., narrowband=False): coherent tone estimator
(calibration guide + API)
- sound_intensity(..., bias_correct=False): finite-difference bias correction
(intensity guide + API)
- room_parameters/decay_curve(..., zero_phase=False): 125 Hz short-T bias
(room-acoustics guide + API)
- OctaveFilterBank/octavefilter attenuation default 60 -> 72 and cheby2
class-1 note (filter-banks guide + API)
- STIPA < 15 s UserWarning (psychoacoustics guide + API)
Also refresh dependent behaviour notes: ln_levels attack skip 2*tau -> 5*tau,
T20/T30 validity thresholds 35/45 -> 46/54 dB, and the zero-phase broadband
~0.2-0.3 dB band-level caveat. Regenerated llms-full.txt.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: truthfulness fixes from the final audit-branch review
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: 2000 Hz, not 2 ms, in the N5/N10 docstring
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 82 review round 1 — bias-correction cutoff, parabolic-peak guard, complexity, snippet self-containment
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
The LICENSE file carried the GPLv3 text from the 2020 initial commit,
while every published declaration — the PyPI classifier in
pyproject.toml, CITATION.cff, .zenodo.json and the README badge and
license section — has stated MIT throughout. The project is and always
was published as MIT; this replaces the stale GPLv3 text with the MIT
license so the repository is coherent.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 18233 sweep/MLS impulse-response acquisition
Add src/phonometry/room_ir.py implementing the ISO 18233:2006
deterministic-excitation IR front end: exponential sine sweep with exact
analytic phase (Annex B), linear spectral-division deconvolution with a
Farina inverse-filter option for harmonic separation (B.5), and
maximum-length-sequence generation (LFSR, orders 2-20) with circular
cross-correlation recovery (Annex A).
Validated against closed forms: known IIR bandpass recovered within 0.1 dB
in band (sweep and MLS), ideal chain to a band-limited delta, +3 dB SNR per
sweep-duration doubling (B.6), and all MLS orders verified as true
maximum-length sequences (autocorrelation L / -1).
433 passed, 12 skipped; ruff, mypy --strict and bandit clean.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 3382 room acoustic parameters from impulse responses
Add src/phonometry/room_acoustics.py implementing ISO 3382-1:2009 and
ISO 3382-2:2008 analysis of measured impulse responses:
- decay_curve(): Schroeder backward integration of the squared IR
(5.3.3, Eq. 1) with background-noise truncation at the noise/slope
crossing and exponential tail compensation (Eq. 3), broadband or per
IEC 61260 fractional-octave band.
- room_parameters(): per-band EDT (0/-10 dB, A.2.2), T20 (-5/-25 dB)
and T30 (-5/-35 dB) by least-squares fits (ISO 3382-2 Annex C),
clarity C50/C80 (Eq. A.10), definition D50 (Eq. A.11) and centre
time Ts (Eq. A.13), octave bands 125 Hz-4 kHz by default with a
one-third-octave option and a broadband mode.
- Validity flags per the 5.3.3 dynamic-range criterion (noise at least
evaluation range + 15 dB below the IR maximum: 25/35/45 dB) plus the
Annex B curvature indicator C = 100*(T30/T20 - 1).
Tests validate against closed forms for exponential decays (EDT = T20 =
T30 = T within 1 %; C_te = 10*lg(exp(13.8155*te/T) - 1); D50 =
1 - exp(-0.6908/T); Ts = T/13.8155), the exact C50/D50 relation
(Eq. A.12), double-slope decays (EDT < T20 < T30) and noise-driven
validity-flag behaviour, with tolerances far below the Table A.1 JNDs.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* chore: move figure/diagram generators to scripts/
generate_graphs.py and generate_diagrams.py join benchmark_filters.py
and gen_llms.py in scripts/, where dev tooling already lives. Updated
the Makefile graphs target, the sys.path bootstrap in the graph guard
tests and in generate_graphs.py itself (src/ is now one level up), the
CONTRIBUTING instructions and the theme-images.css pointer. Verified:
module loads from the new location and regenerating the SVG diagrams
produces byte-identical output.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 3382-3 open-plan office spatial metrics
Add open_plan_metrics() computing the ISO 3382-3:2012 single-number
quantities from a line of workstation measurements: spatial decay rate
D2,S and nominal speech level Lp,A,S,4m (Clause 6.2, Eq.5; 2-16 m
positions on a log-distance regression) and distraction/privacy
distances rD/rP (Clause 6.3; STI-vs-distance linear regression crossing
0,50 and 0,20). Returns a frozen OpenPlanResult; validates the minimum
of four positions (5.2.2) and equal array lengths.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 16283-1 field insulation and ISO 717-1 weighted ratings
Add phonometry.insulation implementing field airborne sound insulation
(ISO 16283-1:2014) and single-number weighted ratings with C/Ctr
(ISO 717-1):
- airborne_insulation: per-band level difference D (Formula (1)),
standardized level difference DnT = D + 10 lg(T/T0) (Formula (2)) and
apparent sound reduction index R' = D + 10 lg(S/A), A = 0,16 V/T
(Formula (4)/(5)); energy-averages microphone positions (Formula (9)).
- weighted_rating: reference-curve shifting method (Clause 4.4, Table 3)
with the 32,0/10,0 dB unfavourable-deviation bound, plus C/Ctr from the
Table 4 spectra (Clause 4.5). Reference values, spectra and method are
identical in the 2013 and 2020 editions.
- energy_average_level: ISO 16283-1 Formula (9) helper.
Verified against the ISO 717-1 Annex C worked example (Rw(C;Ctr) =
30(-2;-3), unfavourable sum 31,8 dB) and exact-bound / tipping edge
cases. 20 new tests; make check green (484 passed, 12 skipped).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: room & building acoustics guide, theory, API and landing (EN)
New "Room & building acoustics" guide (docs/ + Starlight twin) covering the
full measurement chain: ISO 18233 swept-sine/MLS impulse-response acquisition,
ISO 3382-1/2 decay analysis and room parameters (EDT/T20/T30/C50/C80/D50/Ts),
ISO 3382-3 open-plan speech metrics, and ISO 16283-1 / ISO 717-1 field
insulation with weighted ratings. Adds the matching theory section (Schroeder
integration, regression windows, C/D/Ts, D2,S, DnT/R', reference-curve method),
API-reference rows for all new public names, sidebar entry, README highlight +
docs row, EN landing card, CHANGELOG entries, docs index row, and regenerated
llms-full.txt. All snippets validated; site build and make check green.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: Schroeder, ISO 717-1 rating and insulation-setup figures
Add three room/building-acoustics documentation figure sets (each in
en/es x light/dark):
- schroeder_decay: synthetic IR, Schroeder backward integration with
EDT/T20/T30 regressions and evaluation ranges (ISO 3382); annotated
values match room_parameters.
- insulation_rating: ISO 717-1 Annex C example with shifted reference
curve, unfavourable-deviation shading and Rw read at 500 Hz; Rw, C,
Ctr and the unfavourable sum come from weighted_rating.
- diagram_insulation_setup: ISO 16283-1 airborne setup plan view with
the normative minimum distances (clauses 7.6 and 7.2.2).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: gemelas ES de acústica de salas y edificación
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: terminología UNE — ponderación temporal en lugar de balística (ES)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: markdown/README parity with the site + llms guide coverage
Scrub GitHub-unsafe display math (\, thin-spaces and escaped \{ \}) from
docs/intensity.md and docs/psychoacoustics.md — the only two guides that
missed the GitHub-safe math conversion of PRs #78/#79 — matching the safe
forms already used in docs/theory.md. Align calibration's sensitivity
symbol/formula to the site ($S$, single-line \qquad).
Extend scripts/gen_llms.py PAGES with the three omitted guides
(psychoacoustics, intensity, room-acoustics) and regenerate llms.txt /
llms-full.txt so AI-facing artifacts cover all 14 docs pages.
The guides/references were otherwise already in parity (didactic code
comments, theory sections, tables, and figures from PRs #77/#80 all
present); docs/README index and README highlights/table verified complete.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: final-review findings — open_plan validation, limits rename, Farina caveat, MLS averaging test
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: ISO 18233 measurement-chain diagram and guide fixes
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: fix sweep_signal cross-reference in Farina caveat
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 81 review round 1 — inverse-filter guards, truncation threshold, IR input validation, docs pipeline wording
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: blank lines around CONTRIBUTING fence (MD031)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
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
* feat: impedance tube, airflow resistance and ISO 11654 absorption rating
Clean-room from the standards (no third-party tool referenced):
- impedance_tube.py: ISO 10534-2 two-microphone transfer-function method
(reflection factor, surface impedance/admittance, absorption), ISO 10534-1
standing-wave-ratio method, and ASTM E2611 four-microphone transfer-matrix
method (wave decomposition, two-load/one-load T-matrix, transmission loss,
material wavenumber and characteristic impedance). Kelvin (ISO) vs Celsius
(ASTM) speed-of-sound helpers keep the two sign/unit conventions apart.
- airflow_resistance.py: ISO 9053-1 static method (through-origin quadratic
fit) and ISO 9053-2 alternating method (Formula 2), with R_s in Pa·s/m and
resistivity in Pa·s/m².
- absorption_rating.py: ISO 11654 weighted absorption alpha_w (0.05-grid
reference-curve shift, L/M/H shape indicators, absorption classes A-E),
verified against the Annex A.1/A.2 worked examples.
103 module tests plus public-export checks; physics-identity anchors for the
tube (rigid wall alpha=0, perfect absorber r=0/Z=rhoc, det(T)=1, air-layer
recovery to <1e-9) since neither tube standard prints a numeric example.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* feat: ISO 9053-2 Annex A kappa', materials docs, figures & conformance
Address per-module review of the impedance-tube / materials modules:
- ISO 9053-2:2020 Annex A (normative): implement the heat-conduction-corrected
effective ratio of specific heats kappa' (Formula (A.7)) and the thermal
boundary-layer thickness b (A.4/A.5) as effective_kappa() and
thermal_boundary_layer_thickness(). The Annex A.3 worked example is a
digit-exact oracle (b = 1.83e-3 m, kappa' = 1.370). Fix the misleading
_ADIABATIC_KAPPA citation: 1.4 is the uncorrected adiabatic fallback, not the
Annex A output.
- absorption_rating: delegate .plot() to _plotting.plot_weighted_absorption,
matching the repo idiom (reuses _unfavourable_mask / _new_axes).
- docs/materials.md: new guide covering ISO 11654, ISO 9053-1/-2 and
ISO 10534-1/-2 + ASTM E2611, with runnable examples and three figures
(EN/ES, light/dark). Linked from the docs index.
- Conformance report: five materials checks (ISO 11654 Annex A.1/A.2,
ISO 9053-2 Annex A.3 b + kappa', ISO 10534-1 SWR); shared oracles in
reference_data.py. 48/48 checks pass.
Impedance-tube module review returned Approved (no code changes needed;
the Eq (20) phase citation was verified correct against ISO 10534-1 p.5).
Claude-Session: https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm
* docs: experimental-setup diagrams for the materials methods
Add hand-drawn SVG setup diagrams (EN/ES, light/dark) for the new methods,
mirroring the didactic style of the room-acoustics / outdoor diagrams, and
embed them in docs/materials.md:
- Impedance tube (ISO 10534-2): loudspeaker, tube, two flush microphones at
spacing s and distance x1 from the specimen face, rigid backing, incident/
reflected plane waves and the Eq. (17)-(19) relations.
- Four-microphone transmission-loss tube (ASTM E2611): source, two upstream
and two downstream microphones, adjustable two-load termination, the A/B/C/D
travelling waves, and s1/s2/l1/l2/d labelled to match the library's
transfer_matrix_two_load / wave_decomposition parameters (l1, l2 measured
from the specimen front reference plane).
- Airflow resistance (ISO 9053-1/-2): the static rig (specimen, laminar flow
q_v, differential manometer Δp) and the alternating rig (piston 1-4 Hz,
cavity V, specimen/airtight termination, cavity level L_p).
Also apply the whole-branch review's two before-merge items:
- airflow_resistance.py: add the module-level __all__ it was missing.
- impedance_tube.face_quantities: add functional tests (progressive/backward
wave impedance = ±rho*c) so the exported ASTM Eq. (21) helper is covered.
SVG text() now XML-escapes &, < and > so labels may contain them literally.
Diagrams validated by subagent against the standards' figures and the
library's public-API variable names; the ASTM l2 origin was corrected from
the rear to the front specimen face to match the library convention.
Claude-Session: https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm
* fix: harden ISO 11654 input coercion (bot review)
_coerce now rejects multi-dimensional array input instead of silently
flattening it (CodeRabbit) and rejects non-finite values with a clear message
instead of a cryptic downstream integer-conversion error (Gemini). Adds tests
for both.
Claude-Session: https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm
* fix: flag ill-conditioned transfer-matrix solves (bot review)
transfer_matrix_two_load / transfer_matrix_one_load now emit an
ImpedanceTubeWarning when the solve denominator is near-singular relative to
its operands — two insufficiently-different loads (two-load) or a near-resonant
geometry (one-load) — instead of silently propagating inf/nan (CodeRabbit).
Adds tests for both the singular and the well-conditioned case.
Claude-Session: https://claude.ai/code/session_01JJLhQzF5hTdnEi3bfmqnqm
* test: run the full ISO 532-1 Annex B validation from in-repo signals
Commit the ISO 532-1:2017 electronic-attachment validation data under
tests/data/iso532_1/ with a README documenting provenance, ISO copyright,
the source URL and a removal policy. This turns the twelve previously
skipped Annex B.5 technical-signal tests into always-on checks:
- Add the twelve recorded B.5 technical signals (byte-identical to the ISO
attachment) and gather the existing tone-pulse/level/expected fixtures
into the same folder.
- Read the WAVs with the stdlib `wave` module (no soundfile dependency) and
scale per Annex B.1 (full scale = 100 dB SPL -> 2*sqrt(2) Pa peak).
- Validate each signal in the sound field its ISO results workbook was
computed in; signal 15 (vehicle interior) is diffuse, the rest free. This
was the sole failing case and it is a field mismatch, not an algorithm
error. Nmax now reproduces the workbook to < 0.01 % on all twelve, so the
Nmax tolerance is tightened from 5 % to 0.1 %.
- Add two Annex B.5 rows (free + diffuse technical recording) to the
numerical conformance report; docs/CONFORMANCE.md regenerated (34/34).
Also clean the pre-existing mypy --strict debt in scripts/ (13 findings in
conformance_report.py, generate_graphs.py and generate_diagrams.py) and
extend the lint gate and CI to type-check scripts/ alongside src/ so it
cannot regress.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* test: harden ISO 532-1 WAV loading and graceful data absence (PR review)
Addresses the PR #90 bot review:
- Read WAVs with the little-endian dtype '<i2' (RIFF is little-endian, not
host-native) and keep only channel 0 of any multi-channel file, in both
the test reader and the conformance helper.
- Load the Annex B expected-values JSON defensively: the module no longer
crashes on import when the ISO data folder is absent; the six data-backed
tests carry a `requires_iso_data` skip marker instead, honouring the
README's removal policy while the synthetic-signal tests keep running.
- Raise a clear FileNotFoundError (not an IndexError) in the conformance
helper when no Annex B.5 WAV matches.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* test: unify the ISO 532-1 data root behind ISO532_1_TESTDATA (PR review)
CodeRabbit noted the presence gate and expected-values JSON were tied to
the hard-coded in-repo path while the Annex B.5 lookup honoured the
ISO532_1_TESTDATA override, so a valid override with the packaged JSON
absent would still skip. Derive the single DATA root from the override
(defaulting to the in-repo fixtures) and read the JSON, the presence gate
and the B.5 recordings all from it; drop the separate ISO_DIR.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* fix: mypy --strict on PR-C script code surfaced by the merged gate
Merging main (PR #89) brought the ISO 9613-2/9612 figure and conformance
helpers under this branch's extended `mypy src scripts` gate for the first
time: annotate the ISO 9612 Annex D task builder's return type and cast a
numpy int bar-label position to float.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* feat: ISO 9613-1 atmospheric sound absorption
Full alpha(f, T, RH, p) model per ISO 9613-1:1993 clauses 6.2-6.4:
oxygen/nitrogen relaxation frequencies, water-vapour molar
concentration from relative humidity, classical + rotational +
vibrational terms. Validated against a 13-point Table 1 grid
(worst deviation 0.39 %, rounding-limited, vs the standard's
stated +/-10 % accuracy) with the exact midband convention of
Eq. (6) Note 5. air_attenuation_m() feeds the ISO 354 m parameter
directly, closing the user-supplied gap. Two conformance checks
added (34/34).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* refactor: C1 review nits — test name and comment direction
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* test: extend ISO 9613-1 Table 1 oracle against the full standard text
The full 37-page ISO 9613-1:1993 text (now in hand) confirms every
implemented constant of Eqs. (3)-(6), the Annex B psychrometric
conversion and the clause 4.2 reference conditions exactly as coded.
Oracle extended from 13 to 23 Table 1 points, adding the 20-50 degC
sub-tables 1(i)-1(p) unavailable in the interim source. Worst
deviation unchanged at 0.385 % (rounding-limited).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 9613-2 outdoor sound propagation
Implement the ISO 9613-2:1996 general method for octave-band outdoor sound
attenuation: geometrical divergence (Eq. 7), atmospheric absorption (Eq. 8,
via ISO 9613-1 air_absorption), ground effect (general per-region method of
7.3.1 with the Table 3 a'/b'/c'/d' functions, Eq. 9, plus the alternative
method of 7.3.2, Eq. 10, and the DOmega index Eq. 11), and screening (Eq. 12-18
with the C2/C3 factors, single/double diffraction, Kmet and the 20/25 dB caps).
Adds the Cmet meteorological correction (Eq. 21/22) and the LfT(DW)=Lw+Dc-A
receiver-level composition (Eq. 3/6). Frozen dataclass result exposes the
per-term breakdown (Adiv/Aatm/Agr/Abar). New module, tests and exports.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 9612 occupational noise exposure strategies and uncertainty
Add phonometry.occupational_exposure implementing the three ISO 9612:2009
measurement strategies and the normative Annex C uncertainty budget:
- task_based_exposure (Clause 9): per-task energy average (Eq 7), task
contributions (Eq 8), daily level (Eq 9/10); uncertainty Eq C.3 with
sampling u1a (Eq C.6), duration u1b (Eq C.7) and sensitivity coefficients
c1a (Eq C.4) / c1b (Eq C.5).
- job_based_exposure (Clause 10): effective-day energy average (Eq 11),
daily level (Eq 12); Table 1 minimum-duration check; uncertainty Eq C.9.
- full_day_exposure (Clause 11): whole-day averaging (Eq 11/13) with the
Clause 11.3 three-measurement / 3 dB spread rule.
- Table C.4 (job/full-day c1*u1, bilinear interpolation), Table C.5 (u2),
u3 = 1.0 dB, coverage factor k = 1.65 (one-sided 95 %).
Frozen dataclasses (Task, TaskContribution, ExposureResult), ExposureWarning
advisories for the sampling rules, and exports appended to __init__.
Tests reproduce the Annex D (task), E (job) and F (full-day) worked examples
to the standard's precision, plus closed-form oracles (single 85 dB/8 h task
= 85; equal-level tasks; -3.01 dB per halved duration; k = 1.65) and the
Table C.4 / Table 1 lookups. make check green: 1167 passed, 12 skipped.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 9612 review corrections — Table C.4 digits and Eq C.7 denominator
Two Table C.4 cells verified against the page image at 200 dpi and
corrected (N=6/u1=3.5: 3.4 -> 3.3; N=20/u1=2.0: 0.6 -> 0.5), the test
that echoed the wrong N=6 cell now pins the printed value plus the
corrected N=20 cell. The duration_samples branch of Eq C.7 divides by
J(J-1) like Eq C.6 (standard error of the mean), with a regression test
pinning u1b = 1.0 h for samples (4, 6) h. Module docstring now states
the Lp,Cpeak uncertainty scope cut (Table C.5, NOTE 1).
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* test: ISO 9613-2 and ISO 9612 conformance checks
Add a new "Outdoor propagation & occupational exposure" conformance domain
with seven closed-form / worked-example checks backed by shared constants in
tests/reference_data.py:
- ISO 9613-2:1996 Eq. (7) geometrical divergence Adiv = 51 dB at 100 m.
- ISO 9613-2:1996 Table 3 ground limit b'(0) = 10,1 -> Agr(250 Hz) = 17,2 dB
(porous ground, hs = hr = 0, dp -> inf).
- ISO 9613-2:1996 clause 7.4 single/double diffraction 20/25 dB caps.
- ISO 9612:2009 Annex D/E/F daily exposure LEX,8h and expanded uncertainty U
(84,3/2,7, 88,1/3,8, 90,1/3,4 dB), the Task objects rebuilt from the shared
input table.
Registry grows 34 -> 41 checks across 8 domains; docs/CONFORMANCE.md
regenerated via make conformance. Registration and reference-data consistency
tests pin the constants to the published values.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* docs: figures for outdoor propagation and occupational exposure
Add four documentation figures (each EN/ES x light/dark) plus a setup diagram:
- air_absorption_alpha: ISO 9613-1 alpha(f) over frequency at four
temperature/humidity combinations (relaxation roll-off, f^2 growth).
- outdoor_attenuation_breakdown: ISO 9613-2 per-term stacked bar (Adiv, Aatm,
Agr, Abar) with the total A, for a 200 m path over porous ground with a 4 m
barrier.
- exposure_uncertainty: ISO 9612 Annex D task contributions with the daily
LEX,8h and the one-sided 95 % upper limit LEX,8h + U.
- diagram_outdoor_geometry: source-barrier-receiver geometry (dss/dsr/z, the
blocked direct ray and the diffracted ray over the top edge).
Spanish translations added to the generate_graphs / generate_diagrams
dictionaries (decimal commas, UNE terminology). Every image visually audited.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* docs: ISO 9613-1/2 outdoor propagation guide and ISO 9612 exposure (EN)
Add the didactic Outdoor Sound Propagation guide (docs/outdoor-propagation.md)
covering ISO 9613-1 atmospheric absorption and the ISO 9613-2 general method
(divergence, atmospheric, ground and barrier terms, the OutdoorAttenuation
breakdown, the alternative 7.3.2 ground method, Cmet), with executed snippets
and honest scope notes (Amisc/reflections not implemented, 7.3.2 not
auto-wired, per-band Cmet a convenience). Extend the Levels guide with the
ISO 9612 occupational-exposure section (task/job/full-day strategies, the
C.6 I(I-1) vs C.12 N-1 asymmetry, Table C.4, k = 1,65, LEX,8h + U upper limit,
Lp,Cpeak uncertainty out of scope), reproducing the Annex D/E/F worked
examples.
Add the matching theory sections, API-reference rows for every new public
name, README feature list + docs table, docs index and CHANGELOG entries.
llms-full.txt / llms.txt regenerated (make llms) with the new page wired into
gen_llms.py. GitHub-safe math throughout; every snippet executed with pasted
outputs.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* docs: outdoor propagation and exposure site twins (EN + ES)
Add the English and Spanish Starlight twins of the new content:
- guides/outdoor-propagation.md (EN + ES) — code-identical snippets to the
docs page; Spanish prose uses UNE terminology ("ponderación temporal"),
decimal commas outside math and code, and the _es figure variants.
- The ISO 9612 section added to guides/levels.md (EN + ES).
- reference/theory.md and reference/api.md rows (EN + ES).
- Landing cards and standards list bumped 34 -> 37 (index.mdx EN + ES);
guides/outdoor-propagation added to the sidebar.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* fix: final branch review — API rows, precision phrasing and ES links
Adds the five missing public-symbol rows (TaskContribution,
table_c4_contribution, minimum_cumulative_duration_hours, COVERAGE_FACTOR,
INSTRUMENT_U2) to the three API references, states the ISO 9613-2 default
atmosphere (20 °C / 70 %, a Table 2 reference atmosphere) in the API rows
and guides, and softens "digit-for-digit" to "the standard's printed
precision" wherever it covered Annex E, whose final level differs only by
the standard's own pre-rounding. ES pages now use the /es/ link prefix
consistently. Code nits from the review: dead arg > 1 guard removed
(z > 0 implies arg >= 3), _with_advisory uses dataclasses.replace, and the
float-keyed _PRIME_BY_BAND gets a safety comment. llms files regenerated.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* fix: validate propagation/exposure inputs and docs lint (PR review)
Addresses the PR #89 bot review:
- Barrier.__post_init__ rejects negative edge distances and a zero
edge_separation (which previously divided by zero in the C3 double-
diffraction factor); Task.__post_init__ rejects empty samples and a
non-positive duration.
- ground_attenuation and barrier_attenuation reject non-positive
frequencies (a zero frequency gave an infinite wavelength / division by
zero); ground_attenuation also checks a positive distance and
non-negative heights. job_based_exposure rejects a non-positive
sample_duration_hours.
- Rewrite the ISO 9613-2 7.3.2 closed form in the guides and API tables so
Markdownlint no longer parses `(...)[...]` as a reversed link (MD011).
- Regression tests for every new guard; docstrings note the new raises.
Claude-Session: https://claude.ai/code/session_01HUHMx4dxDes2t5UPQ25djP
* feat: ISO 16283-3 facade sound insulation
Add facade_insulation() and FacadeInsulationResult for field facade sound
insulation per ISO 16283-3: the global-method level difference D2m and its
standardized (D2m,nT) and normalized (D2m,n) forms, plus the element-method
apparent sound reduction index R'45 (loudspeaker, -1,5 dB) / R'tr,s (road
traffic, -3 dB). Reuses the ISO 16283-1 energy-averaging helpers, the
Sabine absorption area, and the ISO 717-1 airborne weighted_rating engine
unchanged for the single-number rating. Adds a per-band .plot() profile and
exports.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 16283-3 draft-edition caveat and symmetric R' validation
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 10140 laboratory airborne and impact sound insulation
Add src/phonometry/lab_insulation.py, the laboratory counterpart of the
field ISO 16283 family:
- lab_airborne_insulation: R = L1 - L2 + 10 lg(S/A) with A = 0,16 V/T
(ISO 10140-2:2010 Formula (2); ISO 10140-4:2010 Formula (5)).
- lab_impact_insulation: Ln = Li + 10 lg(A/A0), A0 = 10 m²
(ISO 10140-3:2010 Formula (1)).
- background_correction: ISO 10140-4:2010 Clause 4.3 Formula (4) with the
6/15 dB criteria and the 1,3 dB limit-of-measurement cap.
Single-number Rw / Ln,w with C, Ctr, CI reuse the verified ISO 717-1/2
weighted_rating / weighted_impact_rating engines; position energy
averaging reuses _as_band_levels. 29 new closed-form / identity tests.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: reconcile facade citations with published ISO 16283-3:2016
The facade module was drafted from ISO/DIS 16283-3:2014; its docstrings
cited draft formula numbers with a caveat to reconcile against the
published edition. Reconciled against ISO 16283-3:2016(E) (First edition
2016-02-01):
- Clause numbers are edition-stable: the field-quantity definitions keep
3.12 (R'45deg), 3.13 (R'tr,s), 3.14 (D2m), 3.15 (D2m,nT), 3.16 (D2m,n),
3.17 (A=0,16V/T), and 9.5.1 (surface-level averaging).
- Formula numbers differ: in the 2016 edition the Clause 3 defining
formulas are UNNUMBERED (inline in the term definitions); the numbered
formulas (1)-(21) live in the procedural clauses. The surface-level
energy-average is Formula (7) in 2016 (was Formula (20) in the DIS).
- Dropped the invalid draft Formula (2)-(7) citations from the field-
quantity docstrings and the draft-edition reconciliation caveat.
- Constants and formula bodies are unchanged (-1,5 dB / -3 dB, A0=10 m2,
T0=0,5 s, A=0,16V/T, band ranges): no numeric change; 22 facade tests
green.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: EN 12354-1/2 building acoustic performance prediction with flanking
Implement the EN 12354:2000 simplified single-number prediction model for
apparent airborne (R'w) and impact (L'n,w) sound insulation, including direct
plus flanking transmission (Ff/Df/Fd paths) and the Annex E vibration-reduction
index Kij for rigid cross, rigid T, flexible-interlayer and lightweight-facade
junctions.
Airborne Formula (26)/(27)/(28a) with l0=1 m, Kij,min (29), lining composition
(30)/(31); impact Formula (21) with bare-floor Ln,w,eq (164-35 lg m'), Table 1
flanking correction K, and standardized L'nT,w (3). Results expose per-path
energy contributions so the dominant flanking path is visible.
Validated against the standards' worked examples: Part 1 Annex H.3 airborne
(R'w = 52 dB, 13 paths; +floating-floor 53 dB) and Part 2 Annex E.3 impact
(L'n,w = 45 dB, L'nT,w = 43 dB); all four Annex H junctions reproduce Kij.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 12999-1 building acoustics measurement uncertainty
Add building_uncertainty module implementing ISO 12999-1:2020 standard
uncertainties for sound-insulation quantities: per-band and single-number
values for airborne (Tables 2/3), impact (Tables 4/5) and floor-covering
reduction (Tables 6/7), across measurement situations A/B/C (Clause 5.2),
plus the Annex D sigma_R95 upper limits, Table 1 max repeatability and
Table 8 coverage factors.
Expansion U = k*u with k >= 1 (Clause 8), one-sided factors for conformity
checks (Formulae 4/5), and combination rules from Annexes A/B/C (prediction
input A.1, quadrature A.2/C.2, m-measurement reduction A.7, uncorrelated
single-number B.2). Composable UncertainValue attaches value +- U without
touching the rating dataclasses.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: numpy-stub-agnostic annotations for the two env-dependent mypy sites
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* refactor: PR-B review minors — anchors, Kij floor, uncertainty polish
- Table 3 r_w+ctr_50_5000 σsitu(B)=1.0 verified digit-by-digit against the
ISO 12999-1:2020(E) page image; anomalous but normative (comment added).
- Anchor the ISO 10140 rating tests to independent literals (Rw=54, Ln,w=58 —
reference-curve shape + the 32 dB / 16-band = 2 dB ISO 717 shift).
- background_correction: cross-ref sound_power.background_noise_correction and
document the negative-margin (Lb > Lsb) cap at Lsb−1.3.
- flanking_path: optional kij_min clamp for EN 12354-1 Clause 4.4.2 (Kij≥Kij,min),
documented on flanking_element; tested clamped vs unclamped (H.3 unaffected).
- impact_flanking_correction: comment on nearest-neighbour + tie-to-lower.
- Export COVERAGE_FACTORS (Table 8) as public API.
- single_number_uncertainty: note impact situation A is an estimate (Table 5 fn a).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: conformance checks for facade, lab insulation, EN 12354 and ISO 12999
Add six numerical conformance checks for the PR-B building-acoustics
standards, following the established registry pattern (single source of
truth in tests/reference_data.py, clause citations, honest tolerances):
- ISO 16283-3:2016 Clause 3.12: facade R'45 isolates the -1.5 dB
oblique-incidence correction on an S=A constructed case (exact).
- ISO 10140-2:2021 Formula (2): lab airborne R laid on the ISO 717-1
reference shape (S=A) -> Rw = 54 (the +2-shift analytic anchor).
- EN 12354-1:2000 Annex H.3: airborne prediction R'w = 52 from the 13
transmission paths (direct + 12 flanking), the branch's strongest oracle.
- EN 12354-2:2000 Annex E.3: impact prediction L'n,w = 45 (76-33+2).
- ISO 12999-1:2020 Table 2: airborne band uncertainty, situation A @
1 kHz = 1.8 dB (digit-exact), and Clause 8/Table 8 expanded
uncertainty U = 1.96 u = 2.352 dB (exact k=1.96 arithmetic).
Facade and lab checks join "Room & building acoustics"; the EN 12354 /
ISO 12999 checks form a new "Building prediction & uncertainty" domain.
Registry grows 26 -> 32 checks across 7 domains. Shared expected values
and the Annex H.3 input table move into reference_data.py so the report
and tests cannot drift; a consistency test pins them to the published
worked-example results. docs/CONFORMANCE.md regenerated via make conformance.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: building acoustics II — facade, laboratory, prediction and uncertainty (EN)
Extend the Room & Building Acoustics guide with facade insulation
(ISO 16283-3), laboratory characterisation (ISO 10140), flanking-transmission
performance prediction (EN 12354-1/2) and measurement uncertainty
(ISO 12999-1), with executed snippets reproducing the EN 12354-1 Annex H.3
(R'w = 52 dB) and Annex E.3 (L'n,w = 45 dB) worked examples. Add the matching
theory subsections, API-reference rows for every new public name, README and
landing-page entries (29 -> 33 standards), and CHANGELOG. EN docs and site
twins keep byte-identical code blocks.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: flanking-paths diagram, prediction and uncertainty figures
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: gemelas ES de edificación II (fachadas, laboratorio, predicción, incertidumbre)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: PR-B final-review fixes — count, GitHub-safe math, edition alignment
Landing count corrected to 34 standards (slash-part convention); the
GitHub-unsafe math spacing tokens reintroduced by the new building
sections are scrubbed from the docs/ tree and its site twins; the whole
ISO 10140-2 citation chain aligned to the verified :2010 edition; plus
the six minor documentation-precision fixes from the final review.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 88 review round 1 — isfinite idiom, heading levels, frequencies validation
- building_prediction._check_finite: replace 'v != v or v in (inf,-inf)'
NaN idiom with math.isfinite for SonarCloud (same behavior, cleaner).
- room-acoustics docs (EN docs + site, ES site): relevel the three
parameter-table headings that skipped H2->H4 to H3 (MD001).
- insulation.facade_insulation: validate 'frequencies' length against the
band count, raising a clear ValueError instead of deferring a matplotlib
shape error to plot(). lab_insulation/building_uncertainty have no such
gap (no user-supplied frequencies stored). Regenerated llms-full.txt.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ECMA-418-2 Sottek Hearing Model loudness and auditory front-end
Implement the ECMA-418-2:2025 (4th ed.) psychoacoustic loudness metric and
its shared auditory front-end (Sottek Hearing Model):
- Clause 5 front-end: outer/middle-ear cascade (5.1.3, Table 1), 53-band
gammatone-like auditory filter bank (5.1.4), band-dependent segmentation
(5.1.5, Table 4), rectification/RMS (5.1.6-5.1.7), compressive nonlinearity
(5.1.8, Formula 23, Table 2) and threshold in quiet (5.1.9, Table 3).
- Clause 6.2.2-6.2.7 ACF-based tonal/noise specific loudness (reusable by the
later tonality/roughness metrics).
- Clause 8 assembly: tonal/noise power average, average specific loudness,
time-dependent and single representative loudness value.
The front-end helpers are factored for reuse. Calibration verified: a 1 kHz
40 dB SPL sinusoid yields 0.996 sone_HMS (target 1.0). Adds a frozen
EcmaLoudness result with lazy .plot(), exports, and 12 tests.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ECMA-418-2 tonality (Sottek Hearing Model)
Implements the psychoacoustic tonality metric of ECMA-418-2:2025
(Clause 6.2.8-6.2.11) on top of the committed Sottek front-end and
ACF tonal/noise decomposition.
- tonality_ecma() -> EcmaTonality with the single value T (Formula 63),
average specific tonality T'(z) (Formula 53), tonal frequencies
f_ton,z(z) (Formula 55), time-dependent tonality T(l) (Formula 61)
and its frequency f_ton(l) (Formula 62); optional user band [f_low,f_high].
- Full Clause 6.2.3 band averaging with cross-block-size-group ACF
recomputation (loudness keeps its simplified, valid-for-loudness path;
boundary marker added there). Loudness 0.996 sone guard unchanged.
- _tonal_estimate now also returns the DFT-peak tonal frequency, with the
argmax restricted to the lower DFT half to avoid Nyquist-mirror aliasing.
- .plot() via _plotting.plot_ecma_tonality; exports in __init__.
- 13 conformance tests: 1 kHz/40 dB -> 1 tu_HMS calibration, pure tone
>> noise, f_ton tracks the tone, silence -> 0, band restriction.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ECMA-418-2 roughness (Sottek Hearing Model)
Implement the ECMA-418-2:2025 (4th ed.) psychoacoustic roughness metric
(Clause 7), the third Sottek Hearing Model output and new library
functionality. Reuses the Clause 5 auditory front-end from loudness_ecma
(ear filter, 53-band gammatone bank, specific basis loudness) and adds the
roughness-specific chain: start-only zero-padding and fixed 16384/4096
segmentation (5.1.2.2, 5.1.5.2, 7.1.1), the Hilbert envelope with x32
downsampling to 1500 Hz (7.1.2), the scaled envelope power spectrum
(7.1.3), two-step noise reduction (7.1.4), the four-stage spectral
weighting with quadratic-fit modulation-rate refinement, bias correction,
high/low modulation-rate weighting and fundamental-rate estimation
(7.1.5), interpolation to 50 Hz with the distribution-dependent nonlinear
transform, tabulated calibration c_R and asymmetric smoothing (7.1.7), and
the R'(z) / R(l50) / 90th-percentile aggregation (7.1.8-7.1.10).
Uses the standard's tabulated c_R = 0.0180685 (Formula 104), not
reverse-fit: the 1 kHz / 70 Hz / m=1 / 60 dB calibration signal yields
R = 1.0735 asper (near-but-not-exactly 1). OCR-ambiguous formulae
(66, 71, 78-81) cross-checked against the SQAT MATLAB reference; the
optional entropy weighting (7.1.6, needs RPM telemetry) is documented but
not implemented.
Adds roughness_ecma()/EcmaRoughness, a two-panel .plot(), exports, a
10-test TDD suite and plan/notes-ecma418-2-roughness.md. Full suite
752 passed / 12 skipped; loudness (0.996) and tonality (0.99993) oracles
unchanged (both modules untouched).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* test: pin ECMA-418-2 roughness calibration value and document clean-room variance
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 532-2 Moore-Glasberg loudness
Add the stationary Moore-Glasberg loudness model of ISO 532-2:2017 as a
third loudness method alongside the Zwicker (ISO 532-1) and Sottek
(ECMA-418-2) implementations. The full Clause 7 chain is implemented:
fixed outer/middle-ear transfer (Table 1), the level-dependent roex
auditory filter bank and excitation pattern on the ERB-number scale
(Formulae 1-6), the compressive specific-loudness transform (Formulae
7-9, Tables 2-4, C=0.0617 sone/Cam), binaural inhibition (Formulae
10-13) and integration to total loudness with the phon mapping of
Table 5.
Public API: loudness_moore_glasberg_from_spectrum (exact 5.2/5.4),
loudness_moore_glasberg_from_third_octave (29 bands, 5.5) and
loudness_moore_glasberg (signal wrapper), returning a frozen
MooreGlasbergLoudness with .plot() over the ERB-number scale.
Validated against the Annex B reference signals (tones, white/pink
noise, multi-tone complexes, tone-in-noise) to ~1-4%, well inside the
standard's 2.8 phon expanded uncertainty; the definitional anchor
(1 kHz/40 dB/free/binaural = 1.000 sone) holds end-to-end from the
tabulated constant. 50 new tests; full suite 802 passed / 12 skipped.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 532-3 Moore-Glasberg-Schlittenlacher time-varying loudness
Add loudness_moore_glasberg_time() implementing the ISO 532-3:2023 method:
a six-FFT multi-resolution running short-term spectrum (clause 7.3) feeding
the ISO 532-2 excitation and specific-loudness model on the 0.25 Cam grid
with the 532-3 constants (C=0.063, Tables 2/4, E_THRQ/E0=2.307), integrated
by asymmetric attack/release smoothing into short-term (clauses 7.6-7.8) and
long-term (clause 7.9) loudness traces, with peak long-term loudness,
percentiles and a lazy STL/LTL .plot().
Validated against the Annex C.1 tone table (1 kHz 10-80 dB, 3 kHz, 4 kHz,
100 Hz and monaural earphone) to within 0.4 phon, well inside the 2.8 phon
expanded uncertainty; the 1 kHz/40 dB anchor is exact at 1.000 sone. The
ISO 532-2 and ECMA-418-2 oracles are unchanged (831 passed / 12 skipped).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 532-3 per-ear LTL and Formula-14 kernel width (dichotic path)
Two transcription errors in the ISO 532-3:2023 dichotic (per-ear) binaural
path of the time-varying Moore-Glasberg-Schlittenlacher module:
1. Clause 7.9: the long-term loudness must be computed per ear by the
attack/release averager (Formulae 18-21) and then summed. The code ran a
single AGC on the binaural sum S'_L + S'_R; because the attack/release
branch is nonlinear, AGC(S'_L) + AGC(S'_R) != AGC(S'_L + S'_R) for dichotic
input. Keep two LTL states and sum the two long-term loudnesses. For
diotic/monaural the averager is linear in the (scaled) input so the result
is bit-for-bit identical to the old sum-then-AGC.
2. Formula (14)/(15): the smoothing weight is exp(-(B*Di)^2) with B = 0.08 and
Di in Cam. The code evaluated exp(-((0.08 * Di/0.1)^2)) = exp(-(0.8*Di)^2),
a Gaussian ~10x too narrow, and truncated the taps at +/-1.75 Cam. Remove
the spurious /0.1 and widen the kernel to the standard's +/-18 Cam (145 taps
on the 0.25-Cam grid). For diotic/monaural the L/R excitation ratio is 1
regardless of the kernel, so all oracles are unaffected.
No current validation signal exercises the dichotic path (Annex C signals are
diotic/monaural), so the 532-3 tone oracle and every other oracle remain
byte-identical; added focused dichotic tests plus a byte-identical anchor pin.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: numerical conformance report in CI PR comment
Reframe the PR comment into a persistent numerical conformance report:
each implemented standard -> normative expected value/range -> computed
result -> pass/fail with deviation, grouped by domain.
- scripts/conformance_report.py: registry of 21 checks across 17 standards
and 6 domains (filters/weightings, levels/dosimetry, psychoacoustics,
speech, intensity/power, room/building). Emits a headline summary, a
"Numerical validation - filters & weightings" section (per-architecture
IEC 61260-1 class margins + A/C/G weighting deviation vs the normative
curve) and one conformance table per domain. Deterministic, ~2.7 s.
- tests/reference_data.py: single source of truth for the shared normative
tables (IEC 61672-1 Table 3, ISO 7196 Table 2, ISO 717-1 Annex C R),
imported by both the tests and the report so they cannot drift.
- tests/test_conformance_report.py: smoke test asserting every registered
check passes and the Markdown is well-formed.
- comment_pr.py / workflow: conformance report leads the comment; the
test/coverage table is collapsed below. Retire benchmark_filters.py
(numerical content absorbed into the report).
make check green (740 passed, 12 skipped); ruff, mypy --strict, bandit clean.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* refactor: block-A review minors — docstring notes and tonality band-range
Four Minor findings from the plan-17 block-A psychoacoustics reviews. No
change to any validated numeric output; all five oracles stay byte-identical.
1. ISO 532-2 _source_levels: docstring note that clause 7.4 specifies a roex
weighting for X where the code uses a rectangular +/-ERB_n/2 window —
identical for isolated tones, immaterial for the Annex B broadband cases.
2. ISO 532-2 Formula-10 binaural smoothing kernel: the PDF (clause 8.1) states
"Di is changed in steps of 0,1" over -18..+18 Cam; the code confused the
0.1-Cam index offset with Di-in-Cam (the same /0.1 width bug just fixed in
532-3 Formula-14), making the Gaussian 10x too narrow and truncating to
+/-1.8 Cam. Widen to +/-18 Cam (361 taps) with Di = tap*_I_STEP. Provably
inert: all Annex B cases are diotic (ratio 1) or monaural (unit inhibition).
3. ECMA tonality _band_range: implement the exact Formulae 56-57 edge-midpoint
boundary conditions instead of a centre-frequency threshold; only affects
the optional user-band feature. Added a user-band edge test.
4. ECMA tonality reach: comment that the _CBF-1-band upper-edge term is a dead
defensive guard (N_B>0 only for low bands 0..24, so it never limits reach).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* chore: move conformance design doc out of repo root
* feat: clearer conformance report (value-vs-range) + block-A checks + audit fixes
Table 1 (filters): show, per architecture, the measured relative attenuation
at the binding band vs the class-1 limit it must clear (value + range, not
just a margin). Replace the "none" verdict (read as a failure) with a
"By design (ripple/soft rolloff)" label + footnote; Butterworth (default) and
Chebyshev-II are the only mask-compliant architectures. The measured value and
limit are re-derived with the public class_limits on the same designed SOS; a
smoke-test guard asserts the re-derived class-1 margin matches
verify_filter_class (single source of truth).
Table 2 (weightings): separate the informational max-deviation-from-nominal
(at a frequency extreme with wide, asymmetric tolerance) from the compliance
margin at the binding frequency, where the deviation, the +/- tolerance band
and the headroom are co-located so "value vs range" is unambiguous and in-spec.
Audit: verified all 21 checks (expected value/range vs the standard, real
library call, honest tolerance). One fix - ISO 226 anchored at 60 phon @ 2 kHz
-> 60 dB, a genuine Table B.1 value but coincidentally equal to the phon number
(reads as the trivial 1 kHz identity); re-anchored to the non-coincidental
60 phon @ 100 Hz -> 78.5 dB, sourced from reference_data. No library bug found.
Block-A psychoacoustics (+5, single-source expected in reference_data):
ECMA-418-2 loudness (1 kHz/40 dB -> 1 sone_HMS, c_N), tonality (-> 1 tu_HMS,
c_T), roughness (1 kHz/70 Hz/m=1/60 dB -> clean-room 1.0735 asper with a note
that the standard target is 1.0); ISO 532-2 (1 kHz/40 dB -> 1 sone, C=0.0617);
ISO 532-3 (steady 1 kHz/40 dB -> peak LTL 1 sone). 26/26 checks, 20 standards.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: versioned docs/CONFORMANCE.md with make target, CI staleness check and README link
Persist the numerical conformance report as a committed, versioned file so
every reader sees the library's per-standard validation (expected vs computed)
without running CI.
- docs/CONFORMANCE.md: committed report (26/26 checks, 6 domains, 20 standards)
with an auto-generated "do not hand-edit" header linking the standards docs.
- scripts/conformance_report.py: add a --file-header flag that emits that header
for the committed file (PR-comment body stays header-free); coarsen the
informational delta column to 3 decimals so BLAS/FFT-dependent sub-milli
residuals do not churn the file (status/computed columns keep regressions
visible).
- Makefile: `make conformance` regenerates the file; `make install-hooks`
installs the optional pre-commit hook.
- CI: new fast `conformance` job (runtime deps only) fails a PR if the committed
file is stale, telling the author to run `make conformance` and commit.
- hooks/pre-commit: opt-in convenience hook, scoped to src/scripts/reference
changes; CI is the enforcement.
- README + CONTRIBUTING: link and document the generated report.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: advanced psychoacoustics guide, theory, API (EN)
Document the plan-17 block-A advanced psychoacoustics models: Moore-Glasberg
loudness (ISO 532-2/532-3), Sottek Hearing Model loudness, tonality and
roughness (ECMA-418-2:2025).
- Psychoacoustics guide: new "Advanced loudness & sound-quality models"
section with a model-comparison table, executed snippets, parameter tables,
res.plot() one-liners and figure-code collapsibles (docs + site twin,
code blocks byte-identical).
- Theory: excitation-pattern/specific-loudness chain, Sottek front-end,
ACF tonality and envelope-modulation roughness (GitHub-safe math).
- API reference: rows for the 5 functions (+ 2 MG spectral entry points) and
5 result dataclasses with their .plot().
- README, EN landing (standards 26 -> 29: ISO 532-2, ISO 532-3, ECMA-418-2),
CHANGELOG Unreleased/Added, llms regen.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: advanced-psychoacoustics figures (loudness models, Sottek, tonality/roughness, time-varying)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: gemelas ES de psicoacústica avanzada (Moore-Glasberg, Sottek, tonalidad, aspereza)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 532-2 signal-input path builds a narrowband spectrum (pure-tone loudness)
The loudness_moore_glasberg(x, fs) wrapper routed the signal through the
one-third-octave method, which smeared a pure tone across a whole 1/3-octave
band and used a coherent-gain window normalisation that over-counted power by
1.76 dB. A calibrated 1 kHz tone at 40 dB SPL therefore returned 1.327 sone
instead of the definitional anchor of 1.000 sone.
ISO 532-2 is spectrum-based and its exact input (clauses 5.2/5.4) is a set of
discrete sinusoidal components; the excitation pattern (Formula 5) is computed
per component, so a tone must enter as a single line. The wrapper now forms the
signal's narrowband (FFT) line spectrum with a power-preserving (Parseval)
window normalisation and feeds it to loudness_moore_glasberg_from_spectrum. A
1 kHz/40 dB tone now yields 1.0001 sone / 40 phon and multi-tone signals match
the exact spectrum path. The validated spectrum and third-octave paths are
byte-identical.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: correct MG signal-path mechanism text and presentation default
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 87 review round 1 — float-equality guards, 532-3 FFT window length, tonality short-signal, report/figure fixes
- roughness_ecma: replace three float == 0.0 checks with abs>eps guards
(SonarCloud gate); roughness oracle byte-identical (1.0735).
- loudness_moore_glasberg_time: per-window FFT length (>= window length) so
the 64 ms window is not truncated at 44.1/48 kHz (was ~0.74 dB low in the
20-80 Hz band). 32 kHz anchor byte-identical (n_max 1.0000044713237626).
- tonality_ecma: short signals fall back to averaging all blocks (matches
loudness_ecma), not the final block only; validated anchor unaffected.
- conformance_report: document deliberate 273.0 (speed of sound) vs 273.15
(C1/C2) split matching the library; complete bind_side docstring.
- generate_graphs: roughness annotation uses computed peak fmod; Sottek
metrics use tu_HMS/sone_HMS units; ES translations + 8 figures regenerated.
- hooks/pre-commit: drop set -e so a benign regen failure does not block the
commit (CI staleness check is the gate).
- test_iec_weighting_table3 docstring, Makefile .PHONY completion.
All 5 psychoacoustics oracles byte-identical; make check 868 passed / 12
skipped; ruff + mypy + bandit clean; conformance 26/26.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: localize decimal commas in Sottek-unit figure labels (underscore guard)
The `_` guard in `_translate_figure` skipped decimal-comma localization for
any label containing an underscore. The new Sottek/HMS unit tokens
(`sone_HMS`, `tu_HMS`, `sonios_HMS`) carry underscores, so Spanish figures
regressed to dots (`8.0`, `0.42`) instead of commas (`8,0`, `0,42`).
Drop the underscore guard: the decimal-comma regex only rewrites a bare
`digit.digit` not adjacent to more digits/dots, so unit identifiers and
version numbers (5.3.3) stay intact while genuine decimals get commas.
Mathtext is still skipped via the `$` guard. Regenerated the affected ES
figures (sottek_specific_loudness, tonality_roughness_demo).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: responsive figure sizing — full-width on mobile, 92% plots on desktop (plan 16 T4)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: snippet clarity wave — recording/excitation convention (plan 16 T1)
Apply the recording/excitation naming convention from the P16 audit across
the calibration, intensity, block-processing, time-weighting, levels,
weighting and room-acoustics guides (docs/ + EN guides + ES guides).
- calibration: ref_signal->calibrator_recording, signal->recording (J1 cascade)
- intensity: probe-recording synth + swap-in disclosure on p1/p2
- block-processing / time-weighting: audio_blocks provenance comments
- levels / time-weighting / weighting: signal->recording, provenance upgraded to convention
- room-acoustics: play/record framing on sweep/recorded; synthetic-IR stand-in note
EN docs/guides kept code-identical; ES mirrored with UNE wording.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: one-line canonical plots on result objects (plan 16 T2)
Add a thin plot(ax=None, **kwargs) method to each result dataclass that
renders the essence of its guide's canonical figure in one line, following
the sklearn/statsmodels/librosa pattern. matplotlib stays a soft dependency:
importing phonometry and computing works without it; .plot() raises a clear
ImportError with install guidance. Rendering lives in a new lazy _plotting
module; dataclass methods are thin delegates.
Covers ZwickerLoudness, STIResult, WeightedRatingResult/ImpactRatingResult,
RoomAcousticsResult, SoundPowerResult/ReverberationSoundPowerResult/
SoundPowerIntensityResult, IntensityResult and a new frozen DecayCurve
(returned by decay_curve, tuple-unpackable for backward compat). Rating
results gain band_centers/measured/shifted_reference fields for the figure.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: plot kwargs forwarding, deterministic invalid-band test, octave-impact annotation
- _plot_rating now accepts **kwargs and forwards to the measured curve, so
result.plot(linewidth=2) no longer raises TypeError; parametrized test
covers every public .plot() forwarding a benign kwarg to its primary artist.
- Replace skip-if-all-valid room-acoustics invalid-band test with a directly
constructed RoomAcousticsResult (500 Hz band flagged invalid); assert exactly
the flagged bars are hatched and greyed.
- plot_impact_rating keeps the shifted-reference curve normatively honest
(ref - shift) and marks/annotates the 500 Hz read value with the ISO 717-2
Clause 4.3.2 -5 dB octave rule instead of distorting the curve.
- Move _bar_width next to its only consumer, plot_intensity.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: .plot() one-liners and figure-code collapsibles (plan 16 T3)
Add one-line res.plot() mentions to every guide section whose result type
gained .plot() in T2, and figure-code collapsibles (both the one-liner and
the manual matplotlib path) under the five .plot()-backed data figures.
Document .plot() availability and the DecayCurve return type in the API
reference and CHANGELOG. EN docs + EN/ES site; llms-full.txt regenerated.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: collapsibles for STI-vs-T60 and open-plan figures
Controller follow-up to plan 16 T3: the two figures are reproducible with
public API only. STI-vs-T60 sweeps sti_from_impulse_response over synthetic
exponential IRs at a 10-point T60 grid; the open-plan collapsible rebuilds
the D2,S regression and rD/rP crossings from the OpenPlanResult fields.
EN docs + EN/ES site; executed on Agg; llms-full.txt regenerated.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: final-review polish — result-field docs, comment hygiene, naming
Document band_centers/measured/shifted_reference on WeightedRatingResult
and ImpactRatingResult (CHANGELOG + EN/ES API tables), strip internal plan
tags from theme-images.css and test_result_plots.py, note that [plot] also
powers result .plot() methods, tighten the plot_zwicker_loudness docstring
to say the two-panel layout only occurs when ax is None, sentence-case the
intensity provenance comments, and rename the shadowed impact rating var to
res_imp on the room-acoustics pages.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 86 review round 1 — log-axis bars, style kwargs overrides, snippet physics and notation
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 3744/3746 sound power from pressure measurements
Add sound_power.py: sound power level of a noise source from sound
pressure levels over an enveloping measurement surface, ISO 3744:2010
(engineering, grade 2) and ISO 3746:2010 (survey, grade 3).
- sound_power_pressure(): per-band LW and A-weighted LWA from an
(NM, NB) level array over a hemisphere (radius) or parallelepiped
(dimensions + distance); surface area from the standard's closed forms
for 1/2/3 reflecting planes; energy average (Eq. 12), background K1
(Eq. 16), environmental K2 (Eq. A.2), apparent directivity index
(Eq. 7), expanded uncertainty.
- measurement_positions(): normative Annex B coordinates
(Tables B.1/B.2/B.3), grade- and plane-dependent subsets.
- background_noise_correction(): K1 with engineering/survey criteria.
- environmental_correction(): K2 from A, Sabine T+V, or alpha*Sv.
- SoundPowerResult (frozen dataclass), SoundPowerWarning.
Tests: 24 cases incl. monopole-over-plane exact LW recovery, radius
independence, K1/K2 closed forms, LWA via Annex E, and validations.
make check green (543 passed, 12 skipped); ruff/bandit/mypy --strict clean.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: directivity index under background correction, citation hygiene
Broadband K1 for the apparent directivity index was collapsing to the
surface-area term 10*lg(S/S0) (~+22 dB inflation with background
correction). Compute the true energy-based broadband K1 (per-band Eq. 16
aggregated over bands) so DI is correct in both paths.
Also: multi-band sound_power_level_a without frequencies now returns NaN
(A-weighting needs band centres); dropped PDF page-number citations from
Annex B/E table comments; corrected the survey parallelepiped position
citation to ISO 3746 clause C.1 / Figure C.7.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 3741 reverberation-room sound power
Add the ISO 3741:2010 precision (grade 1) methods for determining sound
power in a reverberation test room, in a new sound_power_reverberation
module:
- sound_power_reverberation() — direct method (Eq. 20): LW from the mean
room SPL and the Sabine equivalent absorption area A = (55,26/c)(V/T60),
with the full normative bracket: 10 lg(A/A0), 4,34*(A/S), the Waterhouse
boundary term 10 lg(1 + S c/(8 V f)), the reference-quantity (C1) and
radiation-impedance (C2) meteorological corrections and the -6 dB
constant. Speed of sound c = 20,05*sqrt(273 + theta).
- sound_power_comparison() — comparison method (Eq. 21) with a reference
sound source: LW = LW(RSS) + (Lp(ST) - Lp(RSS) + C2).
- ReverberationSoundPowerResult frozen dataclass; frequency-dependent
background correction K1 (Eq. 14, same formula as ISO 3744) with the
precision-grade 6/10 dB criteria; A-weighted total via Annex F.
Reuses the ISO 3744 energy-average and A-weighting helpers. Tests cover
exact inversion of Eq. (20), the Waterhouse term vanishing at high
frequency, the comparison method exact by construction, and synergy with
room_parameters T30 feeding the direct method.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 3741 room-qualification advisories and validation ordering
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 9614-2 sound power by intensity scanning
Add sound_power_intensity() computing per-band LW = 10lg(sum Pi/P0) from
per-segment signed normal intensity and areas (partial power Pi = <In,i>*Si),
the scanning-method field indicators FpI (Eq. A.1) and F+/- (Eq. A.2), the
repeatability criterion 3, and the achieved engineering/survey grade per band
(Annex B criteria 1-3). Negative total power flags bands as non-determinable
(clause 9.2); negative partial power raises SoundPowerWarning. Reuses
dynamic_capability_index for Ld = dpI0 - K (Table 1).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 16283-2 impact sound insulation and ISO 717-2 ratings
Add field impact sound insulation (ISO 16283-2, tapping machine) and the
ISO 717-2 single-number weighted impact rating with the CI spectrum
adaptation term, reusing the verified reference-curve shift engine.
- impact_insulation(li, t2, *, volume=None, t0=0.5): per-band
L'nT = Li - 10 lg(T/T0) (Formula (1)) and L'n = Li + 10 lg(A/A0) with
A = 0,16 V/T, A0 = 10 m2 (Formula (2)); positions energy-averaged.
- weighted_impact_rating(values_by_band, bands=None): Ln,w / L'n,w /
L'nT,w (Clause 4.3, octave -5 dB rule) and CI (Clause A.2.1, energetic
sum 100-2500 / 125-2000). Impact unfavourable deviations (measurement
exceeds reference, sign opposite to airborne) reduce to the airborne
search on negated curves, so _best_shift is reused verbatim.
- _best_shift: tolerance absorbs float noise at the exact bound (sums are
true multiples of 0,1 dB); airborne behaviour unchanged.
Verified against ISO 717-2 Annex C worked examples (Table C.1 Ln,w=79,
CI=-11; Table C.3 octave Ln,w=54, CI=0) and an independent brute-force
shift search on 10 000 random curves per band set. 2013 and 2020 editions
are identical for all implemented maths (2020 only adds Annex D rubber
ball, out of scope).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 354 sound absorption in a reverberation room
Add phonometry.sound_absorption implementing BS EN ISO 354:2003:
- absorption_area(t60, volume, ...): equivalent sound absorption area
A = 55,3*V/(c*T) - 4*V*m (Eq. 5/7), speed of sound from Eq. (6).
- absorption_coefficient(t1, t2, volume, sample_area, ...): alpha_s =
(A2-A1)/S built from Eq. (8)/(9), per-measurement c1/c2, unclamped
(alpha_s > 1 valid, clause 3.7 NOTE 2); warns when T2 >= T1.
- attenuation_from_alpha: ISO-354 conversion m = alpha/(10 lg e) from an
ISO 9613-1 attenuation coefficient (m deferred to ISO 9613-1, default 0).
Composes directly with room_parameters() T20/T30 outputs. 25 tests
(exact T->A->T inversion, synthetic-alpha recovery, zero-m air term,
two-temperature Eq. (8), range validation, room_parameters synergy).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: ISO 354 room and sample-size advisories
Add advisory AbsorptionWarning (never raising; result still returned) for
setup conditions outside ISO 354:2003 limits:
- Room volume below the 150 m3 minimum of clause 6.1.1, in both
absorption_area and absorption_coefficient.
- Sample area outside the clause 6.2.1.1 range 10 <= S <= 12 m2, with the
upper limit scaled by (V/200)^(2/3) when V > 200 m3, in
absorption_coefficient.
The volume advisory is emitted once by absorption_coefficient and suppressed
in its internal absorption_area calls to avoid duplicate warnings.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: deferred review minors — airborne brute-force net, advisory consistency, labels
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: sound power guide, impact and absorption sections, theory and API (EN)
Add a "Sound Power" guide covering the three LW routes (ISO 3744/3746
enveloping surface, ISO 3741 reverberation room with Waterhouse/C1/C2,
ISO 9614-2 intensity scanning), with a method-comparison table, executed
snippets and parameter tables. Extend the Room & Building Acoustics guide
with impact sound insulation (ISO 16283-2 + ISO 717-2, L'nT/L'n, tapping
machine, CI) and sound absorption in a reverberation room (ISO 354).
Add the matching theory sections (K1/K2, Waterhouse, C1/C2 derivations;
impact sign conventions; Sabine absorption) and API-reference rows for all
new public names. Update the landing standards count to 26, the sidebar,
README highlights/table, CHANGELOG and llms-full.txt.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: sound-power and impact figures and diagrams
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: include the sound-power guide in llms coverage
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: reposition Ln,w annotation in the impact figure
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: gemelas ES de potencia sonora, impacto y absorción
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: final-review findings — comparison K1 reporting, citations, knee unification, docs attribution
- sound_power_comparison now reports the test-source K1 actually applied
per band (zero when no background), matching the dataclass docstring;
LW unchanged.
- Replace gitignored notes-iso3744-3746.md citation in the DI comment with
the standard reference (ISO 3744:2010, 3.24 / Eq. 7 context).
- Unify the K1 zeroing knee to the >= form (ΔLp >= 15 -> no correction).
- Broaden SoundPowerWarning docstring to name all emitters (ISO 3744/3746,
ISO 3741, ISO 9614-2).
- Fix docs misattributing K2 to ISO 3741 (ISO 3744 K2 vs ISO 3741 absorption
term) across EN/ES room-acoustics guides.
- EN impact-setup diagram uses dot decimals (0.16); regenerate the 4 variants.
- CHANGELOG: label the ISO 717-2 Annex C value Ln,w = 79.
- Warn-after-validate in sound_power_comparison; "is raised" -> "is emitted".
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 84 review round 1 — per-band DI and K2, sweep-reversal repeatability, validation and broadcast ergonomics
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 84 round 2 — partial room-data validation, result-field docs
Raise a clear ValueError in environmental_correction (and its
sound_power_pressure pass-through) when exactly one member of a
room-data pair (reverberation_time/room_volume or
mean_absorption_coefficient/room_surface) is supplied, instead of
silently returning K2=0 as if free field. All-None remains the
legitimate free-field K2=0 path.
Document frequencies on the ReverberationSoundPowerResult row and
negative_band as a standalone per-band boolean field on the
SoundPowerIntensityResult row across docs/api-reference.md and the
EN/ES site twins.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: audit wave A — inter-sample LCpeak, cheby2 class-1 default, 144 kHz weighting target
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: audit wave A — decay validity thresholds, variant sharpness anchors, N5 phase, STIPA warning, Farina guard
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: audit wave B — self-contained snippets, tables, sharpness/open-plan figures, orphans
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: audit wave C — remaining minors and optimizations across the library
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: document the audit-wave parameters across guides and API tables
Cover the public parameters added in the audit fix waves across all three
documentation trees (docs/, site EN, site ES):
- lc_peak(..., oversample=8): inter-sample peak recovery (levels guide + API)
- calculate_sensitivity(..., narrowband=False): coherent tone estimator
(calibration guide + API)
- sound_intensity(..., bias_correct=False): finite-difference bias correction
(intensity guide + API)
- room_parameters/decay_curve(..., zero_phase=False): 125 Hz short-T bias
(room-acoustics guide + API)
- OctaveFilterBank/octavefilter attenuation default 60 -> 72 and cheby2
class-1 note (filter-banks guide + API)
- STIPA < 15 s UserWarning (psychoacoustics guide + API)
Also refresh dependent behaviour notes: ln_levels attack skip 2*tau -> 5*tau,
T20/T30 validity thresholds 35/45 -> 46/54 dB, and the zero-phase broadband
~0.2-0.3 dB band-level caveat. Regenerated llms-full.txt.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: truthfulness fixes from the final audit-branch review
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: 2000 Hz, not 2 ms, in the N5/N10 docstring
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 82 review round 1 — bias-correction cutoff, parabolic-peak guard, complexity, snippet self-containment
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
The LICENSE file carried the GPLv3 text from the 2020 initial commit,
while every published declaration — the PyPI classifier in
pyproject.toml, CITATION.cff, .zenodo.json and the README badge and
license section — has stated MIT throughout. The project is and always
was published as MIT; this replaces the stale GPLv3 text with the MIT
license so the repository is coherent.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 18233 sweep/MLS impulse-response acquisition
Add src/phonometry/room_ir.py implementing the ISO 18233:2006
deterministic-excitation IR front end: exponential sine sweep with exact
analytic phase (Annex B), linear spectral-division deconvolution with a
Farina inverse-filter option for harmonic separation (B.5), and
maximum-length-sequence generation (LFSR, orders 2-20) with circular
cross-correlation recovery (Annex A).
Validated against closed forms: known IIR bandpass recovered within 0.1 dB
in band (sweep and MLS), ideal chain to a band-limited delta, +3 dB SNR per
sweep-duration doubling (B.6), and all MLS orders verified as true
maximum-length sequences (autocorrelation L / -1).
433 passed, 12 skipped; ruff, mypy --strict and bandit clean.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 3382 room acoustic parameters from impulse responses
Add src/phonometry/room_acoustics.py implementing ISO 3382-1:2009 and
ISO 3382-2:2008 analysis of measured impulse responses:
- decay_curve(): Schroeder backward integration of the squared IR
(5.3.3, Eq. 1) with background-noise truncation at the noise/slope
crossing and exponential tail compensation (Eq. 3), broadband or per
IEC 61260 fractional-octave band.
- room_parameters(): per-band EDT (0/-10 dB, A.2.2), T20 (-5/-25 dB)
and T30 (-5/-35 dB) by least-squares fits (ISO 3382-2 Annex C),
clarity C50/C80 (Eq. A.10), definition D50 (Eq. A.11) and centre
time Ts (Eq. A.13), octave bands 125 Hz-4 kHz by default with a
one-third-octave option and a broadband mode.
- Validity flags per the 5.3.3 dynamic-range criterion (noise at least
evaluation range + 15 dB below the IR maximum: 25/35/45 dB) plus the
Annex B curvature indicator C = 100*(T30/T20 - 1).
Tests validate against closed forms for exponential decays (EDT = T20 =
T30 = T within 1 %; C_te = 10*lg(exp(13.8155*te/T) - 1); D50 =
1 - exp(-0.6908/T); Ts = T/13.8155), the exact C50/D50 relation
(Eq. A.12), double-slope decays (EDT < T20 < T30) and noise-driven
validity-flag behaviour, with tolerances far below the Table A.1 JNDs.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* chore: move figure/diagram generators to scripts/
generate_graphs.py and generate_diagrams.py join benchmark_filters.py
and gen_llms.py in scripts/, where dev tooling already lives. Updated
the Makefile graphs target, the sys.path bootstrap in the graph guard
tests and in generate_graphs.py itself (src/ is now one level up), the
CONTRIBUTING instructions and the theme-images.css pointer. Verified:
module loads from the new location and regenerating the SVG diagrams
produces byte-identical output.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 3382-3 open-plan office spatial metrics
Add open_plan_metrics() computing the ISO 3382-3:2012 single-number
quantities from a line of workstation measurements: spatial decay rate
D2,S and nominal speech level Lp,A,S,4m (Clause 6.2, Eq.5; 2-16 m
positions on a log-distance regression) and distraction/privacy
distances rD/rP (Clause 6.3; STI-vs-distance linear regression crossing
0,50 and 0,20). Returns a frozen OpenPlanResult; validates the minimum
of four positions (5.2.2) and equal array lengths.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 16283-1 field insulation and ISO 717-1 weighted ratings
Add phonometry.insulation implementing field airborne sound insulation
(ISO 16283-1:2014) and single-number weighted ratings with C/Ctr
(ISO 717-1):
- airborne_insulation: per-band level difference D (Formula (1)),
standardized level difference DnT = D + 10 lg(T/T0) (Formula (2)) and
apparent sound reduction index R' = D + 10 lg(S/A), A = 0,16 V/T
(Formula (4)/(5)); energy-averages microphone positions (Formula (9)).
- weighted_rating: reference-curve shifting method (Clause 4.4, Table 3)
with the 32,0/10,0 dB unfavourable-deviation bound, plus C/Ctr from the
Table 4 spectra (Clause 4.5). Reference values, spectra and method are
identical in the 2013 and 2020 editions.
- energy_average_level: ISO 16283-1 Formula (9) helper.
Verified against the ISO 717-1 Annex C worked example (Rw(C;Ctr) =
30(-2;-3), unfavourable sum 31,8 dB) and exact-bound / tipping edge
cases. 20 new tests; make check green (484 passed, 12 skipped).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: room & building acoustics guide, theory, API and landing (EN)
New "Room & building acoustics" guide (docs/ + Starlight twin) covering the
full measurement chain: ISO 18233 swept-sine/MLS impulse-response acquisition,
ISO 3382-1/2 decay analysis and room parameters (EDT/T20/T30/C50/C80/D50/Ts),
ISO 3382-3 open-plan speech metrics, and ISO 16283-1 / ISO 717-1 field
insulation with weighted ratings. Adds the matching theory section (Schroeder
integration, regression windows, C/D/Ts, D2,S, DnT/R', reference-curve method),
API-reference rows for all new public names, sidebar entry, README highlight +
docs row, EN landing card, CHANGELOG entries, docs index row, and regenerated
llms-full.txt. All snippets validated; site build and make check green.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: Schroeder, ISO 717-1 rating and insulation-setup figures
Add three room/building-acoustics documentation figure sets (each in
en/es x light/dark):
- schroeder_decay: synthetic IR, Schroeder backward integration with
EDT/T20/T30 regressions and evaluation ranges (ISO 3382); annotated
values match room_parameters.
- insulation_rating: ISO 717-1 Annex C example with shifted reference
curve, unfavourable-deviation shading and Rw read at 500 Hz; Rw, C,
Ctr and the unfavourable sum come from weighted_rating.
- diagram_insulation_setup: ISO 16283-1 airborne setup plan view with
the normative minimum distances (clauses 7.6 and 7.2.2).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: gemelas ES de acústica de salas y edificación
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: terminología UNE — ponderación temporal en lugar de balística (ES)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: markdown/README parity with the site + llms guide coverage
Scrub GitHub-unsafe display math (\, thin-spaces and escaped \{ \}) from
docs/intensity.md and docs/psychoacoustics.md — the only two guides that
missed the GitHub-safe math conversion of PRs #78/#79 — matching the safe
forms already used in docs/theory.md. Align calibration's sensitivity
symbol/formula to the site ($S$, single-line \qquad).
Extend scripts/gen_llms.py PAGES with the three omitted guides
(psychoacoustics, intensity, room-acoustics) and regenerate llms.txt /
llms-full.txt so AI-facing artifacts cover all 14 docs pages.
The guides/references were otherwise already in parity (didactic code
comments, theory sections, tables, and figures from PRs #77/#80 all
present); docs/README index and README highlights/table verified complete.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: final-review findings — open_plan validation, limits rename, Farina caveat, MLS averaging test
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: ISO 18233 measurement-chain diagram and guide fixes
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: fix sweep_signal cross-reference in Farina caveat
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 81 review round 1 — inverse-filter guards, truncation threshold, IR input validation, docs pipeline wording
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: blank lines around CONTRIBUTING fence (MD031)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf