feat: image-source room impulse responses and steady-state room field#229
Conversation
Add two rectangular-room field models to phonometry.room. image_source (A1) synthesises the room impulse response of a shoebox by mirroring a point source in its walls (Kuttruff Room Acoustics 4.1, Eqs. 4.4-4.6; Vorlander Auralization 11.4; Allen & Berkley 1979): the direct sound plus one delayed, attenuated impulse per image, amplitude prod(R_wall^count) * exp(-m r / 2) / (4 pi r) with the pressure reflection factor R = sqrt(1 - alpha) and delay r / c. It returns a broadband or per-octave-band RIR plus the exact sub-sample reflection table (times, distances, orders, amplitudes, image positions), with audible_image_count (Eq. 9.23) and reflection_density (Eq. 4.6) helpers and a reflectogram .plot(). steady_field (A4) predicts the steady-state level Lp = Lw + 10 lg(Q/(4 pi r^2) + 4/R) with the room constant R = S alpha/(1 - alpha) (Bies Engineering Noise Control 6.43-6.44), the critical distance sqrt(Q R / 16 pi) and the Schroeder frequency 2000 sqrt(T/V) (Kuttruff 3.44, 5.44). SteadyFieldResult carries the direct/reverberant/total split versus distance with a .plot().
Pin the exact reflection geometry (direct and first-reflection delay and amplitude), the audible image count (Kuttruff Eq. 9.23), the reflection density (Eq. 4.6), per-band and per-wall assembly, and the domain guards. Cross-check the Eyring reverberation time recovered from the synthetic decay in the near-cubic limit, and use the 2D FDTD solver of phonometry.simulation as an independent oracle for the rigid-wall echo delay and the uniform-damping T60. Verify the closed-form room constant, the critical distance as the direct/reverberant crossover, the Schroeder frequency and the steady-state level.
Add seven Room-acoustics anchors: the image-source direct-sound amplitude and delay, the audible image count (Kuttruff Eq. 9.23), the reflection density (Eq. 4.6), the room constant (Bies Eq. 6.44), the critical distance as the direct/reverberant crossover, the Schroeder frequency (Kuttruff Eq. 3.44) and the steady-state level (Bies Eq. 6.43). Register the two new modules in the API taxonomy. 319/319 checks pass; the report regenerates byte-identically.
Add the image-source room-impulse-response reflectogram of a 7x5x3 m room: the direct sound with the reflection cloud coloured by reflection order, decaying under the 1/r spreading envelope. Four language/theme variants; Spanish strings added to the translation table. Committed as PNG (the dense reflection cloud is far lighter than the vector form).
New guide across the three documentation trees (GitHub docs, theory chapter and the EN/ES site guides): the image-source room impulse response with its reflection-count decomposition, the audible-image count and reflection density, the reflectogram figure with its double snippet, and the steady-state field with the room constant, critical distance and Schroeder frequency. Cross-linked from the room-acoustics section pages and the sidebar. Regenerates the Starlight API reference for the two new modules, extends the curated API table, the README highlights, the bibliography (Vorlander, Allen & Berkley) and the llms files, and records the change in the changelog.
There was a problem hiding this comment.
Sorry @jmrplens, your pull request is larger than the review limit of 150000 diff characters
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds image-source room impulse-response synthesis and steady-state room-field calculations, with public APIs, plotting helpers, conformance checks, bilingual guides, API references, navigation, and generated documentation. ChangesRoom acoustics models
Estimated code review effort: 5 (Critical) | ~90 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant RoomModels
participant Results
participant Plotting
Caller->>RoomModels: Provide room and acoustic parameters
RoomModels->>Results: Produce RIR or steady-field result
Results->>Plotting: Render reflectogram or SPL curves
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request adds image-source room impulse response and steady-state room field predictions to the phonometry.room module, along with documentation, tests, and plotting utilities. The review feedback identifies several key improvements: resolving a potential crash in the reflectogram plot when max_order=0, ensuring reflection_density correctly handles negative times, renaming the confusing rc variable to r_const to avoid confusion with critical distance, adding finiteness checks to positive-value validations, and anchoring all elements in test assertions rather than just the endpoints.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| cbar = ax.figure.colorbar(sc, ax=ax, pad=0.02) | ||
| cbar.set_label("Reflection order") |
There was a problem hiding this comment.
If max_order=0 is passed to image_source_rir, there are no reflections and ref_mask is entirely False. In this case, sc (the scatter plot) will be empty with undefined limits, causing ax.figure.colorbar(sc) to raise a ValueError or TypeError at runtime. Wrapping the colorbar creation in a check like if np.any(ref_mask): avoids this crash.
| cbar = ax.figure.colorbar(sc, ax=ax, pad=0.02) | |
| cbar.set_label("Reflection order") | |
| if np.any(ref_mask): | |
| cbar = ax.figure.colorbar(sc, ax=ax, pad=0.02) | |
| cbar.set_label("Reflection order") |
There was a problem hiding this comment.
Applied in d3c346e: plot() crashed on a direct-only reflectogram (empty scatter, no colorbar limits); guarded with a check for any reflections plus a regression test.
| rc = np.asarray(room_constant, dtype=np.float64) | ||
| directivity = require_positive(directivity, "directivity") | ||
| if np.any(r <= 0.0) or not np.all(np.isfinite(r)): | ||
| raise ValueError("'distance' must be positive and finite.") | ||
| if np.any(rc <= 0.0) or not np.all(np.isfinite(rc)): | ||
| raise ValueError("'room_constant' must be positive and finite.") | ||
| bracket = directivity / (4.0 * np.pi * r**2) + 4.0 / rc |
There was a problem hiding this comment.
Using rc as the variable name for room_constant inside steady_state_spl is highly confusing because rc is the standard abbreviation for critical distance (which is also computed and used in this module). Renaming it to R or r_const would align with standard acoustic notation and prevent confusion with critical_distance. Additionally, ensure that validation checks for positive values also check for finiteness to reject NaN and infinity.
| rc = np.asarray(room_constant, dtype=np.float64) | |
| directivity = require_positive(directivity, "directivity") | |
| if np.any(r <= 0.0) or not np.all(np.isfinite(r)): | |
| raise ValueError("'distance' must be positive and finite.") | |
| if np.any(rc <= 0.0) or not np.all(np.isfinite(rc)): | |
| raise ValueError("'room_constant' must be positive and finite.") | |
| bracket = directivity / (4.0 * np.pi * r**2) + 4.0 / rc | |
| r_const = np.asarray(room_constant, dtype=np.float64) | |
| directivity = require_positive(directivity, "directivity") | |
| if np.any(r <= 0.0) or not np.all(np.isfinite(r)): | |
| raise ValueError("'distance' must be positive and finite.") | |
| if np.any(r_const <= 0.0) or not np.all(np.isfinite(r_const)): | |
| raise ValueError("'room_constant' must be positive and finite.") | |
| bracket = directivity / (4.0 * np.pi * r**2) + 4.0 / r_const |
References
- When validating that numeric arrays or maps contain only strictly positive values, ensure they are also validated as finite (rejecting NaN and infinity), as a simple check like any(x <= 0) will fail to catch non-finite values.
There was a problem hiding this comment.
Rename applied in d3c346e (rc was the critical distance elsewhere in the module, confusing); the finiteness checks you suggested were already present, so that part is redundant.
| volume = require_positive(volume, "volume") | ||
| speed_of_sound = require_positive(speed_of_sound, "speed_of_sound") | ||
| t = np.asarray(time, dtype=np.float64) | ||
| out = 4.0 * np.pi * speed_of_sound**3 * t**2 / volume |
There was a problem hiding this comment.
For negative times, the physical reflection density is zero. Currently, passing a negative time to reflection_density returns a positive value because of the squaring operation (t**2). Using np.maximum(t, 0.0) ensures the density is correctly zero for any negative time inputs.
| out = 4.0 * np.pi * speed_of_sound**3 * t**2 / volume | |
| out = 4.0 * np.pi * speed_of_sound**3 * np.maximum(t, 0.0)**2 / volume |
There was a problem hiding this comment.
Applied in d3c346e: reflection_density clamps t to zero before squaring, so negative times no longer produce a spurious positive density.
| energies = np.sum(res.ir**2, axis=1) | ||
| assert energies[0] > energies[2] |
There was a problem hiding this comment.
When asserting a sequence of values (like the decaying energies across bands), it is best practice to assert the expected relations or values for all elements in the sequence rather than just the endpoints. This ensures each data point is individually anchored.
| energies = np.sum(res.ir**2, axis=1) | |
| assert energies[0] > energies[2] | |
| energies = np.sum(res.ir**2, axis=1) | |
| assert energies[0] > energies[1] > energies[2] |
References
- When writing tests for a sequence of values, assert the expected values for all elements in the sequence rather than just the endpoints, ensuring each data point is individually anchored.
There was a problem hiding this comment.
Applied in d3c346e: the assertion now checks the full monotonic chain across the three bands.
| energies = np.sum(res.ir**2, axis=1) | ||
| assert energies[0] > energies[-1] |
There was a problem hiding this comment.
Similar to the other per-band test, we should assert the expected monotonic relationship across all elements in the sequence of energies rather than just the endpoints.
| energies = np.sum(res.ir**2, axis=1) | |
| assert energies[0] > energies[-1] | |
| energies = np.sum(res.ir**2, axis=1) | |
| assert np.all(np.diff(energies) < 0.0) |
References
- When writing tests for a sequence of values, assert the expected values for all elements in the sequence rather than just the endpoints, ensuring each data point is individually anchored.
There was a problem hiding this comment.
Applied in d3c346e: the assertion now checks strict monotonic decrease across all six bands.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #229 +/- ##
==========================================
- Coverage 96.13% 96.10% -0.03%
==========================================
Files 132 134 +2
Lines 17529 17831 +302
==========================================
+ Hits 16851 17137 +286
- Misses 678 694 +16 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/references.md`:
- Around line 374-375: Update the bibliography sentence around “porous-material
coefficient sets” by adding a verb before “its Chapter 6,” so the sentence
clearly states that Chapter 6 covers the steady-state room field and room
constant.
In `@docs/room-image-sources.md`:
- Around line 37-39: Add an appropriate language identifier, such as text or
latex, to the opening fence of every fenced block in docs/room-image-sources.md,
including the blocks surrounding the displayed equation and the additional
ranges noted in the review, so all fences satisfy markdownlint MD040.
In `@docs/theory-rooms-buildings.md`:
- Line 81: The Schroeder-frequency descriptions should present fs as a heuristic
modal-to-diffuse crossover rather than a hard validity boundary. In
docs/theory-rooms-buildings.md:81, soften the claim that diffuse-field relations
fail below fs and retain room-specific validation guidance; in
docs/room-image-sources.md:161-169, likewise replace categorical below/above
applicability claims with qualified transition-estimate wording.
- Line 71: In docs/theory-rooms-buildings.md:71, describe T as the “Eyring
reverberation time,” not the Eyring rate. In docs/room-image-sources.md:95-102,
reword the fitted decay-slope description to state that it recovers or yields
the Eyring reverberation time, clearly distinguishing the slope rate from the
derived T value.
In `@llms-full.txt`:
- Around line 4874-4883: Guard the T30 output in the room-parameter example by
checking params.t30_valid before accessing params.t30[0]. When the flag is
false, print a clear fallback or instruct readers to increase max_order; only
read and print the T30 value when it is valid.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3873f143-800c-4b05-8ae2-c62dc2d02921
⛔ Files ignored due to path filters (5)
.github/images/image_source_reflectogram.pngis excluded by!**/*.png.github/images/image_source_reflectogram_dark.pngis excluded by!**/*.png.github/images/image_source_reflectogram_es.pngis excluded by!**/*.png.github/images/image_source_reflectogram_es_dark.pngis excluded by!**/*.pngsite/src/generated/api-sidebar.mjsis excluded by!**/generated/**
📒 Files selected for processing (29)
CHANGELOG.mdREADME.mddocs/CONFORMANCE.mddocs/README.mddocs/api-reference.mddocs/references.mddocs/room-image-sources.mddocs/theory-rooms-buildings.mdllms-full.txtllms.txtscripts/api_taxonomy.pyscripts/conformance_report.pyscripts/generate_graphs.pyscripts/generate_llms.pysite/astro.config.mjssite/src/content/docs/es/guides/room-image-sources.mdxsite/src/content/docs/es/guides/sections/room-acoustics.mdsite/src/content/docs/guides/room-image-sources.mdxsite/src/content/docs/guides/sections/room-acoustics.mdsite/src/content/docs/reference/api/index.mdsite/src/content/docs/reference/api/rooms/image-source.mdsite/src/content/docs/reference/api/rooms/steady-field.mdsrc/phonometry/__init__.pysrc/phonometry/_plot/room.pysrc/phonometry/room/__init__.pysrc/phonometry/room/image_source.pysrc/phonometry/room/steady_field.pytests/room/test_image_source.pytests/room/test_steady_field.py
Numerical conformance report✅ 319/319 conformance checks pass across 39 domains and 193 standards - filters class 1 - weightings within IEC 61672-1 class 1. Each row pins a standard clause to its expected normative value and the value the library computes. Every section below is collapsible and stays collapsed while all of its rows pass; a section with any failing row opens automatically. ✅ Numerical validation - filters & weightings: class showcase (IEC 61260-1 · IEC 61672-1 · ISO 7196)IEC 61260-1:2014 class per filter architecture (order 6, one-third-octave, 100 Hz-10 kHz, fs = 48 kHz). For each architecture the table shows, at its binding band, the measured relative attenuation and the class-1 limit it must clear, so the number and the range it must sit in are both visible. A positive margin means the acceptance limits are met with that much room.
Only Butterworth (the library default) and Chebyshev-II are class-compliant architectures. Chebyshev-I and elliptic trade the mask for passband ripple, and Bessel for a maximally-flat group delay (soft rolloff); they cannot satisfy the IEC 61260-1 Class 1/2 attenuation mask by construction, so they are labelled By design - this is expected, not a failure or regression. Frequency-weighting conformance (A/C: IEC 61672-1 Table 3; G: ISO 7196 A.3). The max deviation from nominal is informational (it falls at a frequency extreme where the tolerance is widest and asymmetric); compliance is judged at the binding frequency - the one with the least headroom - where the deviation, the applicable tolerance band and the headroom are shown together.
✅ Filters & weightings: 100% (7/7)
✅ Levels & dosimetry: 100% (6/6)
✅ Room acoustics: 100% (12/12)
✅ Psychoacoustics: 100% (12/12)
✅ Speech transmission (IEC 60268-16): 100% (9/9)
✅ Intensity & sound power: 100% (4/4)
✅ Room & building acoustics: 100% (50/50)
✅ Building prediction & uncertainty: 100% (15/15)
✅ Outdoor propagation & occupational exposure: 100% (10/10)
✅ Materials: absorption, airflow & impedance: 100% (6/6)
✅ Scattering & diffusion (ISO 17497): 100% (5/5)
✅ In-situ road absorption (ISO 13472): 100% (3/3)
✅ Precision sound power (ISO 3745 / 9614-3): 100% (4/4)
✅ Human vibration (ISO 8041 / 2631 / 5349): 100% (15/15)
✅ Speech intelligibility (ANSI S3.5-1997): 100% (7/7)
✅ Impulsive-sound prominence (NT ACOU 112): 100% (2/2)
✅ Room noise (ANSI S12.2-2019): 100% (3/3)
✅ Hearing threshold (ISO 7029 / ISO 389-7): 100% (3/3)
✅ Measurement uncertainty (GUM / Supplement 1): 100% (7/7)
✅ Noise-induced hearing loss (ISO 1999): 100% (3/3)
✅ Multiple-shock whole-body vibration (ISO 2631-5): 100% (6/6)
✅ Sound absorption in enclosed spaces (EN 12354-6): 100% (2/2)
✅ Prominent discrete tones (ECMA-418-1): 100% (2/2)
✅ Tonal audibility (ISO/PAS 20065): 100% (11/11)
✅ Psychoacoustic annoyance & fluctuation strength (Fastl & Zwicker): 100% (3/3)
✅ Electroacoustics: distortion & frequency response: 100% (12/12)
✅ Calibrated spectral analysis (Bendat & Piersol): 100% (6/6)
✅ Correlation, time delay and envelope (B&P / Knapp & Carter): 100% (7/7)
✅ Underwater acoustics (ISO 18405/17208/18406): 100% (6/6)
✅ Underwater sound propagation (transmission loss): 100% (15/15)
✅ Underwater numerical propagation (modes / rays / PE): 100% (4/4)
✅ Aircraft noise (ICAO Annex 16 / IEC 61265): 100% (14/14)
✅ Rotorcraft noise (ECAC Doc 32 / NORAH2): 100% (12/12)
✅ Wind-turbine noise (IEC 61400-11): 100% (3/3)
✅ Porous & multilayer absorbers (Mechel / Bies / Cox & D'Antonio): 100% (10/10)
✅ Program loudness (ITU-R BS.1770 / EBU R 128): 100% (8/8)
✅ 2D FDTD wave simulation (Attenborough & Van Renterghem 2021, Ch. 4): 100% (2/2)
✅ Swept-sine distortion & phase utilities (Farina / Novak): 100% (7/7)
✅ Spherical ground & barriers (Attenborough / Salomons / Bies): 100% (6/6)
Tests & coverage — 23130 tests, 0 failures (✅ all green)
Conformance harness: |
Sonar: - S1244 (image_source.py): drop float == comparisons. The coincident source/receiver guard and the fully-absorbing-wall mask now use <= 0.0 (both quantities are non-negative by construction, so the behaviour and the pinned oracles are unchanged). - S3776 (image_source_rir cognitive complexity): extract _resolve_band_maps, _image_lattice, _image_amplitudes and _sample_ir with identical semantics; the reflection count (1561 at order 10) and exact geometry are unchanged. - S5778 (tests): move the array constructions out of the pytest.raises blocks. Bot findings: - Reflectogram renderer: guard the scatter and colorbar so a max_order=0 (direct-only) result does not crash on an empty scatter; regression test. - reflection_density: clamp negative times so the t**2 term cannot report a spurious positive density before the direct sound. - steady_state_spl: rename the room-constant local to r_const to avoid clashing with the critical distance rc. - Tests: assert the per-band energy decay monotonically across all bands. - Docs: language identifiers on the formula fences (MD040); describe T as the Eyring reverberation time (not a rate) and the fitted slope as recovering it; present the Schroeder frequency as a heuristic modal-to-diffuse crossover rather than a hard cutoff; note the order-limited T30 in the example. References sentence completed. llms regenerated.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/room-image-sources.md (1)
83-94: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winDescribe per-band output as bands, not necessarily octave bands.
ImageSourceResult.frequenciesstores supplied band-centre frequencies and is not restricted to octave bands. Change “one row per octave band” and related wording to “one row per band” unless the API explicitly validates octave spacing.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/room-image-sources.md` around lines 83 - 94, Update the ImageSourceResult documentation around image_source_rir to describe per-band outputs generically: replace “octave band” and “octave bands” wording with “band” and “bands,” including the related coefficient description. Preserve the existing distinction between broadband 1D output and one row per supplied frequency band.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/references.md`:
- Around line 374-377: The Bies 5th-edition reference year is incorrect in
docs/references.md: change it from 2018 to 2017 to match the DOI/publisher year.
The corresponding entry in docs/room-image-sources.md requires no direct change;
use it as the consistency reference.
---
Outside diff comments:
In `@docs/room-image-sources.md`:
- Around line 83-94: Update the ImageSourceResult documentation around
image_source_rir to describe per-band outputs generically: replace “octave band”
and “octave bands” wording with “band” and “bands,” including the related
coefficient description. Preserve the existing distinction between broadband 1D
output and one row per supplied frequency band.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: ea383740-b6f5-4b78-9c34-01aca5e889c5
📒 Files selected for processing (11)
docs/references.mddocs/room-image-sources.mddocs/theory-rooms-buildings.mdllms-full.txtsite/src/content/docs/es/guides/room-image-sources.mdxsite/src/content/docs/guides/room-image-sources.mdxsrc/phonometry/_plot/room.pysrc/phonometry/room/image_source.pysrc/phonometry/room/steady_field.pytests/room/test_image_source.pytests/room/test_steady_field.py
|



Summary
Room acoustics by image sources and the steady-state field for the room domain, clean-room from Kuttruff (Room Acoustics 6e), Vorländer (Auralization) and Bies, Hansen and Howard.
image_source_rir: the synthetic room impulse response of a rectangular room by the image-source method, enumerating the image lattice to a configurable order with the per-wall reflection count, per-band or broadband wall absorption and optional air attenuation, and assembling g(t) as a sum of delayed, attenuated deltas. Frozen result carrying the exact sub-integer image table (times, distances, orders, amplitudes, positions) with a reflectogram.plot(), plusaudible_image_countandreflection_densityhelpers.room_constant,critical_distance,schroeder_frequency,steady_state_splandsteady_state_field: the classical steady-state relations, with a frozen result splitting the direct, reverberant and total field versus distance and its.plot().Validation
Anchored on exact geometry (direct and first-reflection amplitudes and delays to machine precision), the Kuttruff image count (1561 images through order 10), the reflection-density growth law, and the Eyring T60 recovered from the synthetic RIR's reverberant decay (with a documented finding that an elongated room decays slower than Eyring, the Fitzroy/Arau regime). The FDTD 2D solver already in the library is used as an independent numerical cross-check: the rigid-wall echo matches the first image delay, and a uniform-damping T60 comes back at 0.588 s against 0.600 s analytic. The steady-state relations match their closed forms, including the critical distance where the direct and reverberant fields are equal by definition. Seven new conformance checks (319 total, byte-exact report).
Checks
ruff, mypy, bandit, full pytest (3790 passed, no new warnings), conformance byte-exact, figures within tolerance (848, one new in four variants), api-docs with no drift, curated API table 721 names, site i18n parity and full build clean. Adversarially reviewed; both findings applied, plus proactive guards for coincident source/receiver and the length-6 absorption ambiguity.
Summary by CodeRabbit