Run the full ISO 532-1 Annex B loudness validation from in-repo signals (#90)
* 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: wind-turbine noise — apparent sound power & tonal audibility (IEC 61400-11) (#146)
* docs: implementation plan for wind-turbine noise (IEC 61400-11) PR-B2
* feat(wind-turbine): apparent sound power + tonal audibility chain (IEC 61400-11)
Add wind_turbine_noise.py: slant_distance/apparent_sound_power_level (Formula
26) and wind_turbine_tonality (Formulae 30-34) returning a
WindTurbineTonalityResult with .plot(). Named to avoid colliding with the
existing ISO 1996-2 critical_bandwidth/tonal_adjustment/tonal_audibility; the
K_T rating reuses the ISO 1996-2 tonal_adjustment already in the library.
* feat(wind-turbine): conformance domain (IEC 61400-11), 168/168
* feat(wind-turbine): tonal-audibility figure (x4 variants)
* docs(wind-turbine): EN guide + site EN/ES, api-reference, indices, sidebar
* fix(wind-turbine): correct low-frequency band and non-adjacent tone lines
Two correctness fixes from review, against IEC 61400-11:2012+A1:2018:
- The 20-70 Hz candidate uses the fixed absolute 20-120 Hz critical band, not
a band centred on the tone frequency (subclause 9.5.3). The previous
|f - fc| <= CBW/2 window only coincided with 20-120 Hz at fc = 70.
- Tone lines need not be contiguous with the peak. A1:2018 struck "adjacent";
every line in the critical band above the tone threshold and within 10 dB of
the peak is a tone. The Hanning /1.5 correction is now applied per contiguous
run rather than once for the whole set, so separated tones are summed
correctly.
Add tests for a low-frequency tone (excluding sub-20 Hz lines) and for two
separated tone lines.
* fix(wind-turbine): review pass — validation, shared band helper, plot color
- wind_turbine_tonality: validate strictly-increasing and uniformly-spaced
frequencies (the narrowband-spectrum precondition of Formulae 30-34)
- extract shared _critical_band_edges() so the plots use the fixed 20-120 Hz
low-frequency band instead of fc±CBW/2 (matches the classification logic)
- correct the WindTurbineTonalityResult docstring return type
- distinct masking-level line colour in the generated figure (no longer
blends with the grid)
- regenerate the 4 wind_turbine_tonality figure variants
- add regression tests for the frequency-axis validation
* fix(wind-turbine): address CodeRabbit review
- tighten the uniform-spacing tolerance to 1e-3·df so a near-miss axis (e.g.
2,2,2.4 Hz) that would bias df/ENBW is rejected; add a near-miss test
- fix a duplicate-keyword TypeError in plot_epnl / plot_wind_turbine_tonality
when a caller passes lw/label/color; merge defaults so caller values win;
add a plot-kwargs regression test
- remove the unused _AUDIBILITY_REPORT_LIMIT constant (the multi-spectrum
'No relevant tones' reporting rule is out of scope); is_audible stays ΔL_a>0
per IEC 61400-11 §9.5.8 ('a tone is audible if the tonal audibility is above
0 dB')
- add unit=dB to the apparent-power conformance row for table consistency
- document band_levels as background-corrected and the narrowband-spectrum
constraints in api-reference; make the guide code examples self-contained
- markdownlint-clean the wind-turbine plan doc
feat: underwater acoustics — reference levels, ship radiated noise & pile driving (ISO 18405/17208/18406) (#144)
New underwater-acoustics domain (PR-A of #19), clean-room from the standard texts:
- underwater_acoustics — ISO 18405:2017 reference levels re 1 µPa: SPL, SEL (re 1 µPa²·s), zero-to-peak, and 1 µPa <-> 20 µPa re-referencing.
- ship_radiated_noise — ISO 17208-1/-2: radiated noise level (re 1 µPa·m), equivalent monopole source level via the Lloyd's-mirror correction (d_s = 0.7·D), three-hydrophone geometry, tabulated uncertainty.
- pile_driving_noise — ISO 18406:2017: single-strike SEL, cumulative SEL (energy sum and N-identical), and per-strike metrics (SEL, peak SPL, Leq, 90 %-energy pulse duration) with .plot().
Conformance domain with 6 checks (161/161). Analytic oracles independent of the implementation, including an externally hand-computed Lloyd's-mirror transcription guard. Figures in EN/ES × light/dark; EN + site EN/ES guides, API reference and README.
Aeroacoustics (ICAO EPNL, IEC 61265/61400-11) is a separate follow-up PR (PR-B of #19).
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 4b: deprecation-cycle renames of published API (#112)
* 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)
Outdoor sound propagation and occupational noise exposure (ISO 9613-1, ISO 9613-2, ISO 9612) (#89)
* 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
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 4b: deprecation-cycle renames of published API (#112)
* 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)
Audit pass 4b: deprecation-cycle renames of published API (#112)
* 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)
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 4b: deprecation-cycle renames of published API (#112)
* 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)
feat: IEC 61260:1995 / ANSI S1.11-2004 class 0 filter verification (#126)
* 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).
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 4c: final identifier sweep against the naming convention (#113)
* 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).
feat: electroacoustics — distortion (IEC 60268-3 / AES17) and frequency response (Bendat & Piersol) (#143)
* docs: design spec for electroacoustics distortion and frequency response
* feat: electroacoustic distortion and frequency response (IEC 60268-3 / AES17 / Bendat & Piersol)
Add two modules for audio-equipment characterisation from a captured signal.
distortion.py (IEC 60268-3:2013 + AES17-2015):
- thd (THD_F/THD_R), harmonic_distortion (nth-order)
- thd_plus_noise, sinad (AES17 standard notch, transient-trimmed residual)
- weighted_thd (A/C weighting of the residual)
- modulation_distortion (SMPTE), difference_frequency_distortion (CCIF),
total_difference_frequency_distortion, dynamic_intermodulation_distortion (DIM,
Table 2 nine products of the 15 kHz / 3.15 kHz signal)
- harmonic_analysis -> HarmonicDistortionResult with .plot()
frequency_response.py (Bendat & Piersol, Random Data 4e):
- transfer_function (H1 = Gxy/Gxx, H2 = Gyy/Gyx)
- coherence (ordinary gamma^2)
- FrequencyResponseResult with a Bode + coherence .plot()
Every quantity is verified against an exact analytic oracle (synthetic signals
with known harmonic/intermodulation amplitudes; a known LTI path with
gamma^2 = 1 noiseless and SNR/(1+SNR) with output noise). Adds a new conformance
domain (7 checks), tests, four-variant SVG figures, docs (EN + site EN/ES),
api-reference, indices and the astro sidebar.
* review: harden electroacoustics per AI-reviewer feedback
- distortion: reject a fundamental at/above Nyquist before the notch (clear
error instead of a SciPy crash); validate notch_q in weighted_thd for parity
with thd_plus_noise; narrow the harmonic peak-search window to ±0.1·f0 so a
nearby non-harmonic tone is not latched onto a harmonic bin.
- frequency_response: clamp noverlap to nperseg-1 (high overlap no longer
raises); use gyx != 0 instead of abs(gyx) > 0 for the H2 zero-guard.
- tests: assert the full nine-product DIM Table 2 list; add high-overlap,
above-Nyquist and weighted_thd notch_q validation cases.
- conformance: extract a shared _electro_tone helper (de-duplication).
- api-reference: document the window parameter of thd_plus_noise/harmonic_analysis.
* review: avoid float-equality zero-guard flagged by SonarCloud
Use |gyx|^2 > 0 for the H2 denominator guard instead of gyx != 0 — keeps the
cheaper (no sqrt) form gemini asked for while satisfying SonarCloud's
no-float-equality reliability rule.
Audit pass 9: the tests batch (#119)
* 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.
Advanced metrology: G-weighting, ISO 226 contours, ECMA-418-1 tonality, ISO 1996 Lden, IEC 60942:2017 (#76)
* feat: update calibrator validation to IEC 60942:2017 (Ed. 4)
The 2017 edition changes the short-term level fluctuation method (5.3.3):
|max - mean| and |min - mean| against the mean F-time-weighted level, and
tightens the class 1 limit in 160-1250 Hz from 0.10 dB (2003 Table 1) to
0.07 dB (2017 Table 2), with relaxed limits at low frequencies where the
F time-weighting itself ripples. calculate_sensitivity() gains a
frequency= parameter selecting the Table 2 row; max_fluctuation_db=None
now resolves the class 1 limit automatically.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: G frequency weighting for infrasound (ISO 7196:1995)
WeightingFilter/weighting_filter accept curve='G': four zeros at the
origin and the four complex pole pairs of ISO 7196 Table 1 (p. 2),
normalized to 0 dB at 10 Hz per clause 4, bilinear-mapped (no
oversampling needed: the curve acts on 0.25-315 Hz where the plain
design is already exact). CI verifies the response against every
Table 2 nominal value at the exact base-10 third-octave frequencies
(+-0.3 dB) plus the 12 and 24 dB/octave slopes. sel()/ln_levels()
accept 'G' transparently. New docs section EN/ES with an autogenerated
figure (light+dark) overlaying the Table 2 nominals.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 226:2023 normal equal-loudness-level contours
equal_loudness_contour(phon) implements Formula (1) at the 29 preferred
third-octave frequencies of Table 1 (transcribed from the official PDF,
p. 4); loudness_level(spl, frequency) is the exact inverse (Formula 2);
hearing_threshold() exposes the T_f column. Validity enforced per clause
4.1 (20-90 phon, 80 above 4 kHz) and no interpolation between tabulated
frequencies (the standard defines none). Verified in CI against spot
values of the informative Annex B tables B.1/B.2 (+-0.05 dB/phon) plus
the 1 kHz identity and full round-trip. Docs EN/ES with the classic
contour-family figure (light+dark).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: prominent discrete tones - TNR and PR per ECMA-418-1:2024
tone_to_noise_ratio() implements clause 11 (Hann/RMS-averaged FFT, tone
band above the connecting line per Formula 9, masking noise rescaled to
the critical bandwidth per Formula 10, proximate-tone combination per
clause 11.6 Formulae 14-16) and prominence_ratio() implements clause 12
(middle band vs contiguous bands with the fitted Table 2/3 edges of
Formulae 21-22, the 20 Hz-truncated 100 Hz-rescaled lower band of
Formula 24 below 171.4 Hz). Both return a ToneAssessment with the
frequency-dependent prominence criteria (Formulae 12-13 / 25-26).
Critical bands per clause 10 Formulae 2-8. Verified against every
worked example printed in the standard (dfc, band edges, proximity
spacing) and against analytic tones-in-white-noise. Note: the inline
condition printed next to Formula (23) contradicts the clause 12.5
prose (typo in the standard); the prose governs.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: environmental noise descriptors per ISO 1996-1:2016
lden() (3.6.4: +5 dB evening / +10 dB night, default 12/4/8 h periods,
adjustable per country), ldn() (3.6.5, defaults 15/9 h) and
composite_rating_level() (6.5 Formulae 5-6: arbitrary periods with
source/character adjustments, e.g. Table A.1). All reduce to the same
composite form and are verified against hand-computed formula values,
the constant-level analytic offset and period-sum validation.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: changelog for the advanced metrology feature set
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: address advanced-metrology review feedback
- calibration: IEC 60942:2017 Table 2 row boundaries corrected (160 Hz
belongs to the 0.07 dB row, 63 Hz to the 0.20 dB row) with a boundary
regression test
- tonality: tone-band edge walk is safe when the peak sits at a spectrum
boundary; _band_power raises a clear ValueError when the resolution
leaves a band empty (instead of dividing by zero); _fitted_edge clips
marginally out-of-range peaks to the table span
- environmental: composite_rating_level materializes its input (safe for
generators) and rejects an empty period list
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: address second review round
- changelog/docstring wording: the two deviations are each compared
against the limit (not a ratio); 0.07 dB applies at and above 160 Hz
- environmental: periods accepts any Iterable (matches the generator
support and test)
- loudness_contours: document that the 1e-6 relative tolerance only
absorbs float representation error and cannot cross table rows
- tonality: validate resolution_hz > 0 and tone_freq > 0 up front
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: create diagram output dir, hook generate_diagrams into make graphs
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: fourth review round - validation and boundary wording
- composite_rating_level rejects non-finite period durations
- _averaged_spectrum raises a clear error for absurdly coarse
resolution_hz (fewer than 16 samples per segment)
- calibration docs/docstrings state the exact Table 2 boundaries
(0.20 dB at or below 63 Hz; note: the 160 Hz code boundary was
already correct from the previous round)
- weighting page frontmatter mentions G (ISO 7196)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: scope the class 1 HF-accuracy claim to A/C/Z, credit ISO 7196 for G
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
feat: environmental-noise determination (ISO 1996-2 tonal adjustment + uncertainty) (#133)
* 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.
Audit pass 4b: deprecation-cycle renames of published API (#112)
* 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)
Building acoustics II: facade insulation (ISO 16283-3), laboratory measurement (ISO 10140), prediction with flanking (EN 12354) and uncertainty (ISO 12999-1) (#88)
* 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: façade insulation & outdoor radiation prediction (EN 12354-3/-4:2000) (#125)
* 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.
Audit pass 4b: deprecation-cycle renames of published API (#112)
* 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)
feat: laboratory flanking transmission (ISO 10848 vibration reduction index) (#132)
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.
feat: floor-covering impact improvement on a mock-up (ISO 16251-1:2014) (#130)
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
feat: psychoacoustic annoyance and fluctuation strength (Fastl & Zwicker) (#142)
* docs: design spec for psychoacoustic annoyance and fluctuation strength
* feat: psychoacoustic annoyance and fluctuation strength (Fastl & Zwicker)
Close the modern sound-quality set with two new modules.
Psychoacoustic annoyance (Fastl & Zwicker Eqs 16.2-16.4; origin Widmann 1992):
`psychoacoustic_annoyance(n5, sharpness, fluctuation_strength, roughness)`
computes PA = N5·sqrt(1 + wS² + wFR²) exactly, and
`psychoacoustic_annoyance_from_signal` derives the four sensations from a
calibrated signal (documented model-mixing caveat; F is free-field).
Fluctuation strength (Fastl & Zwicker Ch. 10; no ISO standard):
`fluctuation_strength_am_noise` is the exact closed form for AM broadband
noise (Eq. 10.2); `fluctuation_strength` is the Osses et al. (2016) signal
model, implemented clean-room and cross-checked against the paper's Table 1
literature values and the open SQAT reference (used only as a numeric oracle).
Calibrated so the 1 kHz / 60 dB / m=1 / 4 Hz AM tone reads 1.00 vacil by
construction; the AM-tone 70 dB modulation sweep tracks the literature at
Pearson r ≈ 0.98 with the 4 Hz band-pass peak. The Osses model is free-field
only (no diffuse variant), so the function takes no `field` argument.
- 27 new tests (exact PA/closed-form + signal cross-check); full suite 2266 pass
- conformance +3 (PA exact, Eq. 10.2 exact, calibration): 148/148, no drift
- EN/ES guides, api-reference, README, sidebar; two figures (4 variants each)
- `.plot()` on both result dataclasses
* fix: address PR review findings (PA/fluctuation strength)
- fluctuation_strength: guard _cross_covariance against catastrophic
cancellation — a (near-)constant band envelope could make dx·dy slightly
negative, and sqrt() then returned NaN (which slipped past the `denom <= 0`
test and poisoned the sum). Clamp the product to 0 (gemini/coderabbit).
- fluctuation_strength: precompute the Bark-inversion grid at module load
instead of rebuilding a 20k-point grid on every _bark_center_hz call (copilot).
- _plotting: let callers override color/edgecolor in plot_psychoacoustic_annoyance
via **kwargs instead of raising TypeError on a duplicate keyword (coderabbit).
- generate_graphs: the closed-form Eq. 10.2 maximum is at ∛50 ≈ 3.68 Hz, so
relabel the 4 Hz guide line as a reference (the marker already sits on the
true peak); update the ES string. Regenerate the 4 figure variants (coderabbit).
- docs: add the missing combined legend to the fluctuation-strength figure
snippet in the EN/ES guides (coderabbit).
- reference_data: sync the wFR worked-example comment with the constant (copilot).
feat: electroacoustics — distortion (IEC 60268-3 / AES17) and frequency response (Bendat & Piersol) (#143)
* docs: design spec for electroacoustics distortion and frequency response
* feat: electroacoustic distortion and frequency response (IEC 60268-3 / AES17 / Bendat & Piersol)
Add two modules for audio-equipment characterisation from a captured signal.
distortion.py (IEC 60268-3:2013 + AES17-2015):
- thd (THD_F/THD_R), harmonic_distortion (nth-order)
- thd_plus_noise, sinad (AES17 standard notch, transient-trimmed residual)
- weighted_thd (A/C weighting of the residual)
- modulation_distortion (SMPTE), difference_frequency_distortion (CCIF),
total_difference_frequency_distortion, dynamic_intermodulation_distortion (DIM,
Table 2 nine products of the 15 kHz / 3.15 kHz signal)
- harmonic_analysis -> HarmonicDistortionResult with .plot()
frequency_response.py (Bendat & Piersol, Random Data 4e):
- transfer_function (H1 = Gxy/Gxx, H2 = Gyy/Gyx)
- coherence (ordinary gamma^2)
- FrequencyResponseResult with a Bode + coherence .plot()
Every quantity is verified against an exact analytic oracle (synthetic signals
with known harmonic/intermodulation amplitudes; a known LTI path with
gamma^2 = 1 noiseless and SNR/(1+SNR) with output noise). Adds a new conformance
domain (7 checks), tests, four-variant SVG figures, docs (EN + site EN/ES),
api-reference, indices and the astro sidebar.
* review: harden electroacoustics per AI-reviewer feedback
- distortion: reject a fundamental at/above Nyquist before the notch (clear
error instead of a SciPy crash); validate notch_q in weighted_thd for parity
with thd_plus_noise; narrow the harmonic peak-search window to ±0.1·f0 so a
nearby non-harmonic tone is not latched onto a harmonic bin.
- frequency_response: clamp noverlap to nperseg-1 (high overlap no longer
raises); use gyx != 0 instead of abs(gyx) > 0 for the H2 zero-guard.
- tests: assert the full nine-product DIM Table 2 list; add high-overlap,
above-Nyquist and weighted_thd notch_q validation cases.
- conformance: extract a shared _electro_tone helper (de-duplication).
- api-reference: document the window parameter of thd_plus_noise/harmonic_analysis.
* review: avoid float-equality zero-guard flagged by SonarCloud
Use |gyx|^2 > 0 for the H2 denominator guard instead of gyx != 0 — keeps the
cheaper (no sqrt) form gemini asked for while satisfying SonarCloud's
no-float-equality reliability rule.
Advanced psychoacoustics (ISO 532-2/3 Moore-Glasberg, ECMA-418-2 Sottek loudness/tonality/roughness) + numerical conformance report (#87)
* 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
Room and building acoustics: ISO 18233, ISO 3382-1/2/3, ISO 16283-1 + ISO 717-1 (#81)
* 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
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
Advanced psychoacoustics (ISO 532-2/3 Moore-Glasberg, ECMA-418-2 Sottek loudness/tonality/roughness) + numerical conformance report (#87)
* 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
Audit pass 9: the tests batch (#119)
* 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.
Acoustic materials: impedance tube, airflow resistance & sound-absorption rating (ISO 10534-1/-2, ASTM E2611, ISO 9053-1/-2, ISO 11654) (#91)
* 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
Audit pass 4c: final identifier sweep against the naming convention (#113)
* 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).
feat: installed structure-borne sound from equipment (EN 12354-5) (#140)
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.
Audit pass 9: the tests batch (#119)
* 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.
Audit-driven accuracy, robustness and didactic improvements across the library (#82)
* 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
feat: sound insulation by intensity (ISO 15186-1:2000) (#127)
* 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.
Building acoustics II: facade insulation (ISO 16283-3), laboratory measurement (ISO 10140), prediction with flanking (EN 12354) and uncertainty (ISO 12999-1) (#88)
* 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
Audit-driven accuracy, robustness and didactic improvements across the library (#82)
* 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
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 4b: deprecation-cycle renames of published API (#112)
* 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)
feat: mechanical mobility and the FRF family (ISO 7626-1:2011) (#136)
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.
Audit pass 4b: deprecation-cycle renames of published API (#112)
* 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)
Audit pass 4a: concept-based names for the four unpublished modules (#111)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 4b: deprecation-cycle renames of published API (#112)
* 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)
Audit pass 4c: final identifier sweep against the naming convention (#113)
* 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).
Room and building acoustics: ISO 18233, ISO 3382-1/2/3, ISO 16283-1 + ISO 717-1 (#81)
* 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
Outdoor sound propagation and occupational noise exposure (ISO 9613-1, ISO 9613-2, ISO 9612) (#89)
* 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
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 4c: final identifier sweep against the naming convention (#113)
* 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).
Audit pass 9: the tests batch (#119)
* 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.
feat: underwater acoustics — reference levels, ship radiated noise & pile driving (ISO 18405/17208/18406) (#144)
New underwater-acoustics domain (PR-A of #19), clean-room from the standard texts:
- underwater_acoustics — ISO 18405:2017 reference levels re 1 µPa: SPL, SEL (re 1 µPa²·s), zero-to-peak, and 1 µPa <-> 20 µPa re-referencing.
- ship_radiated_noise — ISO 17208-1/-2: radiated noise level (re 1 µPa·m), equivalent monopole source level via the Lloyd's-mirror correction (d_s = 0.7·D), three-hydrophone geometry, tabulated uncertainty.
- pile_driving_noise — ISO 18406:2017: single-strike SEL, cumulative SEL (energy sum and N-identical), and per-strike metrics (SEL, peak SPL, Leq, 90 %-energy pulse duration) with .plot().
Conformance domain with 6 checks (161/161). Analytic oracles independent of the implementation, including an externally hand-computed Lloyd's-mirror transcription guard. Figures in EN/ES × light/dark; EN + site EN/ES guides, API reference and README.
Aeroacoustics (ICAO EPNL, IEC 61265/61400-11) is a separate follow-up PR (PR-B of #19).
feat: psychoacoustic annoyance and fluctuation strength (Fastl & Zwicker) (#142)
* docs: design spec for psychoacoustic annoyance and fluctuation strength
* feat: psychoacoustic annoyance and fluctuation strength (Fastl & Zwicker)
Close the modern sound-quality set with two new modules.
Psychoacoustic annoyance (Fastl & Zwicker Eqs 16.2-16.4; origin Widmann 1992):
`psychoacoustic_annoyance(n5, sharpness, fluctuation_strength, roughness)`
computes PA = N5·sqrt(1 + wS² + wFR²) exactly, and
`psychoacoustic_annoyance_from_signal` derives the four sensations from a
calibrated signal (documented model-mixing caveat; F is free-field).
Fluctuation strength (Fastl & Zwicker Ch. 10; no ISO standard):
`fluctuation_strength_am_noise` is the exact closed form for AM broadband
noise (Eq. 10.2); `fluctuation_strength` is the Osses et al. (2016) signal
model, implemented clean-room and cross-checked against the paper's Table 1
literature values and the open SQAT reference (used only as a numeric oracle).
Calibrated so the 1 kHz / 60 dB / m=1 / 4 Hz AM tone reads 1.00 vacil by
construction; the AM-tone 70 dB modulation sweep tracks the literature at
Pearson r ≈ 0.98 with the 4 Hz band-pass peak. The Osses model is free-field
only (no diffuse variant), so the function takes no `field` argument.
- 27 new tests (exact PA/closed-form + signal cross-check); full suite 2266 pass
- conformance +3 (PA exact, Eq. 10.2 exact, calibration): 148/148, no drift
- EN/ES guides, api-reference, README, sidebar; two figures (4 variants each)
- `.plot()` on both result dataclasses
* fix: address PR review findings (PA/fluctuation strength)
- fluctuation_strength: guard _cross_covariance against catastrophic
cancellation — a (near-)constant band envelope could make dx·dy slightly
negative, and sqrt() then returned NaN (which slipped past the `denom <= 0`
test and poisoned the sum). Clamp the product to 0 (gemini/coderabbit).
- fluctuation_strength: precompute the Bark-inversion grid at module load
instead of rebuilding a 20k-point grid on every _bark_center_hz call (copilot).
- _plotting: let callers override color/edgecolor in plot_psychoacoustic_annoyance
via **kwargs instead of raising TypeError on a duplicate keyword (coderabbit).
- generate_graphs: the closed-form Eq. 10.2 maximum is at ∛50 ≈ 3.68 Hz, so
relabel the 4 Hz guide line as a reference (the marker already sits on the
true peak); update the ES string. Regenerate the 4 figure variants (coderabbit).
- docs: add the missing combined legend to the fluctuation-strength figure
snippet in the EN/ES guides (coderabbit).
- reference_data: sync the wFR worked-example comment with the constant (copilot).
Audit pass 10: complete the .plot() convention and tidy the plotting layer (#120)
* 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)
feat: reverberation-time prediction (Sabine, Eyring, Millington-Sette, Fitzroy, Arau-Puchades) (#134)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
Audit-driven accuracy, robustness and didactic improvements across the library (#82)
* 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
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
Audit-driven accuracy, robustness and didactic improvements across the library (#82)
* 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
feat: underwater acoustics — reference levels, ship radiated noise & pile driving (ISO 18405/17208/18406) (#144)
New underwater-acoustics domain (PR-A of #19), clean-room from the standard texts:
- underwater_acoustics — ISO 18405:2017 reference levels re 1 µPa: SPL, SEL (re 1 µPa²·s), zero-to-peak, and 1 µPa <-> 20 µPa re-referencing.
- ship_radiated_noise — ISO 17208-1/-2: radiated noise level (re 1 µPa·m), equivalent monopole source level via the Lloyd's-mirror correction (d_s = 0.7·D), three-hydrophone geometry, tabulated uncertainty.
- pile_driving_noise — ISO 18406:2017: single-strike SEL, cumulative SEL (energy sum and N-identical), and per-strike metrics (SEL, peak SPL, Leq, 90 %-energy pulse duration) with .plot().
Conformance domain with 6 checks (161/161). Analytic oracles independent of the implementation, including an externally hand-computed Lloyd's-mirror transcription guard. Figures in EN/ES × light/dark; EN + site EN/ES guides, API reference and README.
Aeroacoustics (ICAO EPNL, IEC 61265/61400-11) is a separate follow-up PR (PR-B of #19).
Audit pass 4b: deprecation-cycle renames of published API (#112)
* 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)
Audit pass 9: the tests batch (#119)
* 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.
Sound power determination and building acoustics II: ISO 3744/3746, ISO 3741, ISO 9614-2, ISO 16283-2 + ISO 717-2, ISO 354 (#84)
* 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
Audit pass 4b: deprecation-cycle renames of published API (#112)
* 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)
Sound power determination and building acoustics II: ISO 3744/3746, ISO 3741, ISO 9614-2, ISO 16283-2 + ISO 717-2, ISO 354 (#84)
* 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
Audit pass 9: the tests batch (#119)
* 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.
Sound power determination and building acoustics II: ISO 3744/3746, ISO 3741, ISO 9614-2, ISO 16283-2 + ISO 717-2, ISO 354 (#84)
* 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
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
feat: field survey method (ISO 10052:2021) (#128)
* 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
Audit pass 9: the tests batch (#119)
* 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.
Audit pass 9: the tests batch (#119)
* 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.
feat: objective audibility of tones in noise (ISO/PAS 20065:2016) (#141)
* feat: objective audibility of tones in noise (ISO/PAS 20065:2016)
Engineering-method tonal audibility: the critical band about a tone
(Δfc, Formula 2) and its geometric corner frequencies (Formulae 3-5),
the critical-band masking level LG = LS + 10 lg(Δfc/Δf) (Formula 12),
the masking index av = -2 - lg[1 + (f/502)^2.5] (Formula 13), the
per-tone audibility ΔL = LT - LG - av (Formula 14), and the decisive
and energy-mean mean audibility over spectra (Formula 20/21).
Narrow-band front-end (Clauses 5.3.2/5.3.3/5.3.8, Annex D): from a
spectrum, mean_narrowband_level determines LS iteratively (energy
average with the -1.76 dB Hanning correction, dropping any line more
than 6 dB above the running LS until stable or fewer than five lines
remain each side), tone_level sums the tonal lines about a peak,
analyze_spectrum detects the distinct audible tones (peak detection +
distinctness criteria, Clause 5.3.4), and combined_tone_level performs
the multi-tone "FG" combination (Formula 17). The algorithm is confirmed
against the parent standard DIN 45681:2005-03 and its Annex J reference
program.
Conformance is anchored on the Annex E combustion-engine worked example
(Tables E.1/E.2/E.3): LS = 49.22 dB and LT = 67.96 dB from the spectral
lines, tone detection recovering {118.4, 137.3, 158.8} Hz, the FG
combined level LT = 72.15 dB, the per-tone audibility at 137.3 Hz, the
masking index at 137.3/592.2 Hz and the mean audibility of the five
spectra (144/144). A decisive audibility reproduced exactly needs the
complete spectrum (Table E.1 is truncated to one critical band).
- src/phonometry/tone_audibility.py: the full audibility chain, the
LS/LT spectral front-end, whole-spectrum detection + FG combination,
and ToneAudibilityResult
- _plotting.py: plot_tone_audibility (per-tone ΔL bars, decisive marked)
- generate_graphs.py: figure (4 variants) with ES translations
- conformance: 7 ISO/PAS 20065 checks (new domain)
- docs + site guides (EN/ES) + api-reference + sidebar
- tests: 50 tests, full suite 2225 passed
* feat: separate evaluation of two tones below 1000 Hz (ISO/PAS 20065 §5.3.8)
Add the two-tone resolution branch of Clause 5.3.8 (Formulae 18/19): when
exactly two tones share a critical band and both lie below 1000 Hz, the ear
can still resolve them, so they are rated separately instead of FG-combined
when their frequency difference exceeds
fD = 21·10^(1.2·|lg(fT/212)|^1.8) Hz
evaluated at the more prominent tone (the larger audibility ΔL).
New public helpers `two_tone_separation_frequency` (Formula 19) and
`resolve_tones_separately` (the Formula 18 + threshold decision). No ISO/PAS
20065 worked example exercises this branch (the Annex E band groups three
tones, so the "exactly two tones" rule never fires); the formula and decision
are implemented clean-room from the text and verified against the parent
standard DIN 45681:2005-03 Annex J reference program
(fD = 21 * 10 ^ (1.2 * Abs(Log(fT / 212) / Log(10)) ^ 1.8)). As a consistency
check, at the Annex E tones the threshold keeps them combined, matching that
example's FG grouping.
- 9 new logic/consistency tests; conformance +1 (now 145/145)
- EN/ES guides, API reference and module docstring updated
feat: dynamic transfer stiffness of resilient elements (ISO 10846) (#137)
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.
Audit pass 9: the tests batch (#119)
* 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.
feat: underwater acoustics — reference levels, ship radiated noise & pile driving (ISO 18405/17208/18406) (#144)
New underwater-acoustics domain (PR-A of #19), clean-room from the standard texts:
- underwater_acoustics — ISO 18405:2017 reference levels re 1 µPa: SPL, SEL (re 1 µPa²·s), zero-to-peak, and 1 µPa <-> 20 µPa re-referencing.
- ship_radiated_noise — ISO 17208-1/-2: radiated noise level (re 1 µPa·m), equivalent monopole source level via the Lloyd's-mirror correction (d_s = 0.7·D), three-hydrophone geometry, tabulated uncertainty.
- pile_driving_noise — ISO 18406:2017: single-strike SEL, cumulative SEL (energy sum and N-identical), and per-strike metrics (SEL, peak SPL, Leq, 90 %-energy pulse duration) with .plot().
Conformance domain with 6 checks (161/161). Analytic oracles independent of the implementation, including an externally hand-computed Lloyd's-mirror transcription guard. Figures in EN/ES × light/dark; EN + site EN/ES guides, API reference and README.
Aeroacoustics (ICAO EPNL, IEC 61265/61400-11) is a separate follow-up PR (PR-B of #19).
feat: sound power from surface vibration (ISO/TS 7849-1/-2) (#138)
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.
feat: wind-turbine noise — apparent sound power & tonal audibility (IEC 61400-11) (#146)
* docs: implementation plan for wind-turbine noise (IEC 61400-11) PR-B2
* feat(wind-turbine): apparent sound power + tonal audibility chain (IEC 61400-11)
Add wind_turbine_noise.py: slant_distance/apparent_sound_power_level (Formula
26) and wind_turbine_tonality (Formulae 30-34) returning a
WindTurbineTonalityResult with .plot(). Named to avoid colliding with the
existing ISO 1996-2 critical_bandwidth/tonal_adjustment/tonal_audibility; the
K_T rating reuses the ISO 1996-2 tonal_adjustment already in the library.
* feat(wind-turbine): conformance domain (IEC 61400-11), 168/168
* feat(wind-turbine): tonal-audibility figure (x4 variants)
* docs(wind-turbine): EN guide + site EN/ES, api-reference, indices, sidebar
* fix(wind-turbine): correct low-frequency band and non-adjacent tone lines
Two correctness fixes from review, against IEC 61400-11:2012+A1:2018:
- The 20-70 Hz candidate uses the fixed absolute 20-120 Hz critical band, not
a band centred on the tone frequency (subclause 9.5.3). The previous
|f - fc| <= CBW/2 window only coincided with 20-120 Hz at fc = 70.
- Tone lines need not be contiguous with the peak. A1:2018 struck "adjacent";
every line in the critical band above the tone threshold and within 10 dB of
the peak is a tone. The Hanning /1.5 correction is now applied per contiguous
run rather than once for the whole set, so separated tones are summed
correctly.
Add tests for a low-frequency tone (excluding sub-20 Hz lines) and for two
separated tone lines.
* fix(wind-turbine): review pass — validation, shared band helper, plot color
- wind_turbine_tonality: validate strictly-increasing and uniformly-spaced
frequencies (the narrowband-spectrum precondition of Formulae 30-34)
- extract shared _critical_band_edges() so the plots use the fixed 20-120 Hz
low-frequency band instead of fc±CBW/2 (matches the classification logic)
- correct the WindTurbineTonalityResult docstring return type
- distinct masking-level line colour in the generated figure (no longer
blends with the grid)
- regenerate the 4 wind_turbine_tonality figure variants
- add regression tests for the frequency-axis validation
* fix(wind-turbine): address CodeRabbit review
- tighten the uniform-spacing tolerance to 1e-3·df so a near-miss axis (e.g.
2,2,2.4 Hz) that would bias df/ENBW is rejected; add a near-miss test
- fix a duplicate-keyword TypeError in plot_epnl / plot_wind_turbine_tonality
when a caller passes lw/label/color; merge defaults so caller values win;
add a plot-kwargs regression test
- remove the unused _AUDIBILITY_REPORT_LIMIT constant (the multi-spectrum
'No relevant tones' reporting rule is out of scope); is_audible stays ΔL_a>0
per IEC 61400-11 §9.5.8 ('a tone is audible if the tonal audibility is above
0 dB')
- add unit=dB to the apparent-power conformance row for table consistency
- document band_levels as background-corrected and the narrowband-spectrum
constraints in api-reference; make the guide code examples self-contained
- markdownlint-clean the wind-turbine plan doc
Audit-driven accuracy, robustness and didactic improvements across the library (#82)
* 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