%23%20gallery_category%3A%20Getting%20Started%0A%23%20gallery_title%3A%20Train%20an%20FDN%20to%20match%20a%20measured%20room%0A%23%20gallery_description%3A%20Fit%20an%20FDN%20to%20a%20measured%20room%20impulse%20response%2C%20inspect%20and%20export%20its%20trained%20filters%2C%20and%20validate%20octave-band%20decay%20and%20level.%0A%23%20references%3A%20Concert_Hall_Impulse_Responses%2C%20Learning_Filters_In_FDNs_From_Noisy_RIRs%0A%0Aimport%20marimo%0A%0A__generated_with%20%3D%20%220.25.1%22%0Aapp%20%3D%20marimo.App()%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%0A%20%20%20%20return%20(mo%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo%2C%20pyFDN)%3A%0A%20%20%20%20%23%20TODO%3A%20Replace%20the%20arXiv%20reference%20with%20the%20published%20version%20when%20available.%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20%23%20Train%20an%20FDN%20to%20match%20a%20measured%20room%0A%0A%20%20%20%20Fit%20an%20FDN%20to%20a%20concert-hall%20impulse%20response%20using%20gradient%20descent.%0A%20%20%20%20Start%20with%20a%20flat%201%20s%20decay%2C%20train%20its%20absorption%20and%20output%20EQ%20together%20with%0A%20%20%20%20the%20feedback%20matrix%20and%20gains%2C%20then%20export%20the%20result%20and%20validate%20its%20decay%0A%20%20%20%20and%20level%20in%20octave%20bands.%0A%0A%20%20%20%20Related%20work%3A%20%7BpyFDN.paper_link(%22Learning_Filters_In_FDNs_From_Noisy_RIRs%22)%7D.%0A%20%20%20%20The%20paper%20studies%20fitting%20attenuation%20filters%20to%20noisy%20RIRs%3B%20this%20example%0A%20%20%20%20uses%20a%20cumulative-energy%20loss%20without%20an%20explicit%20noise%20model.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20torch%0A%0A%20%20%20%20import%20pyFDN%0A%0A%20%20%20%20return%20np%2C%20plt%2C%20pyFDN%2C%20torch%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Settings%0A%0A%20%20%20%20Choose%20CPU%20or%20CUDA%20GPU%20below%3B%20both%20use%20float32.%20CUDA%20is%20selected%20when%20available.%0A%20%20%20%20Absorption%20and%20output%20EQ%20are%20first-order%20shelves%3A%20two%20parameters%20and%20one%0A%20%20%20%20biquad%20per%20filter.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo%2C%20torch)%3A%0A%20%20%20%20fs%20%3D%2048000%0A%20%20%20%20_runtimes%20%3D%20%7B%22CPU%22%3A%20%22cpu%22%7D%0A%20%20%20%20_default_runtime%20%3D%20%22CPU%22%0A%20%20%20%20if%20torch.cuda.is_available()%3A%0A%20%20%20%20%20%20%20%20_runtimes%5B%22GPU%20(CUDA)%22%5D%20%3D%20%22cuda%22%0A%20%20%20%20%20%20%20%20_default_runtime%20%3D%20%22GPU%20(CUDA)%22%0A%20%20%20%20runtime_choice%20%3D%20mo.ui.dropdown(%0A%20%20%20%20%20%20%20%20options%3D_runtimes%2C%20value%3D_default_runtime%2C%20label%3D%22Run%20on%22%0A%20%20%20%20)%0A%20%20%20%20mo.hstack(%5Bruntime_choice%5D%2C%20justify%3D%22start%22)%0A%20%20%20%20return%20fs%2C%20runtime_choice%0A%0A%0A%40app.cell%0Adef%20_(runtime_choice)%3A%0A%20%20%20%20device%20%3D%20runtime_choice.value%0A%20%20%20%20return%20(device%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo%2C%20pyFDN)%3A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20%23%23%20Step%201%20%E2%80%94%20the%20target%0A%0A%20%20%20%20The%20Promenadikeskus%20concert%20hall%20in%20Pori%2C%20Finland%2C%20published%20at%0A%20%20%20%20%7BpyFDN.paper_link(%22Concert_Hall_Impulse_Responses%22)%7D.%20Trimmed%20to%20the%20direct%0A%20%20%20%20sound%20and%20normalized%20to%20unit%20energy%2C%20exactly%20as%20in%20**Convert%20a%20room%20impulse%0A%20%20%20%20response%20into%20an%20FDN**%2C%20so%20the%20two%20notebooks%20are%20comparable.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20np%2C%20pyFDN)%3A%0A%20%20%20%20rir%2C%20_file_fs%20%3D%20pyFDN.load_audio(%22s3_r4_o%22%2C%20fs%3Dfs)%0A%20%20%20%20rir%20%3D%20rir%5Bint(np.argmax(np.abs(rir)))%20%3A%5D%20%20%23%20trim%20to%20the%20direct%20sound%0A%20%20%20%20rir%20%3D%20rir%20%2F%20np.linalg.norm(rir)%20%20%23%20unit%20energy%0A%20%20%20%20rir_len%20%3D%20len(rir)%0A%0A%20%20%20%20print(f%22target%20RIR%3A%20%7Brir_len%7D%20samples%20(%7Brir_len%20%2F%20fs%3A.2f%7D%20s)%20at%20%7Bfs%7D%20Hz%22)%0A%20%20%20%20return%20rir%2C%20rir_len%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20mo%2C%20pyFDN%2C%20rir)%3A%0A%20%20%20%20mo.vstack(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.plot_spectrogram(rir%2C%20fs%2C%20title%3D%22The%20target%3A%20a%20measured%20hall%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.labeled_audio(%22the%20room%22%2C%20pyFDN.peak_normalize(rir)%2C%20fs%3Dfs)%2C%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Step%202%20%E2%80%94%20build%20the%20initial%20FDN%0A%0A%20%20%20%20Use%20sixteen%20geometrically%20spaced%2C%20coprime%20delays%2C%20a%20random%20orthogonal%20feedback%0A%20%20%20%20matrix%2C%20normalized%20input%2Foutput%20gains%2C%20and%20a%20flat%201%20s%20decay.%0A%20%20%20%20A%20fixed%20seed%20makes%20the%20starting%20point%20reproducible.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20np%2C%20pyFDN)%3A%0A%20%20%20%20%23%20This%20seed%20gives%20a%20feedback%20matrix%20in%20SO(N).%0A%20%20%20%20init_build%20%3D%20pyFDN.fdn_build_gallery(%0A%20%20%20%20%20%20%20%20N%3D16%2C%0A%20%20%20%20%20%20%20%20fs%3Dfs%2C%0A%20%20%20%20%20%20%20%20delay_range%3D(700%2C%202500)%2C%0A%20%20%20%20%20%20%20%20delay_distribution%3D%22geometric%22%2C%0A%20%20%20%20%20%20%20%20coprime%3DTrue%2C%0A%20%20%20%20%20%20%20%20sort_delays%3DTrue%2C%0A%20%20%20%20%20%20%20%20io_type%3D%22normalized%22%2C%0A%20%20%20%20%20%20%20%20direct_gain%3D0.0%2C%0A%20%20%20%20%20%20%20%20rt%3D1.0%2C%20%20%23%20flat%201%20s%20decay%0A%20%20%20%20%20%20%20%20rt_nyquist%3D1.0%2C%0A%20%20%20%20%20%20%20%20rng%3D2%2C%0A%20%20%20%20)%0A%20%20%20%20assert%20np.linalg.det(init_build.A)%20%3E%200%2C%20%22feedback%20matrix%20is%20not%20in%20SO(N)%22%0A%0A%20%20%20%20print(f%22delays%20(samples)%3A%20%7Binit_build.delays%7D%22)%0A%20%20%20%20print(f%22absorption%3A%20%20%20%20%20%20%20%7Binit_build.post_delay.shape%7D%20%E2%80%94%20flat%201%20s%22)%0A%20%20%20%20return%20(init_build%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Step%203%20%E2%80%94%20make%20it%20trainable%0A%0A%20%20%20%20%60trainable_from_build%60%20creates%20a%20FLAMO%20model%20with%20these%20parameters%3A%0A%0A%20%20%20%20%7C%20Component%20%7C%20Parameter%20%7C%0A%20%20%20%20%7C---%7C---%7C%0A%20%20%20%20%7C%20Absorption%20(%60post_delay%60)%20%7C%20Reverberation%20time%20in%20seconds%20%7C%0A%20%20%20%20%7C%20Output%20EQ%20(%60post_output%60)%20%7C%20Gain%20in%20dB%20%7C%0A%20%20%20%20%7C%20Feedback%20matrix%20%7C%20Orthogonal%20matrix%20on%20%24SO(N)%24%20%7C%0A%20%20%20%20%7C%20Input%2C%20output%20and%20direct%20gains%20%7C%20Linear%20gain%20%7C%0A%0A%20%20%20%20The%20integer%20delays%20stay%20fixed.%20%60AttenuationFilter%60%20maps%20positive%2C%20smoothly%0A%20%20%20%20floored%20RT%20values%20to%20per-delay%20attenuation%2C%20%24-60d_i%2F(%5Cmathrm%7BRT%7D%5C%2Cf_s)%24%20dB%2C%0A%20%20%20%20and%20designs%20the%20absorption%20filters.%20%60OutputEQ%60%20shapes%20the%20output%20spectrum.%0A%0A%20%20%20%20Train%20at%20%602**16%60%20samples%20(1.37%20s%20at%2048%20kHz)%20to%20reduce%20the%20cost%20per%20step.%0A%20%20%20%20Render%20at%20%602**17%60%20(2.73%20s)%20to%20give%20the%20final%20decay%20estimators%20a%20longer%20window.%0A%20%20%20%20Use%20%60set_nfft%60%20to%20switch%20grids%20while%20retaining%20the%20parameters.%0A%20%20%20%20Match%20the%20initial%20model's%20energy%20to%20the%20target%20on%20the%20render%20window.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(device%2C%20fs%2C%20init_build%2C%20np%2C%20pyFDN%2C%20rir%2C%20rir_len%2C%20torch)%3A%0A%20%20%20%20from%20copy%20import%20deepcopy%0A%0A%20%20%20%20train_nfft%20%3D%202**16%20%20%23%201.37%20s%20training%20window%0A%20%20%20%20render_nfft%20%3D%202**17%20%20%23%202.73%20s%20validation%20window%0A%0A%20%20%20%20model%20%3D%20pyFDN.trainable_from_build(%0A%20%20%20%20%20%20%20%20init_build%2C%0A%20%20%20%20%20%20%20%20%23%20every%20gain%20with%20a%20gradient%3A%20A%2C%20b%2C%20c%20and%20D%0A%20%20%20%20%20%20%20%20trainable%3DpyFDN.Trainable(direct%3DTrue)%2C%0A%20%20%20%20%20%20%20%20%23%20absorption%20parametrized%20by%20reverberation%20time%0A%20%20%20%20%20%20%20%20post_delay%3DpyFDN.AttenuationFilter(%0A%20%20%20%20%20%20%20%20%20%20%20%201.0%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20init_build.delays%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20fs%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20design%3D%22first_order_shelf%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20nfft%3Drender_nfft%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20device%3Ddevice%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20dtype%3Dtorch.float32%2C%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%23%20the%20output%20EQ%2C%20starting%20flat%2C%20as%20a%20gain%20in%20dB%0A%20%20%20%20%20%20%20%20post_output%3DpyFDN.OutputEQ(%0A%20%20%20%20%20%20%20%20%20%20%20%200.0%2C%0A%20%20%20%20%20%20%20%20%20%20%20%201%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20fs%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20design%3D%22first_order_shelf%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20nfft%3Drender_nfft%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20device%3Ddevice%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20dtype%3Dtorch.float32%2C%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20nfft%3Drender_nfft%2C%0A%20%20%20%20%20%20%20%20device%3Ddevice%2C%0A%20%20%20%20%20%20%20%20dtype%3Dtorch.float32%2C%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Match%20energy%20on%20the%20validation%20window%3B%20output%20gain%20scales%20the%20entire%20IR%0A%20%20%20%20%23%20because%20the%20initial%20direct%20path%20is%20zero.%0A%20%20%20%20ir_init%20%3D%20pyFDN.flamo_time_response(model%2C%20fs%3Dfs).squeeze().astype(float)%5B%3Arir_len%5D%0A%20%20%20%20energy_gain%20%3D%20np.linalg.norm(rir%5B%3Arender_nfft%5D)%20%2F%20np.linalg.norm(ir_init)%0A%20%20%20%20with%20torch.no_grad()%3A%0A%20%20%20%20%20%20%20%20pyFDN.param(model%2C%20%22output_gain%22).raw().mul_(energy_gain)%0A%20%20%20%20ir_init%20*%3D%20energy_gain%0A%20%20%20%20initial_build%20%3D%20deepcopy(pyFDN.extract_build(model))%0A%0A%20%20%20%20for%20_p%20in%20pyFDN.params(model)%3A%0A%20%20%20%20%20%20%20%20print(_p)%0A%20%20%20%20print(f%22%5Cnenergy%20match%3A%20output%20gain%20x%20%7Benergy_gain%3A.2f%7D%22)%0A%20%20%20%20return%20initial_build%2C%20ir_init%2C%20model%2C%20render_nfft%2C%20train_nfft%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Step%204%20%E2%80%94%20define%20the%20loss%0A%0A%20%20%20%20%60MatchCumulativeEnergy%60%20compares%20integrated%20short-time%20energy%20to%20capture%20decay%0A%20%20%20%20and%20spectral%20shape.%20For%20the%20descending%20frequency%20direction%3A%0A%0A%20%20%20%20%24%24E%5Bf%2Ct%5D%20%3D%20%5Csum_%7Bt'%5Cge%20t%7D%5Csum_%7Bf'%5Cge%20f%7D%7CS%5Bf'%2Ct'%5D%7C%5E2.%24%24%0A%0A%20%20%20%20%60frequency%3D%22both%22%60%20averages%20ascending%20and%20descending%20frequency%20comparisons%20to%0A%20%20%20%20balance%20their%20weighting%20across%20the%20spectrum.%20%60power%3D0.5%60%20compresses%20the%0A%20%20%20%20normalized%20energy%20to%20give%20the%20quieter%20tail%20more%20weight.%20The%20STFT%20window%20is%0A%20%20%20%201024%20samples.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(pyFDN%2C%20rir)%3A%0A%20%20%20%20loss%20%3D%20pyFDN.MatchCumulativeEnergy(rir%2C%20window%3D1024%2C%20power%3D0.5%2C%20frequency%3D%22both%22)%0A%20%20%20%20return%20(loss%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Step%205%20%E2%80%94%20train%0A%0A%20%20%20%20Run%20up%20to%20300%20Adam%20steps%20at%20%60lr%3D3e-2%60%20on%20the%20training%20grid%2C%20then%20render%20the%0A%20%20%20%20trained%20response%20on%20the%20longer%20grid%20for%20validation.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(device%2C%20fs%2C%20loss%2C%20model%2C%20pyFDN%2C%20render_nfft%2C%20rir_len%2C%20train_nfft)%3A%0A%20%20%20%20model.set_nfft(train_nfft)%0A%20%20%20%20log%20%3D%20pyFDN.train_fdn(%0A%20%20%20%20%20%20%20%20model%2C%0A%20%20%20%20%20%20%20%20loss%2C%0A%20%20%20%20%20%20%20%20max_steps%3D300%2C%0A%20%20%20%20%20%20%20%20lr%3D3e-2%2C%0A%20%20%20%20%20%20%20%20patience%3D100%2C%0A%20%20%20%20%20%20%20%20device%3Ddevice%2C%0A%20%20%20%20%20%20%20%20rng%3D0%2C%0A%20%20%20%20)%0A%20%20%20%20model.set_nfft(render_nfft)%0A%20%20%20%20ir_trained%20%3D%20pyFDN.flamo_time_response(model%2C%20fs%3Dfs).squeeze()%5B%3Arir_len%5D%0A%20%20%20%20print(%0A%20%20%20%20%20%20%20%20f%22%7Blog.steps_run%7D%20steps%3A%20loss%20%22%0A%20%20%20%20%20%20%20%20f%22%7Blog.train_loss%5B0%5D%3A.4g%7D%20-%3E%20%7Blog.train_loss%5B-1%5D%3A.4g%7D%22%0A%20%20%20%20)%0A%20%20%20%20return%20ir_trained%2C%20log%0A%0A%0A%40app.cell%0Adef%20_(log%2C%20plt)%3A%0A%20%20%20%20_fig%2C%20_ax%20%3D%20plt.subplots(figsize%3D(8%2C%203.6))%0A%20%20%20%20_ax.semilogy(log.train_loss)%0A%20%20%20%20_ax.set_xlabel(%22Step%22)%0A%20%20%20%20_ax.set_ylabel(%22Cumulative-energy%20RMS%20error%22)%0A%20%20%20%20_ax.set_title(%22Training%20loss%22)%0A%20%20%20%20_fig.tight_layout()%0A%20%20%20%20_fig%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Step%206%20%E2%80%94%20export%20and%20inspect%20the%20fitted%20FDN%0A%0A%20%20%20%20%60extract_build%60%20returns%20a%20NumPy%20%60FDNBuild%60%20with%20absorption%20and%20output%20EQ%20stored%0A%20%20%20%20as%20SOS%20banks.%20Use%20it%20with%20%60build_to_impz%60%2C%20%60build_to_flamo%60%2C%20or%20%60process_fdn%60.%0A%20%20%20%20%60param(...).raw()%60%20gives%20the%20trained%20RT%20values%20in%20seconds%20and%20EQ%20gains%20in%20dB.%0A%0A%20%20%20%20%60plot_fdn_build%60%20shows%20the%20matrix%2C%20gains%2C%20delays%2C%20absorption%20in%20dB%20per%20sample%2C%0A%20%20%20%20and%20output%20EQ%20in%20dB.%20Compare%20the%20initial%20and%20trained%20builds%20below.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(ir_trained%2C%20model%2C%20np%2C%20pyFDN)%3A%0A%20%20%20%20%23%20Depend%20on%20the%20trained%20response%20so%20marimo%20extracts%20after%20training.%0A%20%20%20%20assert%20np.all(np.isfinite(ir_trained))%2C%20%22trained%20FDN%20did%20not%20render%22%0A%20%20%20%20trained_build%20%3D%20pyFDN.extract_build(model)%0A%20%20%20%20for%20_name%2C%20_unit%20in%20((%22post_delay%22%2C%20%22s%22)%2C%20(%22post_output%22%2C%20%22dB%22))%3A%0A%20%20%20%20%20%20%20%20_parameter%20%3D%20pyFDN.param(model%2C%20_name)%0A%20%20%20%20%20%20%20%20print(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%7B_name%7D%20(%7B_unit%7D)%3A%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%7B_parameter.raw().detach().cpu().numpy().ravel().round(2)%7D%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20np.testing.assert_allclose(%0A%20%20%20%20%20%20%20%20%20%20%20%20getattr(trained_build%2C%20_name)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20_parameter.value().detach().cpu().numpy()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20atol%3D1e-12%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20return%20(trained_build%2C)%0A%0A%0A%40app.cell%0Adef%20_(initial_build%2C%20mo%2C%20pyFDN%2C%20trained_build)%3A%0A%20%20%20%20mo.hstack(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.plot_fdn_build(initial_build%2C%20title%3D%22FDN%2C%20untrained%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.plot_fdn_build(trained_build%2C%20title%3D%22FDN%2C%20trained%22)%2C%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Inspect%20the%20responses%0A%0A%20%20%20%20Compare%20the%20full-band%20energy%20decay%20and%20spectrograms%20of%20the%20target%20and%20FDNs.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir_init%2C%20ir_trained%2C%20pyFDN%2C%20rir)%3A%0A%20%20%20%20pyFDN.plot_edc(%0A%20%20%20%20%20%20%20%20rir%2C%0A%20%20%20%20%20%20%20%20ir_init%2C%0A%20%20%20%20%20%20%20%20ir_trained%2C%0A%20%20%20%20%20%20%20%20fs%3Dfs%2C%0A%20%20%20%20%20%20%20%20labels%3D%5B%22Target%20RIR%22%2C%20%22FDN%2C%20untrained%22%2C%20%22FDN%2C%20trained%22%5D%2C%0A%20%20%20%20%20%20%20%20title%3D%22Energy%20decay%20curve%22%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir_init%2C%20pyFDN)%3A%0A%20%20%20%20pyFDN.plot_spectrogram(ir_init%2C%20fs%2C%20title%3D%22FDN%2C%20untrained%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir_trained%2C%20pyFDN)%3A%0A%20%20%20%20pyFDN.plot_spectrogram(ir_trained%2C%20fs%2C%20title%3D%22FDN%2C%20trained%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Listen%0A%0A%20%20%20%20Peak-normalize%20all%20three%20responses%20for%20listening.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir_init%2C%20ir_trained%2C%20mo%2C%20pyFDN%2C%20rir)%3A%0A%20%20%20%20mo.hstack(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.labeled_audio(%22Target%20RIR%22%2C%20pyFDN.peak_normalize(rir)%2C%20fs%3Dfs)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.labeled_audio(%22Untrained%22%2C%20pyFDN.peak_normalize(ir_init)%2C%20fs%3Dfs)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.labeled_audio(%22Trained%22%2C%20pyFDN.peak_normalize(ir_trained)%2C%20fs%3Dfs)%2C%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20gap%3D2%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Step%207%20%E2%80%94%20validate%0A%0A%20%20%20%20Measure%20octave-band%20reverberation%20time%20and%20initial%20level%20in%20the%20target%20and%0A%20%20%20%20both%20rendered%20FDNs%20using%20Schroeder%20integration.%20Compare%20relative%20RT%20error%2C%0A%20%20%20%20mean%20level%20offset%2C%20and%20level%20shape%20error%20(mean%20absolute%20error%20after%20removing%0A%20%20%20%20the%20offset).%20These%20measurements%20assess%20the%20fitted%20response%20independently%20of%0A%20%20%20%20the%20training%20loss.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20pyFDN%2C%20rir)%3A%0A%20%20%20%20est_rt%2C%20f_centre%20%3D%20pyFDN.estimate_rt_bands(rir%2C%20fs)%0A%20%20%20%20est_level%2C%20_%20%3D%20pyFDN.estimate_initial_level_bands(rir%2C%20est_rt%2C%20fs)%0A%0A%20%20%20%20print(f%22bands%20(Hz)%3A%20%20%20%7Bf_centre.round(0)%7D%22)%0A%20%20%20%20print(f%22measured%20RT%3A%20%20%7Best_rt.round(2)%7D%22)%0A%20%20%20%20return%20est_level%2C%20est_rt%2C%20f_centre%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir_init%2C%20ir_trained%2C%20pyFDN)%3A%0A%20%20%20%20rt_init%2C%20_%20%3D%20pyFDN.estimate_rt_bands(ir_init%2C%20fs)%0A%20%20%20%20rt_trained%2C%20_%20%3D%20pyFDN.estimate_rt_bands(ir_trained%2C%20fs)%0A%20%20%20%20level_init%2C%20_%20%3D%20pyFDN.estimate_initial_level_bands(ir_init%2C%20rt_init%2C%20fs)%0A%20%20%20%20level_trained%2C%20_%20%3D%20pyFDN.estimate_initial_level_bands(ir_trained%2C%20rt_trained%2C%20fs)%0A%20%20%20%20return%20level_init%2C%20level_trained%2C%20rt_init%2C%20rt_trained%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20est_level%2C%0A%20%20%20%20est_rt%2C%0A%20%20%20%20f_centre%2C%0A%20%20%20%20level_init%2C%0A%20%20%20%20level_trained%2C%0A%20%20%20%20np%2C%0A%20%20%20%20plt%2C%0A%20%20%20%20pyFDN%2C%0A%20%20%20%20rt_init%2C%0A%20%20%20%20rt_trained%2C%0A)%3A%0A%20%20%20%20_labels%20%3D%20%5B%22Target%20RIR%22%2C%20%22FDN%2C%20untrained%22%2C%20%22FDN%2C%20trained%22%5D%0A%20%20%20%20_fig%2C%20_axes%20%3D%20plt.subplots(1%2C%202%2C%20figsize%3D(10%2C%203.8))%0A%20%20%20%20for%20_ax%2C%20(_title%2C%20_values)%20in%20zip(%0A%20%20%20%20%20%20%20%20_axes%2C%0A%20%20%20%20%20%20%20%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20(%22RT%20(s)%22%2C%20(est_rt%2C%20rt_init%2C%20rt_trained))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22Initial%20level%20(dB)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.lin_to_db(%5Best_level%2C%20level_init%2C%20level_trained%5D)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20strict%3DTrue%2C%0A%20%20%20%20)%3A%0A%20%20%20%20%20%20%20%20for%20_label%2C%20_value%20in%20zip(_labels%2C%20_values%2C%20strict%3DTrue)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.plot(f_centre%2C%20_value%2C%20%22o-%22%2C%20label%3D_label)%0A%20%20%20%20%20%20%20%20_ax.set_xscale(%22log%22)%0A%20%20%20%20%20%20%20%20_ax.set_xlabel(%22Frequency%20(Hz)%22)%0A%20%20%20%20%20%20%20%20_ax.set_ylabel(_title)%0A%20%20%20%20%20%20%20%20_ax.set_title(_title)%0A%20%20%20%20%20%20%20%20_ax.legend()%0A%20%20%20%20_fig.tight_layout()%0A%0A%20%20%20%20for%20_name%2C%20_rt%2C%20_level%20in%20(%0A%20%20%20%20%20%20%20%20(%22FDN%2C%20untrained%22%2C%20rt_init%2C%20level_init)%2C%0A%20%20%20%20%20%20%20%20(%22FDN%2C%20trained%22%2C%20rt_trained%2C%20level_trained)%2C%0A%20%20%20%20)%3A%0A%20%20%20%20%20%20%20%20_err%20%3D%20pyFDN.lin_to_db(_level)%20-%20pyFDN.lin_to_db(est_level)%0A%20%20%20%20%20%20%20%20print(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%7B_name%3A16s%7D%20RT%20error%20%7B100%20*%20np.abs(_rt%20%2F%20est_rt%20-%201).mean()%3A4.1f%7D%25%20%20%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22level%20offset%20%7B_err.mean()%3A%2B5.1f%7D%20dB%20%20%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22level%20shape%20%7Bnp.abs(_err%20-%20_err.mean()).mean()%3A4.2f%7D%20dB%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20_fig%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Validation%20checks%0A%0A%20%20%20%20Check%20that%20the%20measured%20RTs%20are%20finite%20and%20the%20sampled%20absorption%20curves%0A%20%20%20%20are%20contractive%20for%20every%20delay%20line.%0A%20%20%20%20Require%20mean%20RT%20error%20below%2015%25%2C%20worst-band%20error%20below%2035%25%2C%20and%20a%20mean%20error%0A%20%20%20%20less%20than%2030%25%20of%20the%20initial%20model's.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(est_rt%2C%20np%2C%20pyFDN%2C%20rt_init%2C%20rt_trained%2C%20trained_build)%3A%0A%20%20%20%20assert%20np.all(np.isfinite(rt_trained))%2C%20%22trained%20RT%20estimates%20are%20not%20finite%22%0A%20%20%20%20_%2C%20_gain%20%3D%20pyFDN.sos_gain_per_sample_curves(%0A%20%20%20%20%20%20%20%20trained_build.post_delay%2C%20trained_build.delays%0A%20%20%20%20)%0A%20%20%20%20assert%20np.all(np.isfinite(_gain))%20and%20np.all((_gain%20%3E%200)%20%26%20(_gain%20%3C%201))%2C%20(%0A%20%20%20%20%20%20%20%20%22the%20decay%20filters%20are%20not%20contractive%20at%20every%20sampled%20frequency%22%0A%20%20%20%20)%0A%0A%20%20%20%20_err_init%20%3D%20np.abs(rt_init%20%2F%20est_rt%20-%201)%0A%20%20%20%20_err%20%3D%20np.abs(rt_trained%20%2F%20est_rt%20-%201)%0A%20%20%20%20print(f%22RT%20error%20per%20band%2C%20untrained%3A%20%7B_err_init.round(3)%7D%22)%0A%20%20%20%20print(f%22RT%20error%20per%20band%2C%20trained%3A%20%20%20%7B_err.round(3)%7D%22)%0A%20%20%20%20assert%20_err.mean()%20%3C%200.3%20*%20_err_init.mean()%2C%20%22the%20fit%20barely%20moved%20the%20decay%22%0A%20%20%20%20assert%20_err.mean()%20%3C%200.15%2C%20%22the%20trained%20decay%20is%20not%20close%20to%20the%20measurement%22%0A%20%20%20%20assert%20_err.max()%20%3C%200.35%2C%20%22one%20band's%20decay%20is%20far%20off%20the%20measurement%22%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
a8073449567747a77665f0ee2b940efe