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: 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
fix: deterministic documentation-figure rendering (end the flaky figures job) (#131)
* fix: make documentation-figure rendering deterministic across machines
The "Documentation figures up to date" CI job was intermittently failing on the
heavy compute figures (excitation_signals, multiple_shock, sii_vocal_efforts,
sottek_specific_loudness, tonality_roughness_demo): their multi-threaded
numerical backends reorder floating-point reductions depending on the available
core count, so the CI runner produced byte-different SVG/PNG output than the
committed (single-threaded) baseline, and a plain re-run usually "fixed" it.
Pin the numerical thread pools to a single thread so rendering is reproducible
regardless of the host core count:
- generate_graphs.py / generate_diagrams.py set OMP/MKL/OPENBLAS/NUMEXPR/NUMBA/
VECLIB thread counts to 1 before the numeric backends import (covers direct
invocation).
- The Makefile `graphs` target also exports those plus PYTHONHASHSEED=0 before
the interpreter starts (PYTHONHASHSEED cannot be set from within the process);
CI runs `make graphs`, so it inherits this with no workflow change.
Regenerating every figure under these settings produces a zero-byte diff against
the committed images, confirming the current baseline is already the
single-threaded canonical output — so no figure files change here.
* fix: compare documentation figures within tolerance, not byte-exact
The "Documentation figures up to date" job regenerated .github/images with
`make graphs` and required a byte-identical result. That is unachievable on
GitHub's hardware-heterogeneous runner fleet: the pinned software stack
computes a few plotted path coordinates ~1 ULP apart depending on which CPU
microarchitecture (SIMD kernel) the run lands on, so the job failed
intermittently even though the figures were numerically and visually
identical. Single-thread pinning removes intra-machine reduction races but
cannot remove this cross-CPU last-bit difference.
Replace the byte diff with scripts/check_figures.py, which compares the
freshly regenerated figures against the committed versions within a tolerance:
* SVG -- non-numeric structure (elements, text, colours, ordering) must match
exactly; every numeric token must agree within an absolute/relative
tolerance. A moved element or relabelled axis fails; a last-bit
coordinate wobble passes.
* PNG -- identical dimensions, at most a small number of pixels changed beyond
a level threshold (catches localised changes a global RMS dilutes),
and a bounded RMS (catches broad changes the pixel count misses).
* other -- exact byte compare. Added/removed files always fail.
The thread-pinning determinism work from the previous commit is retained (it
still removes genuine intra-machine flakiness); this makes the check robust to
the cross-machine floating-point non-determinism it cannot eliminate.
* fix: harden figure check against hash-id drift and orphan figures
Two robustness gaps found reviewing the tolerance-aware figure check:
* SVG clip-path / marker ids are SHA256 hashes of the underlying float
geometry (matplotlib backend_svg._make_id). A cross-CPU 1-ULP wobble in that
geometry avalanches the whole hash, so the id text would change and trip the
strict structural gate -- the very drift the check exists to tolerate. Now
canonicalise every id and its url(#...)/href="#..." references to
first-appearance placeholders before comparing, so equivalent documents match
regardless of hash text while a genuine id add/remove/reorder/repoint fails.
* The generators only overwrite files, never delete, so a figure that is no
longer produced would survive on disk byte-identical to HEAD and pass the
"committed figure no longer generated" check. Clear the generated SVG/PNG at
the start of the `graphs` target before regenerating (animations *.gif/*.webm
are from the separate `animations` target and are preserved). A full
clear+regenerate reproduces the committed tree exactly, confirming no
committed figure is orphaned.
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 pass 6: split the three overloaded guides (#115)
* docs: split the three overloaded guides
Audit batch 6 - reorganization with full content preservation (verified
line-by-line against HEAD across the three surfaces):
- room-acoustics (1096 lines) keeps ISO 18233 + ISO 3382-1/2/3 + ISO 354
measurement; the building half (ISO 16283 + ISO 717, ISO 10140,
EN 12354-1/2, ISO 12999-1) moves to a new docs/building-acoustics.md
built from the site's existing split, resolving the repo/site
asymmetry. The flanking figure is reunited with its <details> code and
flanking_path is introduced before it is referenced (audit fixes), on
all three surfaces.
- psychoacoustics extracts STI/STIPA to speech-transmission.md
(paralleling speech-intelligibility.md) with STI-vs-SII disambiguation
boxes on the three pages involved; ISO 226 moves into psychoacoustics
where its perception content belongs, fixing its stale 'upcoming
feature' sentence.
- levels extracts occupational-exposure.md (ISO 9612) and
tone-prominence.md (ECMA-418-1), keeping Leq/LN/peak/SEL/Lden and the
spectrogram.
Indexes, sidebar, theory/guide cross-links (~40 edits in 19 files) and
the llms.txt generator PAGES list follow the moves; the four extracted
topics now survive into llms-full.txt instead of dropping out.
* docs: Spanish grammar fixes in the split guides
- 'función de conveniencia' instead of the false friend 'comodidad'
for convenience function (Gemini)
- plural agreement 'estrategias basadas' at the two flagged sites
(Gemini); the remaining 'basada' occurrences are correct singular
agreements ('medición basada en tareas', 'la estrategia basada...')
* docs: address second-round review on the splits
- flanking_path() gains its own parameter table (with the kij_min
Formula E.4 floor contract) in the building guide, all three surfaces
(CodeRabbit)
- the flanking figure snippet explicitly annotates its reuse of paths/
res from the snippet above; the speech-transmission figure snippet is
now fully self-contained (imports + fs) on all surfaces (CodeRabbit)
- ISO 10140-3 joins the laboratory standards footer (CodeRabbit)
- the Speech Intelligibility guide joins llms.txt (regenerated) and the
root README table (CodeRabbit)