at main
16 folders
27 files
Add ISO 9053-1 static airflow-resistance report via .report() (#339)
* Add ISO 9053-1 static airflow-resistance report via .report()
Add a one-page PDF material airflow-resistance test report to
StaticAirflowResult, laid out like an accredited ISO 9053-1:2018
static/direct airflow-method certificate.
The fiche carries a standard-basis line, an optional metadata header
(client, manufacturer, specimen, specimen thickness, test facility,
date, climate), a two-panel body with a metrics table (the evaluation
velocity, the fitted pressure difference, the airflow resistance R, the
specific airflow resistance R_s, the airflow resistivity sigma when a
thickness is available, and the through-origin fit coefficients a and b)
beside the fitted pressure-drop curve, and a boxed specific airflow
resistance R_s with R and sigma alongside, all read at the clause 7.5
reference velocity of 0.5 mm/s. ISO 9053-1 is a material
characterisation, so the fiche carries no pass/fail verdict.
The renderer reuses the shared report layout and metadata container, and
renders in English and Spanish. A worked example is registered in the
report generator and its rendered fiche committed alongside the others.
* Document the plot dependency and use a language-neutral precision note
The static airflow-resistance fiche embeds the fitted curve, so state that
rendering needs both reportlab and matplotlib (phonometry[report,plot]) in the
English and Spanish guides and the materials doc. Describe the evaluation-velocity
precision as one decimal place rather than with a locale-specific separator, and
regenerate the API page.
* Extract the shared material-test fiche scaffold
The dynamic-stiffness (EN 29052-1 / ISO 9052-1) and static airflow-resistance
(ISO 9053-1) fiches share the same one-page shape: title and basis line,
metadata identity grid, a metrics table beside the result's self-scaling plot,
a boxed single-number result with extended terms, and the footer. Move that
common scaffold into a single _material_fiche helper (the content dataclass, the
identity-grid builder, the standard-basis-line helper and the renderer) and have
both fiches supply only their own labels, metric rows and boxed statement. The
rendered output is unchanged.
Add element-normalized intensity insulation report via .report() (#338)
* Add element-normalized intensity insulation report via .report()
Add IntensityElementNormalizedResult.report(): a one-page PDF fiche for
the element-normalized level difference DI,n,e of a small building element
measured with sound intensity (ISO 15186-1:2000, Clause 3.9, Formula (8)).
DI,n,e is a level difference rated by the ISO 717-1 airborne machinery, the
same as the sibling intensity sound reduction index RI, so the fiche is
driven through the shared insulation skeleton (render_insulation_fiche) with
the shared iso717_columns_builder. It lives in the existing iso15186.py
renderer beside the RI fiche, so no second near-identical renderer is added.
The sheet carries the standard-basis line, an optional metadata header, the
per-band table (16 one-third-octave or 5 octave bands) beside the
measured-versus-shifted-reference curve, the boxed rating DI,n,e,w (C; Ctr)
and the intensity-method statement. verbose=True shows the ISO 717 evaluation
per band; a metadata requirement adds a PASS/FAIL verdict (the element
insulation passes at or above the target); language="es" renders the Spanish
fiche.
The example is anchored to the ISO 717-1:2020 Annex C worked-example curve
read as a documented DI,n,e spectrum, pinning the rating to the published
30 (-2; -3) dB through the intensity path without a new numeric oracle. It is
registered in the report generator with its committed PDF and WebP preview,
and the guides and API reference are updated.
* Share the intensity-report request validation helper
Extract the engine, language, rating-presence and band-count guards the two
ISO 15186-1 intensity report methods share into a single
_validate_intensity_report helper, mirroring the flanking-transmission module.
It returns the validated non-None rating so each report method hands it
straight to the renderer, removing the duplicated guard block.
* Regenerate API reference after rebase
Sound power from surface vibration report via .report() (ISO/TS 7849) (#325)
* Add ISO/TS 7849 sound-power-from-vibration .report() fiche
Render VibrationSoundPowerResult (airborne sound power radiated through
surface vibration, ISO/TS 7849-1/-2:2009) to a one-page PDF fiche via the
shared sound-power report engine. The vibration-method variant adds the
surface velocity level Lv and radiation factor epsilon columns, the
radiating area S in the boxed result and the LW = Lv + 10 lg(S/S0) +
10 lg(epsilon) + 10 lg(411/400) basis strip, and names the survey (Part 1,
fixed epsilon = 1) or engineering (Part 2, determined epsilon) method.
Add a sound_power_level_a property (A-weighted total) to the result and the
Spanish translations for the new strings.
* Add the ISO/TS 7849 example fiche and its committed preview
Register an engineering-method (Part 2) example in generate_reports.py: a
gearbox casing of radiating area S = 1.6 m2 surveyed over six octave bands
with a measured radiation factor, giving LWA = 88.7 dB(A) re 1 pW against a
declared 90 dB(A) limit. Commit the rendered PDF and its WebP preview.
* Test the ISO/TS 7849 sound-power-from-vibration fiche
Recompute LW and LWA from the closed-form ISO/TS 7849 Eq. 3/8/12 against a
clean-room oracle and assert they, the band labels, the method part and the
basis prose appear in the PDF; cover the survey/engineering variants, the
verbose radiation-factor column, the verdict, the metadata header, the
Spanish fiche and the rendering contract.
* Document the ISO/TS 7849 report and regenerate the API reference
Add a measurement-report section to the EN and ES vibration-sound-power
guides (with the ReportPreview) and the docs mirror, regenerate the
generated API page for the new report() and sound_power_level_a members, and
record the addition in the changelog.
* Address review on the ISO/TS 7849 report
- Do not present an unweighted broadband LW as LWA: sound_power_level_a now
returns nan without a band spectrum, so the fiche boxes the unweighted total
LW and draws no A-weighted verdict, and the basis strip omits the A-weighting
sentence for a broadband result.
- Drop the unused result parameter from the relation-strip helper.
- Render the fixed impedance term with the locale decimal separator (0.12 dB in
the English fiche, 0,12 dB in the Spanish one) via format_number.
- Cite the accelerometer calibration standard (ISO 16063-21) in the example
instead of IEC 60651, which specifies sound level meters.
- Assert the one-page contract on the longer one-third-octave table and add a
broadband test asserting no false A-weighted claim.
Regenerate the example fiche PDF and WebP preview and the generated API page.
Add wind-turbine tonal audibility report via .report() (#337)
* Add wind-turbine tonal audibility report via .report()
Add WindTurbineTonalityResult.report() rendering a one-page PDF
wind-turbine tonality-assessment fiche (IEC 61400-11:2012+A1:2018,
subclauses 9.5.2-9.5.5).
The sheet carries a standard-basis line, an optional metadata header
(source/situation, client, measurement position, instrumentation, date),
a two-panel body with the critical-band and masking analysis in a metrics
table (tone frequency, critical bandwidth, tone level L_pt, masking-noise
level L_pn, tonality dL_tn, audibility criterion L_a and tonal audibility
dL_a) beside the narrowband-spectrum plot with the critical band, masking
level and tone marked, and a boxed decisive tonal audibility dL_a with the
tone frequency and the audibility decision (a tone is audible when dL_a
exceeds 0 dB). A maximum acceptable tonal audibility supplied via the
metadata requirement adds a PASS/FAIL verdict (a lower audibility is
better). English and Spanish both render.
Register the example fiche, add structural and i18n tests, and regenerate
the committed example PDF, WebP preview and API reference.
* Base the audibility decision on the displayed rounded tonal audibility
The boxed result, verdict and decision text all commit to the tonal
audibility rounded as displayed, but the decision phrase and note branched
on the raw is_audible flag. A raw dL_a just above 0 dB that rounds to a
displayed 0.0 dB could therefore print a self-contradicting "0.0 dB > 0".
Derive the audibility decision from the same rounded value through a shared
helper, and add a boundary regression test.
Add ISO 9053-1 static airflow-resistance report via .report() (#339)
* Add ISO 9053-1 static airflow-resistance report via .report()
Add a one-page PDF material airflow-resistance test report to
StaticAirflowResult, laid out like an accredited ISO 9053-1:2018
static/direct airflow-method certificate.
The fiche carries a standard-basis line, an optional metadata header
(client, manufacturer, specimen, specimen thickness, test facility,
date, climate), a two-panel body with a metrics table (the evaluation
velocity, the fitted pressure difference, the airflow resistance R, the
specific airflow resistance R_s, the airflow resistivity sigma when a
thickness is available, and the through-origin fit coefficients a and b)
beside the fitted pressure-drop curve, and a boxed specific airflow
resistance R_s with R and sigma alongside, all read at the clause 7.5
reference velocity of 0.5 mm/s. ISO 9053-1 is a material
characterisation, so the fiche carries no pass/fail verdict.
The renderer reuses the shared report layout and metadata container, and
renders in English and Spanish. A worked example is registered in the
report generator and its rendered fiche committed alongside the others.
* Document the plot dependency and use a language-neutral precision note
The static airflow-resistance fiche embeds the fitted curve, so state that
rendering needs both reportlab and matplotlib (phonometry[report,plot]) in the
English and Spanish guides and the materials doc. Describe the evaluation-velocity
precision as one decimal place rather than with a locale-specific separator, and
regenerate the API page.
* Extract the shared material-test fiche scaffold
The dynamic-stiffness (EN 29052-1 / ISO 9052-1) and static airflow-resistance
(ISO 9053-1) fiches share the same one-page shape: title and basis line,
metadata identity grid, a metrics table beside the result's self-scaling plot,
a boxed single-number result with extended terms, and the footer. Move that
common scaffold into a single _material_fiche helper (the content dataclass, the
identity-grid builder, the standard-basis-line helper and the renderer) and have
both fiches supply only their own labels, metric rows and boxed statement. The
rendered output is unchanged.
Noise-control performance reports via .report() (#331)
* Add noise-control performance reports via .report()
Add a one-page PDF .report() fiche to the three noise_control result
types, each laid out with a per-band table beside the result's own plot,
a boxed single-number performance figure and an optional PASS/FAIL
verdict:
- EnclosureResult: machine-enclosure insertion loss (Bies, Hansen &
Howard, section 7.4.2). The table lists the supplied panel transmission
loss R, the interior build-up correction C and the net insertion loss
IL = R - C; the boxed figure is the mean insertion loss with the
external and internal surface areas. verbose=True adds the interior
room constant column. A declared minimum passes when the mean meets it.
- ReactiveSilencerResult: reactive-silencer transmission loss (Munjal
Eq. (3.27); Bies sections 8.8-8.9). The table lists the transmission
loss TL and, when end impedances were given, the insertion loss IL; the
boxed figure is the mean transmission loss with the peak and the device
kind. A declared minimum passes when the mean meets it.
- HvacSpectrumResult: HVAC duct-noise spectrum (Bies Chapter 8; VDI
2081-1). A regenerated-noise spectrum boxes the A-weighted sound power
level with the overall total (lower is better); an attenuation spectrum
boxes the mean attenuation (more is better). verbose=True adds the
A-weighting correction and A-weighted band-level columns.
The three renderers share a two-panel skeleton in
_report/_noise_control_fiche.py and reuse the sound-power table builder,
band labels and header grid. Each accepts an optional metadata header,
states its method basis and renders in English or Spanish.
Register one committed example per fiche under .github/reports/, add
structural and clean-room number-presence tests (EN and ES), and update
the CHANGELOG and the regenerated API reference.
* Address SonarCloud findings on the noise-control renderers
Reduce render_noise_control_fiche below the parameter-count threshold by
fixing the two-panel split widths internally (the three renderers never
overrode them), and lift the HVAC verdict symbol/unit selection out of a
nested conditional into an explicit if/elif/else. No change to rendered
output; the committed example fiches are unaffected.
* Round the requirement to display precision and fit the verbose table
Compare the declared requirement at the same one-decimal precision as the
measured value in the noise-control verdict, so the printed comparison can
never contradict the verdict at the boundary. Also trim the verbose
enclosure table columns to sum to the 64 mm left panel width.
Add reverberation-time report fiches via .report() (#336)
Add a one-page PDF .report() to the two reverberation result types.
ReverberationModelResult.report() renders a design-stage prediction of the
reverberation time by the five classical statistical-acoustics models (Sabine,
Eyring, Millington-Sette, Fitzroy and Arau-Puchades): a per-band table with one
reverberation-time column per model beside the model comparison plot, and a
boxed mid-frequency reverberation time from Arau-Puchades with the per-model
spread alongside. It is labelled a prediction, not a measurement: the five
models bracket the reverberation time likely to occur, so no PASS/FAIL verdict
is emitted; a target reverberation time is printed as a reference line only.
ReverberationResult.report() renders an enclosed-space characterisation
(EN 12354-6:2003): a per-band table of the equivalent sound absorption area A
and the reverberation time T beside the reverberation-time plot, the room
volume and object fraction in the header, and a boxed mid-frequency
reverberation time with the mid-frequency absorption area alongside. A target
reverberation time is likewise printed as a reference line without a verdict,
since a room reverberation time is a target range rather than a strictly
higher/lower-is-better quantity.
Both renderers live in a shared _report/reverberation.py module (shared
mid-frequency descriptor, time formatting, octave-band table and header grid
helpers). Add the English and Spanish fixed strings, one committed example
fiche per result under .github/reports/, structural and value-presence tests
in English and Spanish, and the guide sections in both languages. Regenerate
the API reference.
Add structural-vibration FRF reports via .report() (ISO 7626, ISO 10846) (#334)
* Add structural-vibration FRF reports via .report() (ISO 7626, ISO 10846)
Add a one-page PDF .report() fiche to the two structural-vibration
frequency-response result types.
MobilityResult.report() renders a mechanical-mobility measurement fiche
(ISO 7626-1:2011 frequency-response-function definitions; measurement per
ISO 7626-2:2015). Mechanical mobility is a continuous frequency-response
function, not an octave-band quantity, so the sheet presents it honestly as the
mobility magnitude spectrum |Y(f)| plus a compact table of the FRF's
characteristic points (the FRF type, driving-point or transfer, the frequency
range, the peak frequency, the peak mobility magnitude and the phase there),
with a boxed peak mobility |Y| at the frequency it occurs at. It is a
characterisation, so there is no pass/fail verdict.
TransferStiffnessResult.report() renders a dynamic-transfer-stiffness
characterisation fiche for a resilient element (ISO 10846-1:2008 definition;
determined by the direct method, ISO 10846-2:2008, or the indirect
blocking-mass method, ISO 10846-3:2002). The transfer stiffness is a continuous
frequency-response function, so the sheet presents it as the transfer-stiffness
level spectrum Lk(f) plus a compact table of characteristic points (the
determination method, the blocking mass for the indirect method, the frequency
range, and the low-frequency stiffness plateau |k2,1|, its level Lk and the loss
factor there), with a boxed low-frequency Lk. It too is a characterisation, with
no verdict.
Both fiches share a common FRF body (_report/_frf_fiche.py): the title and
basis line, the optional metadata header, the two-panel body with the
characteristic-point table beside the result's own spectrum plot, the boxed
representative value and the footer. English and Spanish both render; example
inputs reuse the modules' oracle-validated closed forms.
* Reuse the result loss_factor in the transfer-stiffness fiche
Share the single ISO 10846-1 (3.8) loss-factor definition by reading the
result's own loss_factor property at the low-frequency index, instead of
recomputing eta = Im/Re in the renderer.
* Drop the unused low-frequency frequency in the stiffness table
The characteristic-point table only needs the magnitude, level and loss
factor at the low-frequency plateau, not its frequency.
chore: project polish + standards compliance (py.typed, CHANGELOG, CITATION, IEC 61672-1/61260-1 suites, class verifier) (#64)
* chore: ship PEP 561 py.typed marker
Downstream users' type checkers now receive the library's strict type
annotations and overloads.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: add CHANGELOG.md (Keep a Changelog format)
Back-filled from the real release history; the Unreleased section
documents plans 1-4 with an explicit 'Numerical behavior changes' block
that will feed the next release notes.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: add CITATION.cff and .zenodo.json metadata
Enables GitHub's 'Cite this repository' button and gives Zenodo explicit
metadata (title, ORCID, license, related identifiers) for the DOI
registration of the next release.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* test: add IEC 61672-1 Table 4 tone-burst compliance suite (fast/slow)
Reference responses and class 1 acceptance limits transcribed from the
official text (BS EN 61672-1:2013 Table 4, standard page 25), F and S
weightings, 1 s down to 1 ms bursts. The standard defines no toneburst
response for an impulse weighting. Includes an Equation (7) consistency
check of the transcription itself.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: add IEC 61260-1:2014 filter class verifier
verify_filter_class(bank) checks every band's relative attenuation
against the class 1/class 2 acceptance limits of BS EN 61260-1:2014
Table 1, with the fractional-octave breakpoint mapping (Formulas 9/10)
and log-frequency interpolation (Formula 11) from the official text.
class_limits() exposes the limit curves (used later for the mask figure).
With default parameters (order 6, fraction 3): butter meets class 1
(+0.4 dB margin); cheby2 lands in class 2, capped exactly by its
attenuation=60 vs the 70 dB far-stopband requirement.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* test: pin A/C weighting to official IEC 61672-1 Table 3 class 1 limits
Nominal weightings and asymmetric class 1 acceptance limits transcribed
from BS EN 61672-1:2013 Table 3 (standard page 22). Full 34-frequency
sweep for A and C at 48 kHz and 96 kHz via steady-state tone gain; both
curves meet class 1 at every nominal frequency.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: document IEC 60942 calibrator assumptions for calculate_sensitivity
From the norms library audit: default 94 dB target matches the standard's
usual calibrator output (principal level >= 90 dB re 20 uPa), sensitivity
inherits the calibrator class tolerance (class 1: +/-0.4 dB, 160-1250 Hz,
Table 1), and the standard's 20 s averaging informs how long the reference
recording should be.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* ci: Codecov upload, numba in a dedicated job, badges and classifiers
- Coverage uploaded to Codecov (OIDC) from the ubuntu tests job; codecov
and SonarCloud quality-gate badges plus downloads/pyversions in README.
- The main test matrix now runs the latest numpy with the pure-Python
impulse kernel (numba removed from requirements.txt and its numpy cap
with it). A new tests-perf job installs .[perf] and is the only one
exercising the jitted kernel - previously NUMBA_DISABLE_JIT=1 meant CI
never ran it at all.
- Python version/audience/typed classifiers in pyproject.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* chore: target mypy at 3.12 (numpy 2.5 stubs use PEP 695 type statements)
Runtime support stays at >= 3.11: Python 3.11 users resolve numpy 2.4.x.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* chore: make the numba fallback type-ignore environment-independent
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* chore: address review feedback on compliance API and CI
- conftest now setdefaults NUMBA_DISABLE_JIT so the tests-perf job (which
sets it to 0) really exercises the jitted kernel - Copilot caught that
pytest_configure was force-disabling JIT everywhere.
- verify_filter_class: rename worn -> num_points (new public API), validate
inputs (num_points >= 16, fraction > 0), never report compliance for an
empty bank, and always evaluate the Table 1 breakpoints so the pass-band
constraints are checked regardless of grid density.
- tests: stateful-bank test now asserts design equality against the
stateless equivalent (was vacuous); round() for burst sample conversion.
- workflow: comment on id-token permission, job name and
persist-credentials: false on the new job's checkout.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
fix: psychoacoustics standards-correctness fixes with corrected calibrations and printed oracles (#172)
* test: repair the ISO 532-1 Annex B fixtures path and extend the Annex B coverage
The relative data path was not adjusted when the test file moved into
tests/psychoacoustics/, so all 21 Annex B validation tests had been
silently skipping through the requires_iso_data guard. Point DATA at
tests/data/iso532_1 again and add a hard in-repo presence assertion so a
future move fails loudly instead of re-skipping.
Also enable the shipped Annex B.4 Test signal 13 (Nmax reproduces the
workbook header to 6e-6 relative), add a bounds-only Annex B.3 pink-noise
check against the workbook tolerance band, and note why the B.4 ramp
signals 6-9 cannot be regenerated.
* fix: ECMA-418-2 roughness front-end applies the Clause 5.1.2 fade-in and the calibration hits 1 asper
The roughness path skipped the mandatory 5 ms trigonometric fade-in of
Formula (1), which loudness and tonality already apply; recordings cut
mid-sound picked up an onset artefact that survived the transient discard
through the asymmetric smoother. The fade-in is now a shared helper used
by all three metrics.
With the fade-in in place and the Clause 7 reference signal synthesized at
its stated level (the overall RMS SPL of the modulated signal, 60 dB, not
the carrier-alone level), the chain reproduces the 1 asper calibration to
0.99990 asper using the tabulated c_R = 0.0180685. The former +7.35 %
'clean-room methodology variance' narrative was wrong and is removed from
the docstring, tests, reference data, conformance check and docs; the test
and conformance now pin the true 1.000 asper anchor.
A code note in the segmentation records why the printed Clause 5.1.5.2
l_last formula cannot be implemented literally (blocks would overrun the
padded signal): the flush-to-end reading is the only self-consistent one.
* fix: psychoacoustic annoyance combines the weightings per F&Z Eq. (16.2)
The combination formula had the '1 +' inside the radical,
N5*sqrt(1 + wS^2 + wFR^2), where Fastl & Zwicker (2006, p. 328) print
PA = N5*(1 + sqrt(wS^2 + wFR^2)). Every PA value with a nonzero
sharpness, fluctuation or roughness contribution was underestimated by
13 to 29 percent, and the worked reference value had been hand-computed
with the same wrong form, so tests and conformance confirmed the bug.
The worked tuple (N5,S,F,R) = (30, 2.0, 0.5, 0.3) now anchors
PA = 37.0478 (wS and wFR unchanged), and the docs, site guides and API
reference show the corrected formula. The SQAT claim is now accurate:
SQAT implements the (1 + sqrt) combination.
* fix: fluctuation-strength Bark constant 0.76e-3 per Zwicker-Terhardt (Osses 2016 Eq. 3 misprint)
Osses/Garcia/Kohlrausch (2016) Eq. (3) prints 0.76e-4 in the critical-band
rate formula, a 10x typo of the Zwicker-Terhardt 0.76e-3 disproved by the
paper's own anchors (0.5 Bark = 50 Hz, 15 Bark = 2.7 kHz, 23.5 Bark =
13.2 kHz). As transcribed, the 47 filter centres spanned 491 Hz-20 kHz with
12 bands pinned at the grid edge, distorting the specific pattern for any
signal away from 1 kHz (0.56 instead of 0.86 vacil at a 125 Hz carrier,
1.10 instead of 0.58 at 8 kHz).
The self-derived calibration constant lands at C_FS = 0.279, close to the
paper's 0.2490 and inside the sanity guard. The re-fitted H(fmod) corners
did not compensate the bug: the 1 kHz modulation sweep is essentially
unchanged (16 Hz point 0.19 -> 0.17 vacil), so the corners stand as fitted.
New oracles: the carrier-frequency sweep (125 Hz-8 kHz) against measured
reference values and the F&Z Fig. 10.5 trend (low-mid plateau, 8 kHz
roll-off), and the Osses Table 1 AM-broadband-noise row as a trend-only
cross-check (the excitation front-end overshoots the BBN pass-band by up
to ~3x, now documented; the FM row is deliberately not pinned). The module
docstring also records the front-end deviations from the paper and that
the normative ECMA-418-2 Clause 9 fluctuation strength is a different,
unimplemented method.
* fix: ECMA-418-2 loudness uses the full Clause 6.2.3 cross-block-size averaging
The loudness path averaged neighbouring-band ACFs only within a block-size
group, skipping the cross-group recomputation Clause 6.2.3 mandates for
bands adjacent to a block-size change. Since Clause 8.1.1 builds loudness
on the Clause 6.2 outputs, the requirement applies to loudness identically;
tonal content near the block-size boundaries was mis-stated by up to
-9.5 % (166.7 Hz tone at the z=2 edge).
The full averaging now lives in loudness_ecma as a single shared
_tonal_noise_split (front-end + Clause 6.2 tonal/noise decomposition) that
both the loudness and tonality metrics call, so the two report an
identical N'_tonal(l, z) for the same signal -- asserted by a new
cross-consistency test that spies on the shared intermediate through both
call sites.
The 1 kHz / 40 dB calibration moves from 0.9960 to 0.9845 sone_HMS. The
-1.55 % residual exceeds the +/-0.25 % adjustment allowed for c_N, which
is kept at the verbatim tabulated value; the investigation (documented in
the module docstring) attributes it to the mandated band averaging around
the block-size-boundary bands excited by the tone's lower flank: no band
averaging reads 0.955, same-group-only averaging 0.996, full cross-group
averaging 0.9845, while block-time smoothing and the fade-in/LP transient
contribute under 0.01 %.
Also in this pass: the common time grid is sized from the 1024-sample
stage's block count instead of the 8192 group's (whose 7 extra trailing
blocks only produced edge-held interpolations), the benign max(.,0) clamp
that Formula 48 does not print is documented in place, _band_range
validates the Formulae 56/57 preconditions (16 Hz < f_L, f_H < 20 kHz,
f_L < f_H) instead of silently swapping reversed edges, and the three
Sottek metrics document their mono-only scope (no Formulae 112/118
binaural combination).
* feat: ISO 532-1 stationary TimeSkip parameter and reference-program provenance notes
Annex B.1 requires the stationary calculation to start from 0.2 s when
validating against the official Annex B recordings (the reference
f_square_and_smooth takes a TimeSkip and the CLI requires it); the
stationary path previously always averaged the whole signal, so leading
silence or the filterbank transient diluted the mean square. The new
time_skip parameter (default 0.0, validated in both modes, applied by the
stationary method only) implements it.
Documentation nits from the same review: the sone-to-phon mapping records
that the exact 10/lg 2 factor and the 3 phon floor come from the
electronic attachment's reference program rather than the printed
Formula (2), the percentile note records the same provenance for the
(k-1, k) mean, and the filterbank tables are labelled correctly (Table A.1
is the reference section; the deviations and gains are Table A.2).
* fix: sharpness Annex B variants use the literal k = 0.11; ISO 532-2/-3 oracle expansion
DIN 45692 Formulas (B.1)/(B.2) print S = 0,11 * moment for the Aures and
von Bismarck variants; the implementation had derived per-variant
constants so the clause 6 reference sound read exactly 1.00 acum, shifting
every Aures result +4.2 % against other implementations of the published
formulas. With the literal 0.11 the reference lands near (not exactly at)
1 acum: 0.96 Aures / 1.02 von Bismarck, asserted non-circularly at
1.00 +/- 0.05. The normative clause 5.2 k remains derived, as the standard
prescribes.
Sharpness verification now covers all 21 Table A.2 rows and all 20
Table A.3 broadband rows (band edges per Tabelle A.1, every signal set to
the clause 6 loudness of 4 sone) at the normative 5 % / 0.05 acum
tolerance, and the conformance report gains a non-definitional Table A.2
row (2.5 kHz -> 1.78 acum) beside the definitional reference-signal check.
ISO 532-3: the module docstring now states the native-fs conformance mode
(clause 5 prescribes a 32 kHz conversion; this implementation processes at
the native rate with per-window FFT lengths) and the Annex C.1 anchor is
pinned at 32/44.1/48 kHz (0.9918/0.9926/0.9900 with a 0.5 s tone, 0.3 %
cross-rate spread). New oracles: the Annex C.3 multi-tone complexes, the
Annex C.1.2/C.1.3 sone columns, the ISO 532-2 Annex B phon columns, and a
note marking the byte-identical anchor pin as a regression pin rather than
a conformance oracle. The Table 5 phon mapping documents its silent
saturation at 120 phon.
* feat: ISO/PAS 20065 extended uncertainty of the audibility (Clauses 5.4 and 6)
Implements the Gaussian propagation of the uniform 3 dB narrow-band level
uncertainty through the audibility chain (Formulae (22)-(27)) to the
extended uncertainty U with 90 % bilateral coverage (k = 1.645,
Formula (29)), plus the Formula (28) combination for the mean audibility.
analyze_spectrum now reports the per-tone U on the result object, and the
docstrings carry the Clause 6 requirement that U shall be considered when
fewer than 12 spectra are averaged.
Oracles: the printed Table E.2 U column (the decisive 137.3 Hz tone
reproduces 2.79 dB to 0.006 dB from the E.1 lines; the flanking tones
within 0.1 dB, their critical bands extending beyond the truncated table),
the 2 FG row (3.21 dB, using the N summated tone levels as the K summands,
the reading that reproduces the print), the Table E.4 decisive chains of
all five spectra, the Annex E Step 4 mean uncertainty (1.38 dB) and the
Table E.2 LG/av columns and line-snapped band limits. Table E.3's full
tone record is pinned as printed data (its spectra lines are not
published). The E.1 NOTE 1 truncated-band LS values are recorded as not
reproducible under any tested iteration variant.
The distinctness test and module docstring record the DIN-vs-ISO print
difference (fT/sqrt(2) on both edges per the executable DIN 45681 Annex J
program, versus the asymmetric ISO print), the A-weighting requirement of
Clause 5.3.2, and the Formula (19) validity range 50 Hz < fT < 1000 Hz.
Two new conformance rows cover the per-tone and mean uncertainties.
* feat: ECMA-418 decision-threshold constants, degenerate-input warnings and range-note fixes
The verbatim decision criteria of ECMA-418-2 are now exposed and pinned as
constants-tests: audibility at 0.01 sone_HMS total basis loudness
(Clause 5.1.9), prominent tonality at 0.4 tu_HMS (Clause 6.3) and
prominent roughness at 0.2 asper (Clause 7.2) -- beyond the calibration
points these are the standard's only further numeric anchors (its annexes
are graphical).
ECMA-418-1 TNR/PR: the prominent verdict is documented as the numeric
criterion only (the aural-examination and lower-threshold-of-hearing
requirements of clauses 11.8/12.8 and 8/9 are the caller's
responsibility), and degenerate inputs now warn instead of silently
returning meaningless verdicts -- peaks within one bin of the 89.1 Hz /
11.2 kHz range edge (where bin snapping can flip the verdict) and
critical bands at numeric-noise power (silence, DC). A code note records
that the '11 220 Hz' upper range limit printed in clause 4.1.2
contradicts every formula and table of the standard, which use 11 200 Hz.
The binaural-scope notes now cite the correct clauses: Formula (118) /
Clause 8.1.5 for loudness, Formula (112) / Clause 7.1.11 for roughness,
and no binaural combination exists for tonality.
* docs: errata entries for the ECMA-418, ISO/PAS 20065 and Osses 2016 print defects; changelog for the psychoacoustics review
Records four defects surfaced by the standards-correctness review in the
errata registry: the ECMA-418-1 clause 4.1.2 '11 220 Hz' range misprint,
the internally inconsistent ECMA-418-2 clause 5.1.5.2 last-block formula,
the ISO/PAS 20065 clause 5.3.4 edge-steepness print that contradicts the
executable DIN 45681 Annex J reference program, and the Osses 2016 Eq. (3)
Bark-constant exponent typo (marked as a non-standard source). The
changelog gains the corresponding Added/Fixed entries.
* docs: regenerate the psychoacoustics figures and the conformance report
The five figures whose generators consume the corrected metrics
(Sottek loudness, tonality/roughness demo, loudness-model comparison,
fluctuation strength, psychoacoustic annoyance) are re-rendered in all
four language/theme variants. The conformance report picks up the
corrected anchors (roughness 0.9999 asper at the overall-60 dB reference,
ECMA loudness 0.9843 sone_HMS with the full Clause 6.2.3 averaging,
PA 37.0478) and the three new rows: the DIN 45692 Table A.2 sharpness
oracle and the ISO/PAS 20065 per-tone and mean extended uncertainties.
235/235 checks pass.
* docs: correct the PA formula box in the annoyance figure
The figure's info panel still printed the pre-fix combination
N5 sqrt(1 + wS^2 + wFR^2); it now shows the Fastl & Zwicker Eq. (16.2)
form N5 (1 + sqrt(wS^2 + wFR^2)) that the plotted curves already use.
* test: regenerate the ECMA loudness and roughness golden baselines
The goldens pin exact refactor-drift baselines; the ECMA-418-2 loudness
values legitimately changed with the full Clause 6.2.3 band averaging and
the roughness values with the Clause 5.1.2 fade-in, so the two cases are
recaptured with scripts/bench.py --golden (the tonality case is
byte-identical: it already used the full averaging).
* docs: uncertainty API in the tone-audibility guides and API reference
The Clause 5.4/6 extended uncertainty gets its section in the guide (EN and
the two site mirrors), the API reference gains the new function row and the
changed signatures (time_skip, extended_uncertainties), and a Spanish
output comment prints the dot the program actually emits.
* refactor: review follow-ups on the psychoacoustics changes
Range guards use direct comparisons, the API-reference row states the real
audibility_uncertainty signature, and the loudness calibration comment
matches the report's computed figure.
B, AU and D frequency weightings (ANSI S1.4-1983, IEC 61012, IEC 537) (#286)
* feat(metrology): add B, AU and D frequency weightings
Extend WeightingFilter / weighting_filter with three more curves next to
A/C/G/Z, all sharing the 1 kHz normalization, the high_accuracy
oversampled design and multichannel/stateful processing:
- B per ANSI S1.4-1983 Appendix C (Formula C2: the C weighting with one
more zero at the origin and a real pole at f5 = 158.48932 Hz),
documented as historical since IEC 61672-1 dropped it.
- AU per IEC 61012:1990: the A weighting cascaded with the six-pole U
low-pass of Table 2, for measuring audible sound in the presence of
ultrasound. The Table 2 poles reproduce every Table 1 nominal value
within 0.05 dB; the design oversamples toward 288 kHz because the U
roll-off acts up to 40 kHz.
- D per the withdrawn IEC 537:1976, from its published rational transfer
function. Cross-checked against SQAT (identical zeros/poles) and
librosa's independent closed form (within 0.002 dB, 10 Hz to 20 kHz),
and pinned against the IEC 537 table republished in NASA CR-3406
(Table SLD-I), which the response reproduces within 0.1 dB everywhere
except that table's 1600/2500 Hz cells.
verify_weighting_class now also attests B (ANSI Table IV design goals,
Table V Type 1/2 masks in the class slots, plus the between-nominals
sweep against the Appendix C analytic form) and AU (nominal A + U with
the Table 1 separate-unit tolerances and the subclause 2.2 explicit
values at 25/31.5/40 kHz). G and D are rejected with a clear error: no
class-structured tolerance tables exist for them.
The conformance report gains four rows: B against the strictest Type 0
mask at 48 kHz, AU over the full 10 Hz-40 kHz Table 1 range at 96 kHz,
and D against the published tabulated curve. Tests pin the transcribed
masks to independent reference_data copies and the realized responses
to the standards' tables at the nominal frequencies (31.5 Hz / 1 kHz /
8 kHz pins per curve, 50 Hz for D where its table starts).
The weighting guide (EN/ES/docs) gains a section on the three curves
with worked LD-vs-LA and ultrasound-rejection examples, the curve
family figure now draws all six curves, and the API reference and
CHANGELOG are updated accordingly.
* docs: regenerate the derived artifacts after rebasing onto main
* refactor(metrology): extract the analog weighting design from the constructor
Move the per-curve analog ZPK construction into a private _analog_design
method so the constructor reads linearly, and construct the filter outside
the pytest.raises block in the class-verifier rejection test.
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(site): link sidebar group labels to their section landing pages (#212)
* feat(site): link sidebar group labels to their section landing pages
Replace the stock Starlight sidebar with a local override
(src/components/Sidebar.astro + SidebarSublist.astro, adapted from
upstream 0.41.3) that renders every group permanently expanded and,
when a group's first item carries attrs: { 'data-group-link': true },
consumes that entry and turns the group label itself into the link
(Overview-first convention). The separate Overview rows disappear from
the sidebar while the landing pages stay published, indexed and in the
prev/next chain. Groups without a landing page (Start, Reference and
the generated API sections) remain static headings, now without a
caret, and the collapsed flag has no effect.
The API sidebar generator marks reference/api the same way, so the
"API reference" label links to the index page. sidebar.css splits
typography and color rules, guards the color on
:not([aria-current='page']) so the accent pill keeps its inverted text
when a label link is the current page, and restores the accent
hover/focus feedback for label links.
* test: expect the linked API group entry in the sidebar fragment
* refactor(site): forward landing link attributes and flatten the sublist mapping
perf: parallel test suite with shared heavy fixtures (#203)
* test: share memoised heavy analysis results and drop a debug artifact write
The Annex C.1 Moore-Glasberg time-varying loudness suites analysed the
same calibrated tones independently (the phon table and the sone table
pin different columns of the identical result), and the anchor tone was
recomputed by three checks; a module-scoped memoising fixture now
computes each distinct (frequency, level, field, presentation) tone once
and shares the frozen result object. The ECMA-418-2 roughness peak and
modulation-depth checks likewise reuse the module calibration result
instead of recomputing the same 70 Hz AM chain. No oracle, tolerance or
assertion changes; the shared dataclasses are frozen and only read.
The pink-noise flatness test no longer renders and writes a debug PNG to
a fixed repo path: the plot asserted nothing, left an untracked file
behind and raced under parallel test workers. Its numerical flatness
assertion is unchanged.
* build: run the test suite in parallel with pytest-xdist
pytest -n auto in make test, make coverage and the CI tests job fans the
3241 tests out across every CPU core; pytest-xdist joins the dev
requirements. Each worker pins its numerical thread pools to one thread
(OMP/MKL/OpenBLAS/NumExpr/Accelerate) because with one worker per core
the nested BLAS pools only add contention: measured about 25% faster
wall-clock and about 40% less total CPU than unpinned workers on the
heavy DSP modules. Full suite on a 12-core host: 508 s to 138 s
(about 3.7x). pytest-cov combines the per-worker data files itself, so
the coverage.xml handed to Codecov and Sonar is unchanged (verified
identical 96% line coverage, same statement and miss counts). The
tests-perf job stays serial: it exists to exercise the numba-jitted
kernel, where per-worker JIT recompilation would dominate.
* docs: changelog entry for the parallel test suite
docs: modular-first documentation, metrology filter exports and the always-expanded sidebar (#210)
* feat: export octave_filter and octavefilter from phonometry.metrology
Move the octave_filter() convenience wrapper, its deprecated
octavefilter() alias and the design cache from phonometry/__init__.py
into phonometry.metrology.core, next to the OctaveFilterBank they wrap.
Both names are now re-exported by the metrology subpackage (matching
how every other deprecated alias propagates, e.g. normalizedfreq) and
the top level keeps re-exporting them, so existing imports are
unaffected and the deprecation warning behavior is unchanged.
The generated API reference now documents both under metrology.core,
and the API index and llms.txt quickstart present the modular
subpackage import as the primary form.
* docs: modular API in every snippet, with self-contained continuation blocks
Convert every remaining flat-API code block in docs/ and in the English
and Spanish site guides and theory pages to the modular subpackage form
(from phonometry import <domain> plus <domain>.func(...)): 228 blocks
per tree, including the figure <details> blocks the earlier retrofit
had left flat and the legacy module-path imports
(phonometry.occupational_exposure, phonometry.room_noise,
phonometry.noise_induced_hearing_loss).
Continuation blocks that leaned on names defined in a neighbouring
snippet now carry their own minimal setup (imports plus the defining
statements), so every runnable example executes exactly as printed; the
stale 'from the snippet above' comments are gone. Blocks that
deliberately reference the reader's own data (recording, audio_blocks,
device captures) keep their placeholder form.
Every block in the three trees was executed before and after the
rewrite: all previously-running blocks produce byte-identical output,
123 continuation blocks now run standalone, and the intentional
placeholders fail at the same documented point as before.
llms.txt and llms-full.txt regenerated (make llms) to pick up the
converted guide content and the modular quickstart.
* docs: drop the old-anchor map from the theory index
The three theory index pages (docs/theory.md and the EN and ES site
reference pages) still carried the migration table mapping every anchor
of the old single-page theory reference to its new host page. All
internal links were retargeted when the reference was split, so the
table only added noise; the per-domain section lists stay.
* docs: present the modular namespaces as the primary API surface
The API quick-reference intro and the README quickstart still framed
the flat top-level API as the primary surface. Both now lead with the
domain-subpackage import (the form used across the documentation) and
note that every public name remains importable from the top level. The
ph.-prefixed usage snippets in the quick table drop the alias prefix,
and the deprecation note for the pre-3.2 module paths is unchanged.
* site: fully expanded sidebar with typographic hierarchy
Every sidebar group and subgroup now ships open: the collapsed flags are
gone from astro.config.mjs and from the generated api-sidebar.mjs (the
emitter in scripts/generate_api_docs.py no longer writes them), so the
whole map of the docs is visible at once, including in the mobile
drawer where the domain groups used to render as closed chevrons. The
groups stay toggleable.
With ~150 entries visible, hierarchy comes from a small stylesheet
(src/styles/sidebar.css) instead of folding: top-level groups become
small uppercase eyebrow headers with a hairline separator, nested group
labels step down one text size, and links two levels deep tighten up so
the API reference stays compact. Starlight color tokens only, so light
and dark themes are covered; summary toggles and focus styles are
untouched (pa11y 30/30).
* docs: runnable wind-turbine snippets on the site pages
The docs-tree wind turbine guide already synthesized its inputs (the
band levels for the apparent sound power example and the narrowband
tonality spectrum), but the EN and ES site pages still consumed
undefined placeholder names. Port the same setup lines so the site
blocks execute standalone like their docs counterpart.
* docs: review fixes for the snippet sweep
Drop the calibrator-recording step comment that the self-containment
pass copied into the dBFS and RMS-vs-peak calibration blocks (their
code synthesizes a plain capture, not a calibrator tone), and remove
the docs index's mention of the theory anchor map that no longer
exists. llms-full.txt regenerated.
docs: modular-first documentation, metrology filter exports and the always-expanded sidebar (#210)
* feat: export octave_filter and octavefilter from phonometry.metrology
Move the octave_filter() convenience wrapper, its deprecated
octavefilter() alias and the design cache from phonometry/__init__.py
into phonometry.metrology.core, next to the OctaveFilterBank they wrap.
Both names are now re-exported by the metrology subpackage (matching
how every other deprecated alias propagates, e.g. normalizedfreq) and
the top level keeps re-exporting them, so existing imports are
unaffected and the deprecation warning behavior is unchanged.
The generated API reference now documents both under metrology.core,
and the API index and llms.txt quickstart present the modular
subpackage import as the primary form.
* docs: modular API in every snippet, with self-contained continuation blocks
Convert every remaining flat-API code block in docs/ and in the English
and Spanish site guides and theory pages to the modular subpackage form
(from phonometry import <domain> plus <domain>.func(...)): 228 blocks
per tree, including the figure <details> blocks the earlier retrofit
had left flat and the legacy module-path imports
(phonometry.occupational_exposure, phonometry.room_noise,
phonometry.noise_induced_hearing_loss).
Continuation blocks that leaned on names defined in a neighbouring
snippet now carry their own minimal setup (imports plus the defining
statements), so every runnable example executes exactly as printed; the
stale 'from the snippet above' comments are gone. Blocks that
deliberately reference the reader's own data (recording, audio_blocks,
device captures) keep their placeholder form.
Every block in the three trees was executed before and after the
rewrite: all previously-running blocks produce byte-identical output,
123 continuation blocks now run standalone, and the intentional
placeholders fail at the same documented point as before.
llms.txt and llms-full.txt regenerated (make llms) to pick up the
converted guide content and the modular quickstart.
* docs: drop the old-anchor map from the theory index
The three theory index pages (docs/theory.md and the EN and ES site
reference pages) still carried the migration table mapping every anchor
of the old single-page theory reference to its new host page. All
internal links were retargeted when the reference was split, so the
table only added noise; the per-domain section lists stay.
* docs: present the modular namespaces as the primary API surface
The API quick-reference intro and the README quickstart still framed
the flat top-level API as the primary surface. Both now lead with the
domain-subpackage import (the form used across the documentation) and
note that every public name remains importable from the top level. The
ph.-prefixed usage snippets in the quick table drop the alias prefix,
and the deprecation note for the pre-3.2 module paths is unchanged.
* site: fully expanded sidebar with typographic hierarchy
Every sidebar group and subgroup now ships open: the collapsed flags are
gone from astro.config.mjs and from the generated api-sidebar.mjs (the
emitter in scripts/generate_api_docs.py no longer writes them), so the
whole map of the docs is visible at once, including in the mobile
drawer where the domain groups used to render as closed chevrons. The
groups stay toggleable.
With ~150 entries visible, hierarchy comes from a small stylesheet
(src/styles/sidebar.css) instead of folding: top-level groups become
small uppercase eyebrow headers with a hairline separator, nested group
labels step down one text size, and links two levels deep tighten up so
the API reference stays compact. Starlight color tokens only, so light
and dark themes are covered; summary toggles and focus styles are
untouched (pa11y 30/30).
* docs: runnable wind-turbine snippets on the site pages
The docs-tree wind turbine guide already synthesized its inputs (the
band levels for the apparent sound power example and the narrowband
tonality spectrum), but the EN and ES site pages still consumed
undefined placeholder names. Port the same setup lines so the site
blocks execute standalone like their docs counterpart.
* docs: review fixes for the snippet sweep
Drop the calibrator-recording step comment that the self-containment
pass copied into the dBFS and RMS-vs-peak calibration blocks (their
code synthesizes a plain capture, not a calibrator tone), and remove
the docs index's mention of the theory anchor map that no longer
exists. llms-full.txt regenerated.
B, AU and D frequency weightings (ANSI S1.4-1983, IEC 61012, IEC 537) (#286)
* feat(metrology): add B, AU and D frequency weightings
Extend WeightingFilter / weighting_filter with three more curves next to
A/C/G/Z, all sharing the 1 kHz normalization, the high_accuracy
oversampled design and multichannel/stateful processing:
- B per ANSI S1.4-1983 Appendix C (Formula C2: the C weighting with one
more zero at the origin and a real pole at f5 = 158.48932 Hz),
documented as historical since IEC 61672-1 dropped it.
- AU per IEC 61012:1990: the A weighting cascaded with the six-pole U
low-pass of Table 2, for measuring audible sound in the presence of
ultrasound. The Table 2 poles reproduce every Table 1 nominal value
within 0.05 dB; the design oversamples toward 288 kHz because the U
roll-off acts up to 40 kHz.
- D per the withdrawn IEC 537:1976, from its published rational transfer
function. Cross-checked against SQAT (identical zeros/poles) and
librosa's independent closed form (within 0.002 dB, 10 Hz to 20 kHz),
and pinned against the IEC 537 table republished in NASA CR-3406
(Table SLD-I), which the response reproduces within 0.1 dB everywhere
except that table's 1600/2500 Hz cells.
verify_weighting_class now also attests B (ANSI Table IV design goals,
Table V Type 1/2 masks in the class slots, plus the between-nominals
sweep against the Appendix C analytic form) and AU (nominal A + U with
the Table 1 separate-unit tolerances and the subclause 2.2 explicit
values at 25/31.5/40 kHz). G and D are rejected with a clear error: no
class-structured tolerance tables exist for them.
The conformance report gains four rows: B against the strictest Type 0
mask at 48 kHz, AU over the full 10 Hz-40 kHz Table 1 range at 96 kHz,
and D against the published tabulated curve. Tests pin the transcribed
masks to independent reference_data copies and the realized responses
to the standards' tables at the nominal frequencies (31.5 Hz / 1 kHz /
8 kHz pins per curve, 50 Hz for D where its table starts).
The weighting guide (EN/ES/docs) gains a section on the three curves
with worked LD-vs-LA and ultrasound-rejection examples, the curve
family figure now draws all six curves, and the API reference and
CHANGELOG are updated accordingly.
* docs: regenerate the derived artifacts after rebasing onto main
* refactor(metrology): extract the analog weighting design from the constructor
Move the per-curve analog ZPK construction into a private _analog_design
method so the constructor reads linearly, and construct the filter outside
the pytest.raises block in the class-verifier rejection test.
feat: 2D FDTD wave simulation as a public API (phonometry.simulation) (#221)
* feat: 2D FDTD wave simulation as a public API (phonometry.simulation)
Promote the staggered-grid pressure-velocity FDTD engine behind the
documentation animations to the library, following the reference model of
Attenborough & Van Renterghem (2021) chapter 4: fdtd_simulation() returns a
frozen FDTDResult with per-probe pressure histories, optional field
snapshots and .plot(); the FDTD2D stepping engine gains rasterised rigid
obstacle masks and per-side locally reacting real-impedance boundaries
(Eqs. 4.33-4.35) alongside the existing sponge layers, plus the
arbitrary-waveform SignalSource; scripts/fdtd2d.py becomes a re-export shim
so the committed animations render from the library code (verified
bit-identical). Validated against analytic oracles: box and duct
eigenfrequencies, free-field arrival delay and cylindrical decay, the
rigid-wall image echo, the normal-incidence impedance reflection
coefficient, the discrete dispersion relation and second-order convergence.
* test: FDTD conformance checks (rigid-box mode and pulse arrival delay)
Two additive analytic anchors in the conformance report: the (1,1)
eigenfrequency of a 1.0 x 0.7 m rigid box against the closed form
f = (c/2) sqrt(1/lx^2 + 1/ly^2), and the probe-to-probe arrival delay of a
free-field pulse over 0.6 m of air against (r2 - r1)/c.
* docs: FDTD concept figure and pipeline diagram
A single-concept figure from the public result object (a barrier
diffraction snapshot with the source, probes and geometry overlaid next to
the two probe histories, PNG per the raster-figure policy) and an SVG
pipeline diagram of the solver (domain, geometry, sources, leapfrog
update, stability bound, frozen result), both in the four language/theme
variants; existing figures regenerate byte-identically.
* docs: FDTD simulation guide (EN/ES), API reference and indices
A didactic guide in the three documentation trees: what FDTD is (the
staggered leapfrog scheme and the Courant bound), the sources, probes,
obstacles and boundary conditions, when a wave-based simulation is worth
its cost, the 2D line-source limits versus 3D, and the numerical
dispersion resolution rule, with runnable snippets whose printed values are
verified. New Wave simulation sidebar group with its section landing pages,
the generated API pages for phonometry.simulation.fdtd, the curated
api-reference rows, the README index entry, the bibliography cross-links
and the changelog entry.
* refactor: harden FDTD input validation and decompose the solver setup
- Reject multidimensional SignalSource samples instead of silently
flattening them, and require integral counts (sponge_width, steps,
record_every, decimate, snapshot_every, absorbing_layer_cells) and
integral source/probe cell indices: fractional values previously
crashed later inside numpy indexing or silently truncated the cell.
- Reuse a preallocated divergence buffer in the pressure step instead of
allocating a full-grid array every time step.
- Split FDTD2D.__init__, _build_edges and fdtd_simulation into focused
helpers with identical semantics; build boundary specs with
dict.fromkeys; construct test inputs outside pytest.raises blocks.
- Correct the numerical-dispersion prose in the guide, docstring and API
quick table: 10 cells per wavelength bounds the on-axis error at about
1.6 % ((k dx)^2 / 24), about 1.4 % at the default cfl, not below 1 %,
matching Attenborough & Van Renterghem's directional analysis; list
duration as its own parameter and drop the em-dashes from the table.
The default numerical path is bit-identical: a six-run battery (rigid
box, absorbing CW, mixed impedance/obstacle/damping, engine
record/decimate, variable c/rho maps, cfl 0.99 duct) reproduces the
previous arrays exactly (array_equal) and the committed figures
regenerate without churn.
* docs: state the dispersion error sign and the heterogeneous-domain caveat
The leading-order dispersion figure is the magnitude of a negative signed
error, and in a domain with spatially varying sound speed the 10-cell rule
uses the smallest speed while slower cells run at a lower local Courant
number, nearer the small-Courant bound. Stated in the guide (EN/ES), the
GitHub page and the source docstring, with the API page regenerated.
chore: golden baseline and hotspot benchmarks (overhaul phase 0) (#161)
* chore: golden baseline and hotspot benchmarks (overhaul phase 0)
Groundwork for the 2026-07 overhaul (modularization + vectorization):
- scripts/bench.py: deterministic micro-benchmarks for the performance
hotspots (ECAC Doc 29 contour chain, rotorcraft hemisphere fill, PE
marching solver, ECMA-418-2 analysers, human-vibration weighting), a
--figures mode that times every figure function, and a --golden mode that
freezes the current numeric outputs.
- tests/golden_data.py: 542 golden values captured from the current
implementations (auto-generated, regenerated only on reviewed, intended
numeric changes).
- tests/test_golden_baseline.py: asserts every hotspot case reproduces its
golden array (rtol 1e-9; 1e-7 for the long-filter-chain ECMA analysers),
guarding the upcoming package reorganization and vectorized refactors
against numeric drift.
Baseline (Ryzen CI-class box): contour 10x8 grid 212 ms, ECMA loudness
2.72 s and tonality 2.99 s per 0.5 s signal, hemisphere 4.8 ms/query;
figure generation 123.6 s per language/theme variant, dominated by
loudness-models (36 s), tonality-roughness (22 s) and airport contour (21 s).
* style: wrap generated golden arrays to short lines
Review feedback: the single-line array literals (up to 4.8k characters)
hurt diff viewers and IDEs; the generator now wraps four values per line.
Golden values unchanged.
Room and building acoustics: ISO 18233, ISO 3382-1/2/3, ISO 16283-1 + ISO 717-1 (#81)
* feat: ISO 18233 sweep/MLS impulse-response acquisition
Add src/phonometry/room_ir.py implementing the ISO 18233:2006
deterministic-excitation IR front end: exponential sine sweep with exact
analytic phase (Annex B), linear spectral-division deconvolution with a
Farina inverse-filter option for harmonic separation (B.5), and
maximum-length-sequence generation (LFSR, orders 2-20) with circular
cross-correlation recovery (Annex A).
Validated against closed forms: known IIR bandpass recovered within 0.1 dB
in band (sweep and MLS), ideal chain to a band-limited delta, +3 dB SNR per
sweep-duration doubling (B.6), and all MLS orders verified as true
maximum-length sequences (autocorrelation L / -1).
433 passed, 12 skipped; ruff, mypy --strict and bandit clean.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 3382 room acoustic parameters from impulse responses
Add src/phonometry/room_acoustics.py implementing ISO 3382-1:2009 and
ISO 3382-2:2008 analysis of measured impulse responses:
- decay_curve(): Schroeder backward integration of the squared IR
(5.3.3, Eq. 1) with background-noise truncation at the noise/slope
crossing and exponential tail compensation (Eq. 3), broadband or per
IEC 61260 fractional-octave band.
- room_parameters(): per-band EDT (0/-10 dB, A.2.2), T20 (-5/-25 dB)
and T30 (-5/-35 dB) by least-squares fits (ISO 3382-2 Annex C),
clarity C50/C80 (Eq. A.10), definition D50 (Eq. A.11) and centre
time Ts (Eq. A.13), octave bands 125 Hz-4 kHz by default with a
one-third-octave option and a broadband mode.
- Validity flags per the 5.3.3 dynamic-range criterion (noise at least
evaluation range + 15 dB below the IR maximum: 25/35/45 dB) plus the
Annex B curvature indicator C = 100*(T30/T20 - 1).
Tests validate against closed forms for exponential decays (EDT = T20 =
T30 = T within 1 %; C_te = 10*lg(exp(13.8155*te/T) - 1); D50 =
1 - exp(-0.6908/T); Ts = T/13.8155), the exact C50/D50 relation
(Eq. A.12), double-slope decays (EDT < T20 < T30) and noise-driven
validity-flag behaviour, with tolerances far below the Table A.1 JNDs.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* chore: move figure/diagram generators to scripts/
generate_graphs.py and generate_diagrams.py join benchmark_filters.py
and gen_llms.py in scripts/, where dev tooling already lives. Updated
the Makefile graphs target, the sys.path bootstrap in the graph guard
tests and in generate_graphs.py itself (src/ is now one level up), the
CONTRIBUTING instructions and the theme-images.css pointer. Verified:
module loads from the new location and regenerating the SVG diagrams
produces byte-identical output.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 3382-3 open-plan office spatial metrics
Add open_plan_metrics() computing the ISO 3382-3:2012 single-number
quantities from a line of workstation measurements: spatial decay rate
D2,S and nominal speech level Lp,A,S,4m (Clause 6.2, Eq.5; 2-16 m
positions on a log-distance regression) and distraction/privacy
distances rD/rP (Clause 6.3; STI-vs-distance linear regression crossing
0,50 and 0,20). Returns a frozen OpenPlanResult; validates the minimum
of four positions (5.2.2) and equal array lengths.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* feat: ISO 16283-1 field insulation and ISO 717-1 weighted ratings
Add phonometry.insulation implementing field airborne sound insulation
(ISO 16283-1:2014) and single-number weighted ratings with C/Ctr
(ISO 717-1):
- airborne_insulation: per-band level difference D (Formula (1)),
standardized level difference DnT = D + 10 lg(T/T0) (Formula (2)) and
apparent sound reduction index R' = D + 10 lg(S/A), A = 0,16 V/T
(Formula (4)/(5)); energy-averages microphone positions (Formula (9)).
- weighted_rating: reference-curve shifting method (Clause 4.4, Table 3)
with the 32,0/10,0 dB unfavourable-deviation bound, plus C/Ctr from the
Table 4 spectra (Clause 4.5). Reference values, spectra and method are
identical in the 2013 and 2020 editions.
- energy_average_level: ISO 16283-1 Formula (9) helper.
Verified against the ISO 717-1 Annex C worked example (Rw(C;Ctr) =
30(-2;-3), unfavourable sum 31,8 dB) and exact-bound / tipping edge
cases. 20 new tests; make check green (484 passed, 12 skipped).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: room & building acoustics guide, theory, API and landing (EN)
New "Room & building acoustics" guide (docs/ + Starlight twin) covering the
full measurement chain: ISO 18233 swept-sine/MLS impulse-response acquisition,
ISO 3382-1/2 decay analysis and room parameters (EDT/T20/T30/C50/C80/D50/Ts),
ISO 3382-3 open-plan speech metrics, and ISO 16283-1 / ISO 717-1 field
insulation with weighted ratings. Adds the matching theory section (Schroeder
integration, regression windows, C/D/Ts, D2,S, DnT/R', reference-curve method),
API-reference rows for all new public names, sidebar entry, README highlight +
docs row, EN landing card, CHANGELOG entries, docs index row, and regenerated
llms-full.txt. All snippets validated; site build and make check green.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: Schroeder, ISO 717-1 rating and insulation-setup figures
Add three room/building-acoustics documentation figure sets (each in
en/es x light/dark):
- schroeder_decay: synthetic IR, Schroeder backward integration with
EDT/T20/T30 regressions and evaluation ranges (ISO 3382); annotated
values match room_parameters.
- insulation_rating: ISO 717-1 Annex C example with shifted reference
curve, unfavourable-deviation shading and Rw read at 500 Hz; Rw, C,
Ctr and the unfavourable sum come from weighted_rating.
- diagram_insulation_setup: ISO 16283-1 airborne setup plan view with
the normative minimum distances (clauses 7.6 and 7.2.2).
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: gemelas ES de acústica de salas y edificación
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: terminología UNE — ponderación temporal en lugar de balística (ES)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: markdown/README parity with the site + llms guide coverage
Scrub GitHub-unsafe display math (\, thin-spaces and escaped \{ \}) from
docs/intensity.md and docs/psychoacoustics.md — the only two guides that
missed the GitHub-safe math conversion of PRs #78/#79 — matching the safe
forms already used in docs/theory.md. Align calibration's sensitivity
symbol/formula to the site ($S$, single-line \qquad).
Extend scripts/gen_llms.py PAGES with the three omitted guides
(psychoacoustics, intensity, room-acoustics) and regenerate llms.txt /
llms-full.txt so AI-facing artifacts cover all 14 docs pages.
The guides/references were otherwise already in parity (didactic code
comments, theory sections, tables, and figures from PRs #77/#80 all
present); docs/README index and README highlights/table verified complete.
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: final-review findings — open_plan validation, limits rename, Farina caveat, MLS averaging test
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: ISO 18233 measurement-chain diagram and guide fixes
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: fix sweep_signal cross-reference in Farina caveat
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* fix: PR 81 review round 1 — inverse-filter guards, truncation threshold, IR input validation, docs pipeline wording
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
* docs: blank lines around CONTRIBUTING fence (MD031)
Claude-Session: https://claude.ai/code/session_013kkVt3nxi9svp1an28uHxf
Audit pass 9: the tests batch (#119)
* test: the tests batch of the audit
Audit batch 9 - the test layer hardens, with src/ numerics untouched:
reference_data drift closure: the test files that duplicated oracle
values as literals now import the shared constants (11 files), with a
rewritten module docstring describing the actual regime; the EN 12354-6
Annex E surfaces move into reference_data (test + conformance import
them); all 8 previously-dead constants are wired into asserts instead
of deleted.
Conformance: 90 -> 95 checks across 22 domains, all passing and
byte-stable. New: ECMA-418-1 critical-band and proximity anchors (new
Prominent discrete tones domain), an ISO 10534-2 synthesize->recover
identity, the ISO 717-2 Annex C.1 impact rating, and an ISO 18233 sweep
deconvolution vs freqz check - each with a tolerance-rationale comment
(new convention). The STI domain is now Speech transmission
(IEC 60268-16), the ISO 9613-1 checks live under outdoor propagation,
and the registry floors are realistic.
match= backfill: 98 bare pytest.raises(ValueError) sites now pin the
actual message; one test found passing via the wrong validation path is
fixed to exercise the intended error.
Performance: the eight heavy files drop from 203 s to 148 s and the
full suite from ~360 s to 288 s (1785 tests) - module-scoped STIPA
fixture, shortened property-test signals with measured-margin comments
(exact anchors kept at the length their tolerance needs, with the
measurements documented), and the zero-margin cached<uncached
performance assert gains a 1.5x margin (it flaked on CI by 0.06 ms).
Small gaps: 13 zips gain strict=True and 3 become itertools.pairwise;
StatefulWeightingFilter gains its three missing invalid-input tests;
utils.py verified as having no raising paths.
* test: show both chained values in the ISO 10534-1 check display
A |r| mismatch used to fail the check while displaying only matching
absorption values; the expected/computed strings now carry both, like
the ECMA-418-1 check does (CodeRabbit).
* test: escape the pipes in the ISO 10534-1 display strings
Unescaped | inside GFM table cells broke the CONFORMANCE.md rendering
(CodeRabbit); regenerated byte-stable.
refactor: reorganize the package into twelve domain subpackages (3.2) (#162)
* refactor: private core to _internal, module-path shims, _plot skeleton (C1)
Foundation of the 3.2 package reorganization:
- _validation/_types/_warnings/_levels_math/utils move (git mv) into the
private phonometry/_internal/ package; ~90 import sites retargeted.
- New phonometry/_compat.py: dynamic sys.modules shims keep every moved
public module path importable for one deprecation cycle (silent import,
DeprecationWarning on attribute access); generalizes and absorbs the
former loudness.py PEP 562 shim (its 3.1 wording preserved).
- _plotting.py moves (git mv) to phonometry/_plot/common.py; a silent
explicit re-export shim keeps the old private path and the 84 lazy
.plot() call sites working until each domain retargets them.
- _warn_renamed gains a 'since' parameter (3.2 for the package moves).
- tests: new test_package_architecture.py (ast edge whitelist +
fresh-interpreter subpackage imports); test_deprecated_aliases gains the
frozen 85-path pre-move snapshot and the _MOVED shim behavior test;
pytest pythonpath=tests for the upcoming mirrored test tree.
- .git-blame-ignore-revs scaffold (hashes appended at the end of Phase 1).
* refactor: metrology subpackage (C2)
core, filter_design, frequencies, parametric_filters, levels, calibration,
compliance and uncertainty move (git mv) into phonometry/metrology/ with a
curated re-export __init__; the flat API and the old module paths keep
working (facade re-exports + _compat shims). Uncertainty renderers carve out
of _plot/common.py into _plot/metrology.py; the 14 metrology test files move
to tests/metrology/ and deep imports across tests/scripts point at the new
canonical paths.
* refactor: psychoacoustics subpackage (C3)
The thirteen psychoacoustics modules (Zwicker/ECMA/Moore-Glasberg loudness,
contours, sharpness, tonality, roughness, fluctuation strength, annoyance,
tonal audibility, private Zwicker tables) move into
phonometry/psychoacoustics/; renderers carve into _plot/psychoacoustics.py;
twelve test files move to tests/psychoacoustics/. The phonometry.loudness
alias now resolves to the relocated canonical module in a single hop.
* refactor: hearing and emission subpackages (C4+C5)
hearing/ gains threshold (renamed from hearing.py), noise-induced hearing
loss, occupational exposure, SII and STI; emission/ gains the ISO 3740
sound-power family, ISO 9614 intensity and ISO/TS 7849 vibration-based
power. Renderers carve into _plot/hearing.py and _plot/emission.py; the
eleven domain test files move under tests/hearing/ and tests/emission/.
* refactor: materials, room and building subpackages (C6-C8)
materials/ collects ISO 354/11654/12999-2 absorption, scattering-diffusion,
road absorption, impedance tube, airflow resistance and EN 29052-1 dynamic
stiffness; room/ the room-acoustics, impulse-response, open-plan, room-noise,
reverberation-prediction and EN 12354-6 modules; building/ the eleven-module
EN 12354 / ISO 717 / ISO 16283 insulation family with EN 15657 and
EN 12354-5 structure-borne sound. Renderers carve into the matching _plot
modules; twenty-six test files move under their domain directories.
* fix: complete the building plot TYPE_CHECKING imports (C8 follow-up)
* refactor: vibration and environmental subpackages (C9+C10)
vibration/ collects human vibration (ISO 8041-1/2631/5349), multiple-shock
(ISO 2631-5), mechanical mobility (ISO 7626-1) and transfer stiffness
(ISO 10846); environmental/ collects the renamed rating (Lden/Ldn) and
measurement (ISO 1996-2) modules plus outdoor propagation (ISO 9613-2),
air absorption (ISO 9613-1), wind-turbine noise (IEC 61400-11) and NT
ACOU 112 impulse prominence. Renderers carve into their _plot modules and
ten test files move under the domain directories.
* refactor: aircraft, underwater and electroacoustics subpackages (C11-C13)
aircraft/ collects EPNL, the renamed atmospheric absorption, ECAC Doc 29
airport contours and ECAC Doc 32 rotorcraft hemispheres; underwater/ the
renamed acoustics/propagation/sound_speed modules with ship noise, pile
driving, ambient noise, sonar equation, seabed reflection and the numerical
solvers; electroacoustics/ distortion and frequency response. Renderers
carve into their _plot modules; seventeen test files move under the domain
directories. All twelve domain subpackages are now in place.
* refactor: finalize the 3.2 package reorganization (C14)
- api-reference gains a Namespaces section (the twelve subpackages with
scope and the 'from phonometry import aircraft as air' idiom); README
quick-start notes the namespaces; CHANGELOG records the added namespaces,
the deprecated flat module paths and the pickle note.
- plot_excitation moves from _plot/common.py to _plot/room.py and the
facade imports it from there; _plot/common.py now holds only shared
helpers and infrastructure.
- hearing/ and environmental/ package __init__ re-export the full public
surface of their renamed modules (threshold, rating) so the pre-move
package-path imports stay silent, as promised by the migration contract.
- SonarCloud cpd exclusion follows core.py to metrology/; llms.txt
regenerated; .git-blame-ignore-revs lists the move commits and the local
blame config registers it.
Full suite 2647 passed; conformance 194/194; golden baseline byte-stable;
import time unchanged vs main (1.09 s vs 1.17 s).
* fix: re-export the measurement surface on phonometry.environmental
Subagent-review finding: the generated environmental/__init__.py missed the
ISO 1996-2 measurement module (its facade import block was momentarily
path-corrupted when the subpackage exports were collected), so
phonometry.environmental.assess_tonal_audibility and 15 sibling names
raised AttributeError while every other domain namespace was complete. Adds
the missing re-exports (including EnvironmentalMeasurementWarning on the
domain namespace) and a new architecture invariant test asserting that
every name the facade imports from a domain submodule is reachable on that
subpackage, so this class of gap cannot recur.
* test: forbid absolute self-imports in the architecture check
Review feedback: the ast edge extraction only saw relative imports, so an
absolute 'from phonometry.x import y' inside the package would bypass the
cross-package whitelist. Such imports now fail the architecture test
directly (the codebase has none; relative imports are the convention).
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: normative ISO 717 rating report (.report() PDF fiche) (#238)
* feat(building): ISO 717 Annex C rating report (.report() -> PDF)
Add a report(path) method that renders an ISO 717 Annex C sound-insulation
fiche to a one-page PDF. WeightedRatingResult.report() renders the airborne
ISO 717-1 layout (the Rw (C; Ctr) statement, the measured-versus-shifted
reference plot and the Table C.1 evaluation table with the sum of unfavourable
deviations), and ImpactRatingResult.report() renders the impact ISO 717-2
counterpart with the Ln,w (CI) statement and the opposite deviation sign.
SoundReductionResult.report() is a convenience that rates the predicted R(f)
and writes its fiche in one call.
Rendering uses reportlab, added as the optional phonometry[report] extra so it
stays out of the runtime dependencies; a missing reportlab raises a clear
ImportError with the install command, mirroring the matplotlib guard behind
.plot(). The rating results gain a quantity field ("airborne"/"impact") that
selects the report labels and standard reference.
* test(building): verify the ISO 717 fiche against the Annex C worked examples
Pins the report content to the printed worked examples: ISO 717-1 Annex C
Table C.1 (Rw(C;Ctr) = 30(-2;-3) dB, unfavourable sum 31,8 dB, reference shifted
by -22 dB and the per-band deviations) and ISO 717-2 Annex C Table C.1
(Ln,w = 79 dB, CI = -11 dB, sum 28,0 dB).
* feat(building): one-page ISO 717 fiche with a vector (SVG) plot
Embeds the ISO 717 plot as vector graphics via svglib (svg2rlg, text as paths)
so the curve stays crisp at any resolution, tightens the layout to a single
A4 page, and reduces the single-number statement line. Adds svglib to the
optional 'report' extra.
* feat(building): two-column ISO 717 fiche (plot beside condensed table)
Places the vector plot and the condensed ISO 717 Annex C evaluation table
side by side (datasheet layout) and drops the plot's redundant title, since
the header already states the single number and the unfavourable-deviation sum.
* feat(building): accredited-laboratory ISO 717 report fiche + ReportMetadata
Rework the ISO 717 `.report()` PDF into an accredited-laboratory test-report
layout modelled on ISO 10140-2 / ISO 16283 lab reports rated per ISO 717:
a standard-basis line, an optional metadata header block, the one-third-octave
table beside the measured-versus-shifted-reference plot, a boxed single-number
result, an optional PASS/FAIL verdict row and a footer with a fixed disclaimer.
Report metadata is now a shared frozen `ReportMetadata` dataclass (specimen,
client, room and climatic conditions, laboratory identity and an optional
requirement); all fields optional and only the supplied ones render, so the
same object drives a full accredited fiche or, with metadata=None, a prediction
fiche. `report()` gains `metadata` and `verbose` keywords; `verbose=True` keeps
the ISO 717 Annex C evaluation columns. The rating results keep their vector
(SVG) plot. `ReportMetadata` is exported at the top level and documented.
* test: add pypdf dev dependency for the report one-page assertion
* feat(building): enlarge the report plot and narrow the table to match the accredited reference
* feat(building): fixed report plot axis, legend above, per-room climate metadata
* feat: generate committed example ISO 717 report fiches (make reports)
Adds scripts/generate_reports.py and a 'reports' Make target that render the
example .report() fiches into .github/reports/ (airborne and impact), linked
from the documentation as normative-report examples, mirroring the figure
pipeline. Not byte-checked in CI (the vector plot differs by ~1 ULP across
CPUs); tests/test_generate_reports.py checks the generator still yields valid
one-page PDFs.
* test(architecture): treat _report as a rendering leaf like _plot
The report renderer mirrors _plot: domain modules import it lazily inside
.report() and it references the rating result types only under TYPE_CHECKING,
so the whitelist exempts the _report edges exactly as it does _plot, and the
TYPE_CHECKING-only guarantee now covers _report too.
* refactor(_report): address Sonar findings on the ISO 717 renderer
Imports reportlab names directly per function instead of via a dict alias
(clears the capitalized-local warnings), extracts the result box and verdict
into helpers to lower the render function's cognitive complexity, switches the
doc kwargs to a dict literal, and hoists the path construction out of the
report tests' pytest.raises blocks.
* fix(_report): address PR review (XML escaping, temperature/humidity ranges, extras)
- Escape user-supplied metadata strings before rendering them in reportlab
Paragraphs so a '&', '<' or '>' can no longer crash the XML parser.
- ReportMetadata now validates by physical range: dimensions/mass/volume/
pressure/requirement stay strictly positive, temperatures may be zero or
negative (cold test conditions), relative humidity must be 0..100 %.
- Add svglib to the 'full' extra (it is a real report dependency).
- Use a comma decimal separator consistently across the fiche, enclose the
verbose sum row in the table border, and assert equal-length band arrays.
- Rename the render-leaf architecture test for its _plot + _report scope.
* fix(_report): use a period decimal separator (English fiche)
The fiche text is English, so numbers use a period separator throughout
(metadata grid and one-third-octave table alike), matching accredited English
lab reports. A locale-aware separator will come with the planned EN/ES i18n.
* docs: regenerate API reference after the ReportMetadata docstring update
* refactor(_report): lower ReportMetadata validation complexity
Extract a _require helper so __post_init__ is three short field loops,
dropping the cognitive complexity flagged by Sonar.
* docs(_report): describe ReportMetadata validation accurately in the summary
The class summary now states the actual per-range validation (positive
dimensions/mass/volume/pressure/requirement, finite temperatures allowing 0 C
or below, humidity within 0..100 %) instead of a blanket strictly-positive
claim; the :raises: note and __post_init__ behaviour are unchanged.
Audit pass 9: the tests batch (#119)
* test: the tests batch of the audit
Audit batch 9 - the test layer hardens, with src/ numerics untouched:
reference_data drift closure: the test files that duplicated oracle
values as literals now import the shared constants (11 files), with a
rewritten module docstring describing the actual regime; the EN 12354-6
Annex E surfaces move into reference_data (test + conformance import
them); all 8 previously-dead constants are wired into asserts instead
of deleted.
Conformance: 90 -> 95 checks across 22 domains, all passing and
byte-stable. New: ECMA-418-1 critical-band and proximity anchors (new
Prominent discrete tones domain), an ISO 10534-2 synthesize->recover
identity, the ISO 717-2 Annex C.1 impact rating, and an ISO 18233 sweep
deconvolution vs freqz check - each with a tolerance-rationale comment
(new convention). The STI domain is now Speech transmission
(IEC 60268-16), the ISO 9613-1 checks live under outdoor propagation,
and the registry floors are realistic.
match= backfill: 98 bare pytest.raises(ValueError) sites now pin the
actual message; one test found passing via the wrong validation path is
fixed to exercise the intended error.
Performance: the eight heavy files drop from 203 s to 148 s and the
full suite from ~360 s to 288 s (1785 tests) - module-scoped STIPA
fixture, shortened property-test signals with measured-margin comments
(exact anchors kept at the length their tolerance needs, with the
measurements documented), and the zero-margin cached<uncached
performance assert gains a 1.5x margin (it flaked on CI by 0.06 ms).
Small gaps: 13 zips gain strict=True and 3 become itertools.pairwise;
StatefulWeightingFilter gains its three missing invalid-input tests;
utils.py verified as having no raising paths.
* test: show both chained values in the ISO 10534-1 check display
A |r| mismatch used to fail the check while displaying only matching
absorption values; the expected/computed strings now carry both, like
the ECMA-418-1 check does (CodeRabbit).
* test: escape the pipes in the ISO 10534-1 display strings
Unescaped | inside GFM table cells broke the CONFORMANCE.md rendering
(CodeRabbit); regenerated byte-stable.
docs: modular-first documentation, metrology filter exports and the always-expanded sidebar (#210)
* feat: export octave_filter and octavefilter from phonometry.metrology
Move the octave_filter() convenience wrapper, its deprecated
octavefilter() alias and the design cache from phonometry/__init__.py
into phonometry.metrology.core, next to the OctaveFilterBank they wrap.
Both names are now re-exported by the metrology subpackage (matching
how every other deprecated alias propagates, e.g. normalizedfreq) and
the top level keeps re-exporting them, so existing imports are
unaffected and the deprecation warning behavior is unchanged.
The generated API reference now documents both under metrology.core,
and the API index and llms.txt quickstart present the modular
subpackage import as the primary form.
* docs: modular API in every snippet, with self-contained continuation blocks
Convert every remaining flat-API code block in docs/ and in the English
and Spanish site guides and theory pages to the modular subpackage form
(from phonometry import <domain> plus <domain>.func(...)): 228 blocks
per tree, including the figure <details> blocks the earlier retrofit
had left flat and the legacy module-path imports
(phonometry.occupational_exposure, phonometry.room_noise,
phonometry.noise_induced_hearing_loss).
Continuation blocks that leaned on names defined in a neighbouring
snippet now carry their own minimal setup (imports plus the defining
statements), so every runnable example executes exactly as printed; the
stale 'from the snippet above' comments are gone. Blocks that
deliberately reference the reader's own data (recording, audio_blocks,
device captures) keep their placeholder form.
Every block in the three trees was executed before and after the
rewrite: all previously-running blocks produce byte-identical output,
123 continuation blocks now run standalone, and the intentional
placeholders fail at the same documented point as before.
llms.txt and llms-full.txt regenerated (make llms) to pick up the
converted guide content and the modular quickstart.
* docs: drop the old-anchor map from the theory index
The three theory index pages (docs/theory.md and the EN and ES site
reference pages) still carried the migration table mapping every anchor
of the old single-page theory reference to its new host page. All
internal links were retargeted when the reference was split, so the
table only added noise; the per-domain section lists stay.
* docs: present the modular namespaces as the primary API surface
The API quick-reference intro and the README quickstart still framed
the flat top-level API as the primary surface. Both now lead with the
domain-subpackage import (the form used across the documentation) and
note that every public name remains importable from the top level. The
ph.-prefixed usage snippets in the quick table drop the alias prefix,
and the deprecation note for the pre-3.2 module paths is unchanged.
* site: fully expanded sidebar with typographic hierarchy
Every sidebar group and subgroup now ships open: the collapsed flags are
gone from astro.config.mjs and from the generated api-sidebar.mjs (the
emitter in scripts/generate_api_docs.py no longer writes them), so the
whole map of the docs is visible at once, including in the mobile
drawer where the domain groups used to render as closed chevrons. The
groups stay toggleable.
With ~150 entries visible, hierarchy comes from a small stylesheet
(src/styles/sidebar.css) instead of folding: top-level groups become
small uppercase eyebrow headers with a hairline separator, nested group
labels step down one text size, and links two levels deep tighten up so
the API reference stays compact. Starlight color tokens only, so light
and dark themes are covered; summary toggles and focus styles are
untouched (pa11y 30/30).
* docs: runnable wind-turbine snippets on the site pages
The docs-tree wind turbine guide already synthesized its inputs (the
band levels for the apparent sound power example and the narrowband
tonality spectrum), but the EN and ES site pages still consumed
undefined placeholder names. Port the same setup lines so the site
blocks execute standalone like their docs counterpart.
* docs: review fixes for the snippet sweep
Drop the calibrator-recording step comment that the self-containment
pass copied into the dBFS and RMS-vs-peak calibration blocks (their
code synthesizes a plain capture, not a calibrator tone), and remove
the docs index's mention of the theory anchor map that no longer
exists. llms-full.txt regenerated.
feat: theoretical panel sound reduction, radiation efficiency and point mobilities (#233)
* feat: theoretical panel sound insulation, plate radiation efficiency and point mobilities
Predict the airborne sound reduction index R(f) of building elements from
their physical properties, closing the EN 12354 chain from panel physics to
the single-number rating without a laboratory measurement.
building.panel_transmission adds the mass law and coincidence dip by Sharp's
method (single_panel_transmission_loss), the mass-spring-mass double wall with
an optional porous cavity fill (double_wall_transmission_loss,
mass_spring_mass_resonance), all from Bies, Hansen & Howard 5e Section 7.2.
building.aperture_transmission adds transmission through slits (Gomperts) and
circular holes (Wilson & Soroka) and their area-weighted composition with the
wall (Hopkins Section 4.3.10, Eq. 4.92), so a bare opening caps the composite
at 10 lg(S/Sa).
vibration.radiation_efficiency predicts the Leppington/Maidanik radiation
efficiency of a bending plate (Hopkins Section 2.9), the radiation factor
ISO 7849 otherwise measures; vibration.point_mobility adds the closed-form
point impedances and mobilities of infinite plates, beams and rods and the
injected power (Cremer, Heckl & Petersson 3e Table 5.1).
Every prediction exposes .plot() and, for R(f), .rating() (ISO 717-1). Anchored
by closed-form oracles (exact mass law, coincidence and mass-air-mass
frequencies, aperture area limit) and digitized curves from the source books.
* docs: panel sound insulation guide, theory, conformance, figure and API reference
Add the Predicting Panel Sound Insulation guide (EN/ES), the concept figure
(single/double wall, radiation efficiency, composite aperture), theory sections
in the rooms-buildings and vibration references, eleven conformance checks
(mass law slope, coincidence and mass-air-mass frequencies, aperture area
limit, slit resonance, plate/beam mobilities), the generated API reference
pages and the curated api-reference table, llms.txt and the CHANGELOG entry.
* refactor: address PR #233 review (Sonar S1192, slit numerical stability)
- Extract the duplicated 'Frequency [Hz]' axis label (_plot/building.py) and the
'frequency must be positive' message (panel_transmission.py) into module
constants (SonarQube S1192).
- Reformulate the Gomperts slit transmission coefficient (Eq. 4.99) by
multiplying numerator and denominator by cos^2(Ke): mathematically identical
(tau unchanged to 2.5e-16) but finite where cos(Ke) crosses zero, instead of
dividing by it.
- Guard slit_resonance_frequencies against a slit so wide that the effective
depth d + 2e turns non-positive, raising a clear error instead of a NaN.
- Propagate the earlier em-dash prose fix into the embedded llms-full.txt.
- Add regression tests for the wide-slit guard and the cos(Ke)=0 sweep.
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