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 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
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 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
Audit pass 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
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 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 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
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)
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)
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)
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)
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 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.
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.
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 1: hard bugs in docs, figures and plotting (#108)
* fix: first pass of audit findings (docs, figures, plotting)
Documentation:
- fix the Spanish filter-figure regression: _showfilter gains a close
flag and the generator saves through themed_path after drawing, so the
translation pass runs on the finished figure; restore the two
_ES_EXACT keys removed in #82 (they are emitted by filter_design, not
by the script)
- repair the broken building-acoustics.md link in the enclosed-space
guide (the repo split lives in room-acoustics.md)
- make the multiple-shock and intensity snippets self-contained: define
az/fs with a synthetic shock train (verified outputs 20.94 m/s2,
R=0.46, probability 0.03) and import numpy with stated outputs
(F2=3.41, Ld=8.0), in the repo doc and both site languages
- decimal points in the English Formula texts (1.07, 55.3) that had
kept the Spanish comma
Figures:
- stop drawing data series in the grid colour (invisible on light
backgrounds): time-weighting input burst, hearing-threshold 20 yr and
NIPTS 10 yr curves, and the unweighted/partial bars now use a visible
neutral grey; regenerate the 15 affected figure sets
Plotting layer:
- plot_facade_insulation forwards user kwargs to the primary D2m,nT
curve only, so label=/color= no longer raise or merge the four curves;
the kwargs-forwarding test now covers the facade plot
- harden three edge cases: all-masked SII (zero contribution no longer
divides 0/0), empty per-impulse prominence, and nearest-band lookup
for the NC governing marker instead of float equality
- sharpness normalization sanity guards raise RuntimeError instead of
AssertionError
* refactor: address review feedback on the NC marker and facade test
- plot_noise_criterion: pick the nearest valid (non-NaN) band and place
the governing marker on that band's frequency so x and y stay paired;
skip the marker if every level is NaN (Copilot)
- add an explicit regression test for plot_facade_insulation(label=...),
which used to raise TypeError (Copilot)
feat: 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).
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: 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 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 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
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)
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.
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.
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
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: 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.
Audit pass 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
Audit pass 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
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)
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
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 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)
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 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 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 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)
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)
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)
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).
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: 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 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).
Docs usability: measurement-role snippet clarity, one-line result plots, responsive figures (#86)
* fix: responsive figure sizing — full-width on mobile, 92% plots on desktop (plan 16 T4)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: snippet clarity wave — recording/excitation convention (plan 16 T1)
Apply the recording/excitation naming convention from the P16 audit across
the calibration, intensity, block-processing, time-weighting, levels,
weighting and room-acoustics guides (docs/ + EN guides + ES guides).
- calibration: ref_signal->calibrator_recording, signal->recording (J1 cascade)
- intensity: probe-recording synth + swap-in disclosure on p1/p2
- block-processing / time-weighting: audio_blocks provenance comments
- levels / time-weighting / weighting: signal->recording, provenance upgraded to convention
- room-acoustics: play/record framing on sweep/recorded; synthetic-IR stand-in note
EN docs/guides kept code-identical; ES mirrored with UNE wording.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: one-line canonical plots on result objects (plan 16 T2)
Add a thin plot(ax=None, **kwargs) method to each result dataclass that
renders the essence of its guide's canonical figure in one line, following
the sklearn/statsmodels/librosa pattern. matplotlib stays a soft dependency:
importing phonometry and computing works without it; .plot() raises a clear
ImportError with install guidance. Rendering lives in a new lazy _plotting
module; dataclass methods are thin delegates.
Covers ZwickerLoudness, STIResult, WeightedRatingResult/ImpactRatingResult,
RoomAcousticsResult, SoundPowerResult/ReverberationSoundPowerResult/
SoundPowerIntensityResult, IntensityResult and a new frozen DecayCurve
(returned by decay_curve, tuple-unpackable for backward compat). Rating
results gain band_centers/measured/shifted_reference fields for the figure.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: plot kwargs forwarding, deterministic invalid-band test, octave-impact annotation
- _plot_rating now accepts **kwargs and forwards to the measured curve, so
result.plot(linewidth=2) no longer raises TypeError; parametrized test
covers every public .plot() forwarding a benign kwarg to its primary artist.
- Replace skip-if-all-valid room-acoustics invalid-band test with a directly
constructed RoomAcousticsResult (500 Hz band flagged invalid); assert exactly
the flagged bars are hatched and greyed.
- plot_impact_rating keeps the shifted-reference curve normatively honest
(ref - shift) and marks/annotates the 500 Hz read value with the ISO 717-2
Clause 4.3.2 -5 dB octave rule instead of distorting the curve.
- Move _bar_width next to its only consumer, plot_intensity.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: .plot() one-liners and figure-code collapsibles (plan 16 T3)
Add one-line res.plot() mentions to every guide section whose result type
gained .plot() in T2, and figure-code collapsibles (both the one-liner and
the manual matplotlib path) under the five .plot()-backed data figures.
Document .plot() availability and the DecayCurve return type in the API
reference and CHANGELOG. EN docs + EN/ES site; llms-full.txt regenerated.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: collapsibles for STI-vs-T60 and open-plan figures
Controller follow-up to plan 16 T3: the two figures are reproducible with
public API only. STI-vs-T60 sweeps sti_from_impulse_response over synthetic
exponential IRs at a 10-point T60 grid; the open-plan collapsible rebuilds
the D2,S regression and rD/rP crossings from the OpenPlanResult fields.
EN docs + EN/ES site; executed on Agg; llms-full.txt regenerated.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: final-review polish — result-field docs, comment hygiene, naming
Document band_centers/measured/shifted_reference on WeightedRatingResult
and ImpactRatingResult (CHANGELOG + EN/ES API tables), strip internal plan
tags from theme-images.css and test_result_plots.py, note that [plot] also
powers result .plot() methods, tighten the plot_zwicker_loudness docstring
to say the two-panel layout only occurs when ax is None, sentence-case the
intensity provenance comments, and rename the shadowed impact rating var to
res_imp on the room-acoustics pages.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 86 review round 1 — log-axis bars, style kwargs overrides, snippet physics and notation
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
Audit pass 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
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).
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: 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 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 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
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 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
Audit pass 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
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: 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 3: shared validation, energy math, types and warning base (#110)
* refactor: shared validation, energy-math, types and warning base
Audit batch 3 — retire the per-module reinventions of the same private
infrastructure, with numerically identical behavior (the full suite's
pinned worked-example values and the byte-stable conformance report are
unchanged):
- _validation.py grows require_positive / require_non_negative /
require_fraction / require_choice (all NaN-rejecting, quoted-parameter
messages); en12354_6 and iso2631_5 drop their local equivalents
- new _levels_math.py (energy_mean / energy_sum / weighted_energy_mean)
replaces the two duplicated _energy_average helpers and the inline
energy combinations in sound_power, sound_power_intensity,
sound_power_reverberation, occupational_exposure, intensity and
insulation; the weighted denominator respects the reduction axis so
axis-varying weights stay correct
- new _types.py (Real alias, as_float_or_array) replaces four duplicate
alias definitions and thirteen copies of the scalar-or-array return
idiom
- new _warnings.py with PhonometryWarning: all eleven warning classes
rebase onto it so users can filter the whole library at once; bare
UserWarning sites gain proper categories (FilterBankWarning,
STIWarning, TonalityWarning, ImpulseResponseWarning) and the
category-less frequencies warning is fixed
- the byte-identical _check_grade and _validate_conditions duplicates
are merged into single definitions
Deliberately NOT migrated (subtly different math, noted in the audit):
the SII masking composition, the NT ACOU 112 reference-time-normalized
rating and the exposure-weighted sum in occupational_exposure.
* refactor: address review feedback on the shared helpers
- the scalar validators reject infinities too (math.isfinite), matching
their own 'finite' docstrings and the isfinite validators elsewhere
(Copilot)
- as_float_or_array accepts scalars via np.ndim instead of assuming an
ndarray (Gemini)
- energy_mean computes through np.mean, bit-identical to sum/n at every
call-site shape (verified) and robust to tuple axes at runtime
(Gemini)
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: 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 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: 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