%23%20gallery_category%3A%20Getting%20Started%0A%23%20gallery_title%3A%20Train%20an%20FDN%20to%20match%20a%20measured%20room%0A%23%20gallery_description%3A%20Hands-on%20walk-through%20of%20fitting%20an%20FDN%20to%20a%20measured%20room%20impulse%20response%20by%20gradient%20descent%20-%20a%20generic%201%20s%20reverberator%20in%2C%20decay%20and%20output%20EQ%20trained%20out%20-%20with%20the%20objective%2C%20the%20parametrization%20and%20the%20runtime%20each%20a%20switch%20you%20can%20turn.%20Every%20step%20has%20experiments%20to%20try.%0A%23%20references%3A%20Concert_Hall_Impulse_Responses%0A%0Aimport%20marimo%0A%0A__generated_with%20%3D%20%220.24.0%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)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20Train%20an%20FDN%20to%20match%20a%20measured%20room%0A%0A%20%20%20%20**Convert%20a%20room%20impulse%20response%20into%20an%20FDN**%20designs%20a%20reverberator%0A%20%20%20%20analytically%3A%20measure%20the%20decay%2C%20measure%20the%20level%2C%20turn%20both%20into%20filters.%0A%20%20%20%20This%20notebook%20does%20the%20opposite.%20It%20starts%20from%20a%20**generic%201%20s%20reverberator%0A%20%20%20%20that%20knows%20nothing%20about%20the%20room**%2C%20hands%20every%20parameter%20to%20an%20optimizer%2C%0A%20%20%20%20and%20lets%20a%20loss%20function%20find%20them.%0A%0A%20%20%20%20%7C%20%7C%20%7C%20%7C%0A%20%20%20%20%7C%20---%20%7C%20---%20%7C%20---%20%7C%0A%20%20%20%20%7C%20the%20target%20%7C%20a%20measured%20concert%20hall%20%7C%20what%20the%20loss%20compares%20against%20%7C%0A%20%20%20%20%7C%20the%20start%20%7C%20a%20flat%201%20s%20FDN%20%7C%20what%20the%20optimizer%20is%20given%20%7C%0A%20%20%20%20%7C%20the%20knobs%20%7C%20decay%2C%20output%20EQ%2C%20matrix%2C%20gains%20%7C%20what%20has%20a%20gradient%20%7C%0A%20%20%20%20%7C%20the%20exam%20%7C%20octave-band%20%24T_%7B60%7D%24%20and%20level%20%7C%20measured%20*after*%2C%20fed%20to%20nothing%20%7C%0A%0A%20%20%20%20Three%20things%20have%20to%20be%20right%20before%20an%20optimizer%20is%20worth%20reaching%20for%2C%20and%0A%20%20%20%20each%20is%20a%20step%20below%3A%20a%20**parametrization**%20the%20decay%20cannot%20escape%20from%2C%20a%0A%20%20%20%20**module**%20that%20can%20change%20the%20colour%2C%20and%20a%20**loss**%20that%20can%20see%20a%20decay%20at%0A%20%20%20%20all.%20The%20obvious%20choice%20of%20loss%20gets%20the%20last%20one%20confidently%20wrong%2C%20and%0A%20%20%20%20section%204%20shows%20the%20table%20where%20it%20does.%0A%0A%20%20%20%20Each%20code%20cell%20ends%20with%20a%20**Try%20this**%20block%3A%20change%20a%20number%2C%20and%20marimo%0A%20%20%20%20re-runs%20everything%20downstream%20%E2%80%94%20plots%2C%20metrics%20and%20audio%20included.%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%20numpy%20as%20np%0A%20%20%20%20import%20plotly.graph_objects%20as%20go%0A%20%20%20%20import%20torch%0A%0A%20%20%20%20import%20pyFDN%0A%0A%20%20%20%20return%20go%2C%20np%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%20%E2%80%94%20the%20two%20switches%20worth%20having%20up%20front%0A%0A%20%20%20%20**Runtime.**%20The%20fit%20is%20300%20gradient%20steps%20through%20a%20%242%5E%7B17%7D%24-point%20frequency%0A%20%20%20%20grid%2C%20which%20is%20a%20couple%20of%20minutes%20of%20CPU.%20If%20the%20runtime%20has%20an%20NVIDIA%20GPU%0A%20%20%20%20it%20appears%20in%20the%20dropdown%3B%20pick%20it%20and%20everything%20downstream%20moves.%20Apple's%0A%20%20%20%20MPS%20backend%20is%20deliberately%20*not*%20offered%20%E2%80%94%20the%20frequency-domain%20recursion%20is%0A%20%20%20%20a%20complex%20matrix%20solve%2C%20and%20MPS%20implements%20neither%20%60matrix_exp%60%20nor%20complex%0A%20%20%20%20%60linalg.solve%60.%0A%0A%20%20%20%20Precision%20is%20paired%20with%20the%20device%20rather%20than%20left%20free.%20%60float64%60%20is%20the%0A%20%20%20%20reference%3A%20a%20cumulative-energy%20loss%20reads%20the%20quietest%20samples%20in%20the%20buffer%2C%0A%20%20%20%20so%20it%20is%20the%20one%20objective%20where%20round-off%20at%20the%20end%20of%20a%20render%20could%0A%20%20%20%20plausibly%20matter.%20On%20*this*%20fit%20it%20does%20not%20%E2%80%94%20the%20two%20precisions%20land%20within%0A%20%20%20%200.2%20%25%20of%20the%20same%20loss%20%E2%80%94%20so%20%60float32%60%20is%20the%20default%20and%20the%20run%20is%20half%20as%0A%20%20%20%20long.%20On%20a%20consumer%20GPU%20it%20is%20not%20a%20close%20call%3A%20%60float64%60%20there%20is%20a%20factor%0A%20%20%20%20of%2032%20down%20on%20throughput.%0A%0A%20%20%20%20**EQ%20design.**%20The%20two%20filters%20the%20FDN%20needs%20%E2%80%94%20the%20in-loop%20attenuation%20that%0A%20%20%20%20sets%20the%20decay%2C%20and%20the%20output%20EQ%20that%20colours%20it%20%E2%80%94%20take%20the%20same%0A%20%20%20%20%60pyFDN.EQDesign%60%20name.%20A%20ten-band%20graphic%20EQ%20spends%20ten%20numbers%20and%20eleven%0A%20%20%20%20biquads%20on%20each%3B%20a%20first-order%20shelf%20spends%20two%20and%20one.%20Nothing%20downstream%0A%20%20%20%20names%20a%20design%20class%2C%20so%20switching%20the%20whole%20notebook%20is%20switching%20one%20word.%0A%0A%20%20%20%20The%20shelf%20is%20the%20default%20because%20it%20is%20the%20cheaper%20run%20by%20a%20wide%20margin%20%E2%80%94%0A%20%20%20%20eleven%20biquads%20per%20delay%20line%20instead%20of%20one%20is%20roughly%20five%20times%20the%20wall%0A%20%20%20%20clock%20for%20the%20same%20300%20steps%20%E2%80%94%20and%20because%20on%20this%20room%20it%20is%20*not*%20the%20worse%0A%20%20%20%20answer.%20Both%20results%20are%20tabulated%20further%20down%2C%20so%20the%20comparison%20is%0A%20%20%20%20readable%20without%20paying%20for%20it.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo%2C%20np%2C%20torch)%3A%0A%20%20%20%20from%20pyFDN.eq%20import%20CENTER_FREQUENCIES%0A%0A%20%20%20%20fs%20%3D%2048000%0A%0A%20%20%20%20%23%20Where%20the%20fit%20runs%2C%20and%20in%20what%20precision.%20float64%20on%20a%20consumer%20GPU%20is%20a%0A%20%20%20%20%23%20factor%20of%2032%20down%20on%20throughput%2C%20so%20it%20is%20not%20offered%20there.%0A%20%20%20%20_runtimes%20%3D%20%7B%22CPU%20%E2%80%94%20float32%22%3A%20(%22cpu%22%2C%20torch.float32)%7D%0A%20%20%20%20if%20torch.cuda.is_available()%3A%0A%20%20%20%20%20%20%20%20_runtimes%5Bf%22GPU%20%E2%80%94%20%7Btorch.cuda.get_device_name(0)%7D%2C%20float32%22%5D%20%3D%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22cuda%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20torch.float32%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20_runtimes%5B%22CPU%20%E2%80%94%20float64%2C%20the%20reference%20run%22%5D%20%3D%20(%22cpu%22%2C%20torch.float64)%0A%20%20%20%20_default_runtime%20%3D%20next(k%20for%20k%20in%20_runtimes%20if%20%22GPU%22%20in%20k%20or%20%22float32%22%20in%20k)%0A%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%0A%20%20%20%20%23%20Everything%20that%20differs%20between%20the%20two%20designs%2C%20in%20one%20place%3A%20the%20name%2C%0A%20%20%20%20%23%20its%20dimensions%2C%20and%20where%20on%20the%20frequency%20axis%20its%20parameters%20sit%20(the%0A%20%20%20%20%23%20design's%20own%20band%20layout%2C%20not%20the%20estimator's%20--%20they%20coincide%20at%20the%0A%20%20%20%20%23%20octave%20centres%20and%20nowhere%20else).%0A%20%20%20%20_designs%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%22First-order%20shelf%20%E2%80%94%202%20numbers%2C%201%20biquad%22%3A%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22first_order_shelf%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%202%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%20np.array(%5B1.0%2C%20fs%20%2F%202%5D)%2C%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%22Ten-band%20graphic%20EQ%20%E2%80%94%2010%20numbers%2C%2011%20biquads%22%3A%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22graphic_eq%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%2010%2C%0A%20%20%20%20%20%20%20%20%20%20%20%2011%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20np.concatenate((%5B1.0%5D%2C%20CENTER_FREQUENCIES%2C%20%5Bfs%20%2F%202%5D))%2C%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%7D%0A%20%20%20%20design_choice%20%3D%20mo.ui.dropdown(%0A%20%20%20%20%20%20%20%20options%3D_designs%2C%0A%20%20%20%20%20%20%20%20value%3D%22First-order%20shelf%20%E2%80%94%202%20numbers%2C%201%20biquad%22%2C%0A%20%20%20%20%20%20%20%20label%3D%22EQ%20design%22%2C%0A%20%20%20%20)%0A%0A%20%20%20%20mo.hstack(%5Bruntime_choice%2C%20design_choice%5D%2C%20justify%3D%22start%22%2C%20gap%3D3)%0A%20%20%20%20return%20design_choice%2C%20fs%2C%20runtime_choice%0A%0A%0A%40app.cell%0Adef%20_(design_choice%2C%20runtime_choice%2C%20torch)%3A%0A%20%20%20%20device%2C%20dtype%20%3D%20runtime_choice.value%0A%20%20%20%20design%2C%20n_parameters%2C%20n_sections%2C%20param_frequencies%20%3D%20design_choice.value%0A%0A%20%20%20%20print(f%22device%3A%20%20%20%20%20%20%7Bdevice%7D%20(%7Btorch.get_num_threads()%7D%20CPU%20threads%20available)%22)%0A%20%20%20%20print(f%22dtype%3A%20%20%20%20%20%20%20%7Bstr(dtype).removeprefix('torch.')%7D%22)%0A%20%20%20%20_plural%20%3D%20%22biquad%22%20if%20n_sections%20%3D%3D%201%20else%20%22biquads%22%0A%20%20%20%20print(f%22design%3A%20%20%20%20%20%20%7Bdesign%7D%20%20%E2%80%94%20%20%7Bn_parameters%7D%20numbers%2C%20%7Bn_sections%7D%20%7B_plural%7D%22)%0A%20%20%20%20print(f%22parameters%20sit%20at%20(Hz)%3A%20%7Bparam_frequencies.round(0)%7D%22)%0A%0A%20%20%20%20%23%20Try%20this%3A%0A%20%20%20%20%23%20%20%20pick%20the%20other%20EQ%20design%20-%3E%20the%20whole%20notebook%20re-fits%20and%20re-measures.%0A%20%20%20%20%23%20%20%20%20%20%20Ten%20numbers%20reach%20a%2035%25%20lower%20loss%20than%20two%20and%20a%20*worse*%20mean%20RT%0A%20%20%20%20%23%20%20%20%20%20%20error%2C%20by%20being%20wrong%20in%20completely%20different%20bands.%20Budget%20five%0A%20%20%20%20%23%20%20%20%20%20%20times%20the%20wall%20clock%20for%20it.%0A%20%20%20%20%23%20%20%20pick%20float64%20-%3E%20the%20same%20answer%2C%20twice%20the%20wall%20clock.%20Worth%20doing%20once%2C%0A%20%20%20%20%23%20%20%20%20%20%20to%20see%20that%20it%20is%20the%20same%20answer.%0A%20%20%20%20return%20design%2C%20device%2C%20dtype%2C%20n_sections%2C%20param_frequencies%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%0A%20%20%20%20%23%20Try%20this%3A%20any%20other%20packaged%20response%20is%20a%20one-word%20change%20--%0A%20%20%20%20%23%20%20%20pyFDN.load_audio(%22s3_r1_o%22)%20%20-%3E%20the%20same%20hall%2C%20receiver%20closer%20to%20the%20source%0A%20%20%20%20%23%20and%20the%20whole%20notebook%20re-fits%20to%20it.%0A%20%20%20%20return%20rir%2C%20rir_len%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%20The%20yardstick%20%E2%80%94%20measured%2C%20then%20put%20away%0A%0A%20%20%20%20Octave-band%20%24T_%7B60%7D%24%20and%20initial%20level%20of%20the%20*target*%2C%20by%20Schroeder%20backward%0A%20%20%20%20integration.%20In%20the%20analytic%20notebook%20these%20numbers%20**are**%20the%20design.%20Here%0A%20%20%20%20they%20are%20the%20exam%20paper%3A%20computed%20once%2C%20fed%20to%20nothing%2C%20and%20compared%20against%0A%20%20%20%20at%20the%20end.%0A%0A%20%20%20%20Note%20the%20shape%20%E2%80%94%202.8%20s%20at%20the%20bottom%2C%201.2%20s%20at%208%20kHz%2C%20and%20a%20fall%20in%20between%0A%20%20%20%20that%20never%20reverses%2C%20with%20a%20plateau%20across%2063%2F125%20Hz%20and%20another%20across%0A%20%20%20%20500%20Hz%2F1%20kHz.%20The%20monotone%20part%20is%20what%20a%20shelf%20can%20reach%3B%20the%20plateaus%20are%0A%20%20%20%20what%20it%20cannot.%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%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%20a%20reverberator%20that%20knows%20nothing%20about%20the%20room%0A%0A%20%20%20%20%60fdn_build_gallery%60%20builds%20the%20whole%20starting%20point%20in%20one%20call%3A%20a%20random%0A%20%20%20%20orthogonal%20feedback%20matrix%2C%20sixteen%20normalized%20gains%2C%20no%20dry%20path%2C%20and%0A%20%20%20%20per-delay%20absorption%20for%20a%20**flat%201%20s%20decay%20in%20every%20band**.%20Only%20the%20delays%0A%20%20%20%20are%20sampled%20separately%2C%20so%20that%20%60distribution%3D%22geometric%22%60%20and%20%60coprime%3DTrue%60%0A%20%20%20%20can%20be%20asked%20for.%0A%0A%20%20%20%20%60init_build%60%20is%20a%20complete%20FDN%2C%20not%20a%20scaffold%20%E2%80%94%20the%20untrained%20render%20further%0A%20%20%20%20down%20is%20just%20this%20build%2C%20so%20nothing%20has%20to%20be%20reconstructed%20by%20hand%20to%20say%0A%20%20%20%20what%20the%20optimizer%20started%20from.%0A%0A%20%20%20%20The%20room%20is%202.8%20s%20at%2063%20Hz%20falling%20to%201.2%20s%20at%208%20kHz.%20A%20flat%201%20s%20start%20is%0A%20%20%20%20therefore%20wrong%20by%20a%20factor%20of%20three%20at%20the%20bottom%20of%20the%20spectrum%20and%20by%20a%0A%20%20%20%20fifth%20at%20the%20top%20%E2%80%94%20and%20wrong%20in%20the%20*opposite%20direction*%20at%20the%20two%20ends%2C%0A%20%20%20%20which%20is%20the%20part%20no%20single%20scalar%20can%20fix.%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%20num_delays%20%3D%2016%0A%20%20%20%20delays%20%3D%20pyFDN.sample_delay_lengths(%0A%20%20%20%20%20%20%20%20num_delays%2C%0A%20%20%20%20%20%20%20%20(700%2C%202500)%2C%20%20%23%20samples%3A%20about%2015-52%20ms%20at%2048%20kHz%0A%20%20%20%20%20%20%20%20distribution%3D%22geometric%22%2C%0A%20%20%20%20%20%20%20%20coprime%3DTrue%2C%0A%20%20%20%20%20%20%20%20sort%3DTrue%2C%0A%20%20%20%20%20%20%20%20rng%3D1%2C%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20rng%3D2%20is%20not%20arbitrary.%20The%20orthogonal%20training%20parametrization%20lives%20on%0A%20%20%20%20%23%20SO(N)%2C%20so%20it%20hands%20a%20det%3C0%20matrix%20back%20with%20its%20last%20column%20flipped%3A%20not%0A%20%20%20%20%23%20the%20matrix%20you%20asked%20for.%20This%20seed%20lands%20in%20SO(N)%2C%20and%20the%20assertion%0A%20%20%20%20%23%20below%20is%20what%20says%20so.%0A%20%20%20%20init_build%20%3D%20pyFDN.fdn_build_gallery(%0A%20%20%20%20%20%20%20%20N%3Dnum_delays%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%3A%20the%20entire%20prior%20knowledge%20of%20the%20room%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%20delays%20%3D%20init_build.delays%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%0A%20%20%20%20%23%20Try%20this%3A%0A%20%20%20%20%23%20%20%20num_delays%20%3D%208%20%20%20%20-%3E%20half%20the%20matrix%2C%20half%20the%20density.%20Does%20the%20fit%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20still%20reach%20the%20room's%20decay%3F%20(it%20does)%20Does%20it%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20sound%20like%20it%3F%20(less%20so%20--%20decay%20is%20not%20density)%0A%20%20%20%20%23%20%20%20(2000%2C%206000)%20%20%20%20%20%20-%3E%20a%20much%20longer%20delay%20range%3A%20sparser%2C%20and%20the%20early%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20part%20of%20the%20tail%20stops%20resembling%20the%20target%0A%20%20%20%20%23%20%20%20rt%3D2.5%20%20%20%20%20%20%20%20%20%20%20%20-%3E%20start%20near%20the%20answer%20instead%20of%20far%20from%20it%2C%20and%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20watch%20the%20loss%20curve%20start%20an%20order%20of%20magnitude%20down%0A%20%20%20%20return%20delays%2C%20init_build%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%20wraps%20the%20build%20into%20a%20FLAMO%20model.%20Two%20of%20the%20hooks%0A%20%20%20%20get%20a%20**module%20with%20a%20parameter**%20rather%20than%20a%20baked%20filter%3A%0A%0A%20%20%20%20%7C%20what%20%7C%20which%20parameter%20%7C%20how%20%7C%0A%20%20%20%20%7C---%7C---%7C---%7C%0A%20%20%20%20%7C%20**decay**%20%7C%20%60post_delay%60%20hook%20%7C%20trained%20%E2%80%94%20as%20reverberation%20time%20in%20seconds%2C%20via%20%60pyFDN.AttenuationFilter%60%20%7C%0A%20%20%20%20%7C%20**colour**%20%7C%20%60post_output%60%20hook%20%7C%20trained%20%E2%80%94%20as%20gain%20in%20dB%2C%20via%20%60pyFDN.OutputEQ%60%20%7C%0A%20%20%20%20%7C%20fine%20structure%20of%20%24%5Clvert%20H%20%5Crvert%24%20%7C%20feedback%20matrix%20%24A%24%20%7C%20trained%20%E2%80%94%20on%20%24SO(N)%24%20%7C%0A%20%20%20%20%7C%20level%20%7C%20gains%20%24b%24%2C%20%24c%24%20%7C%20trained%20%7C%0A%20%20%20%20%7C%20dry%20path%20%7C%20%24D%24%20%7C%20trained%20%7C%0A%20%20%20%20%7C%20when%20the%20echoes%20fall%20%7C%20delays%20%7C%20**fixed**%20%E2%80%94%20integer%20sample%20counts%2C%20no%20gradient%20to%20take%20%7C%0A%0A%20%20%20%20Both%20filters%20are%20parametrized%20by%20the%20**quantity%20you%20would%20plot**%2C%20not%20by%0A%20%20%20%20biquad%20coefficients.%20That%20is%20not%20a%20convenience%3B%20it%20is%20what%20makes%20the%20fit%0A%20%20%20%20converge%20at%20all.%20The%20two%20panels%20below%20are%20the%20argument.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.accordion(%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Why%20the%20decay%20is%20trained%20as%20a%20reverberation%20time%22%3A%20mo.md(r%22%22%22%0A%20%20%20%20Training%20the%20attenuation%20filter's%20*coefficients*%20does%20not%20work%2C%20and%20not%20for%0A%20%20%20%20want%20of%20tuning.%20A%20too-quiet%20FDN%20offers%20any%20loss%20the%20same%20cheap%20direction%20%E2%80%94%0A%20%20%20%20more%20loop%20gain%20%E2%80%94%20so%20a%20raw%20SOS%20cascade%2C%20with%20nothing%20holding%20its%20poles%20inside%0A%20%20%20%20the%20unit%20circle%2C%20walks%20straight%20out%20of%20it.%20At%20%60lr%3D3e-2%60%20and%20at%20%60lr%3D1e-3%60%0A%20%20%20%20alike%20the%20fit%20diverges%20within%20fifty%20steps%2C%20the%20loss%20ends%20four%20orders%20of%0A%20%20%20%20magnitude%20*above*%20where%20it%20started%2C%20and%20the%20extracted%20FDN%20renders%20as%20%60nan%60.%0A%0A%20%20%20%20%60pyFDN.AttenuationFilter%60%20rewrites%20the%20filter%20as%20a%20differentiable%20function%20of%20the%0A%20%20%20%20reverberation%20time%3A%0A%0A%20%20%20%20%24%24%5Cmathrm%7BRT%7D_k%20%5C%3B%5Clongrightarrow%5C%3B%20%5Cunderbrace%7B-60%5C%2Cd_i%20%2F%20(%5Cmathrm%7BRT%7D_k%20f_s)%7D_%5Ctext%7BdB%20per%20round%20trip%7D%20%5C%3B%5Clongrightarrow%5C%3B%20%5Ctext%7Bdesign%7D%20%5C%3B%5Clongrightarrow%5C%3B%20%5Ctext%7Bbiquads%7D%24%24%0A%0A%20%20%20%20A%20positive%20RT%20means%20a%20negative%20dB%20attenuation%2C%20which%20means%20a%20contractive%20loop%0A%20%20%20%20%E2%80%94%20for%20**every**%20value%20the%20parameter%20can%20take.%20One%20RT%20per%20band%20is%20shared%20by%0A%20%20%20%20all%20%24N%24%20delay%20lines%2C%20and%20what%20differs%20between%20them%20is%20only%20the%20round-trip%0A%20%20%20%20length%20%24d_i%24%3A%20exactly%20the%20homogeneous%20decay%20an%20FDN%20is%20designed%20for.%0A%0A%20%20%20%20What%20the%20RT%20still%20needs%20a%20floor%20for%20is%20the%20*sign*.%20A%20gradient%20step%20that%20puts%0A%20%20%20%20a%20band%20at%20or%20below%20zero%20turns%20%24-60%20d_i%20%2F%20(%5Cmathrm%7BRT%7D%20f_s)%24%20from%20an%0A%20%20%20%20attenuation%20into%20a%20gain.%20%60AttenuationFilter%60%20floors%20it%20the%20same%20way%20whatever%20the%0A%20%20%20%20design%20%E2%80%94%20softplus%2C%20one%20round%20trip%20of%20the%20longest%20delay%20line%2C%20a%20knee%20one%20floor%0A%20%20%20%20wide%20%E2%80%94%20so%20a%20band%20that%20dips%20across%20zero%20still%20has%20a%20gradient%20to%20come%20back%20on.%0A%20%20%20%20%22%22%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Why%20an%20output%20EQ%20has%20to%20be%20in%20the%20model%20at%20all%22%3A%20mo.md(r%22%22%22%0A%20%20%20%20The%20gains%20%24b%24%20and%20%24c%24%20are%20one%20frequency-flat%20number%20per%20delay%20line%2C%20so%20no%0A%20%20%20%20setting%20of%20them%20is%20a%20filter%3A%20on%20its%20own%2C%20an%20FDN%20can%20place%20its%20band%20*decays*%0A%20%20%20%20but%20not%20its%20band%20*levels*.%20%60pyFDN.OutputEQ%60%20adds%20the%20one%20module%20that%20can%20%E2%80%94%0A%20%20%20%20the%20output%20EQ%2C%20which%20sits%20outside%20the%20recursion%20%E2%80%94%20and%20parametrizes%20it%20the%0A%20%20%20%20same%20way%20the%20decay%20is%20parametrized%2C%20by%20gain%20in%20dB%20rather%20than%20by%20free%20biquad%0A%20%20%20%20coefficients.%20It%20starts%20flat.%20Being%20outside%20the%20loop%20it%20constrains%20nothing%2C%0A%20%20%20%20so%20unlike%20the%20decay%20it%20needs%20no%20floor%20and%20no%20bound.%0A%0A%20%20%20%20%22Outside%20the%20recursion%22%20invites%20one%20wrong%20reading%2C%20so%3A%20the%20output%20EQ%20**is**%0A%20%20%20%20an%20ordinary%20member%20of%20the%20FLAMO%20graph.%20%60assemble_fdn_core%60%20wires%20it%20in%20after%0A%20%20%20%20the%20output%20gain%20as%20a%20leaf%20named%20%60post_output%60%2C%20the%20same%20optimizer%20steps%20it%20as%0A%20%20%20%20steps%20the%20feedback%20matrix%2C%20and%20it%20is%20in%20every%20render%20below.%20Outside%20the%0A%20%20%20%20*recursion*%20is%20a%20statement%20about%20where%20it%20sits%20in%20the%20signal%20flow%20%E2%80%94%20which%20is%0A%20%20%20%20exactly%20why%20it%20can%20shape%20the%20spectrum%20without%20touching%20the%20decay%20%E2%80%94%20not%20about%0A%20%20%20%20it%20being%20applied%20separately%20afterwards.%0A%0A%20%20%20%20In%20the%20analytic%20pipeline%20this%20same%20filter%20is%20*designed*%2C%20once%2C%20from%20the%0A%20%20%20%20residual%20between%20the%20target's%20band%20levels%20and%20the%20FDN's.%20Here%20it%20is%20another%0A%20%20%20%20parameter%2C%20and%20the%20last%20section%20checks%20what%20residual%20is%20left%20over.%0A%20%20%20%20%22%22%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Why%20nfft%20%3D%202**17%2C%20and%20why%20it%20is%20not%20a%20performance%20knob%22%3A%20mo.md(r%22%22%22%0A%20%20%20%20%242%5E%7B17%7D%24%20is%202.73%20s%20at%2048%20kHz%2C%20and%20it%20is%20chosen%20once%20and%20used%20for%20everything.%0A%20%20%20%20Both%20jobs%20it%20has%20to%20do%20put%20a%20floor%20under%20it.%20The%20loss%20compares%20this%20window%0A%20%20%20%20against%20the%20target%2C%20so%20it%20has%20to%20hold%20the%20decay%20being%20fitted%3B%20and%20the%20*same*%0A%20%20%20%20render%20is%20what%20the%20octave-band%20estimators%20at%20the%20bottom%20measure%2C%20where%0A%20%20%20%20Schroeder%20integration%20over%20a%20window%20shorter%20than%20the%20decay%20under-reads%20it.%0A%20%20%20%202.73%20s%20clears%20both%3A%20the%20target%20has%20only%20%24-72%24%20dB%20of%20its%20energy%20left%20after%20it%2C%0A%20%20%20%20and%20the%20band%20RTs%20come%20out%20equal%20to%20three%20decimals%20against%20a%20render%20four%20times%0A%20%20%20%20as%20long.%0A%0A%20%20%20%20That%20is%20what%20lets%20the%20trained%20model%20be%20measured%20directly%2C%20rather%20than%0A%20%20%20%20exported%20to%20an%20%60FDNBuild%60%20and%20re-rendered%20at%20some%20larger%20%60nfft%60.%20The%20reason%0A%20%20%20%20such%20a%20round%20trip%20is%20otherwise%20needed%20is%20that%20%60nfft%60%20is%20**structural**%20in%0A%20%20%20%20FLAMO%20%E2%80%94%20it%20fixes%20the%20frequency%20grid%2C%20the%20delay%20phase%20ramps%20and%20the%20alias%0A%20%20%20%20envelope%20of%20every%20module%20at%20construction%2C%20and%20there%20is%20no%20setter%20%E2%80%94%20so%20a%0A%20%20%20%20render%20at%20a%20different%20length%20means%20rebuilding.%0A%0A%20%20%20%20One%20argument%20is%20deliberately%20*not*%20passed%3A%20%60alias_decay_db%60.%20Evaluating%20an%0A%20%20%20%20FDN%20as%20%24(I%20-%20A%20D(z))%5E%7B-1%7D%24%20on%20the%20DFT%20grid%20renders%20one%20period%20of%20a%20*periodic*%0A%20%20%20%20signal%2C%20so%20whatever%20the%20true%20response%20still%20has%20beyond%20%60nfft%60%20wraps%20back%0A%20%20%20%20around%20to%20the%20start%20of%20the%20buffer.%20A%20**lossless**%20FDN%20cannot%20do%20without%20the%0A%20%20%20%20correction%2C%20since%20its%20poles%20sit%20exactly%20on%20the%20unit%20circle%20where%20the%20inverse%0A%20%20%20%20is%20near-singular.%20This%20one%20decays%2C%20far%20enough%20down%20by%20the%20end%20of%20the%20training%0A%20%20%20%20window%20that%20the%20wrap-around%20is%20inaudible%20against%20it.%20In%20%60float32%60%20the%20setting%0A%20%20%20%20is%20actively%20*harmful*%3A%20the%20%24%5Cgamma%5En%24%20reconstruction%20amplifies%20round-off%20at%0A%20%20%20%20the%20end%20of%20the%20buffer%20by%20the%20same%20factor%20it%20suppresses%20aliasing%2C%20which%20is%0A%20%20%20%20exactly%20where%20backward%20integration%20reads.%0A%20%20%20%20%22%22%22)%2C%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(design%2C%20device%2C%20dtype%2C%20fs%2C%20init_build%2C%20np%2C%20pyFDN%2C%20rir%2C%20rir_len%2C%20torch)%3A%0A%20%20%20%20nfft%20%3D%202**17%20%20%23%202.73%20s%20at%2048%20kHz%20%E2%80%94%20long%20enough%20for%20the%20loss%20and%20the%20metrics%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%20the%20decay%2C%20as%20a%20reverberation%20time%20rather%20than%20as%20coefficients%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%3Ddesign%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20nfft%3Dnfft%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%3Ddtype%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%3Ddesign%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20nfft%3Dnfft%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%3Ddtype%2C%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20nfft%3Dnfft%2C%0A%20%20%20%20%20%20%20%20device%3Ddevice%2C%0A%20%20%20%20%20%20%20%20dtype%3Ddtype%2C%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20One%20excitation%2C%20reused%20by%20every%20render.%20Building%20it%20explicitly%20is%20what%0A%20%20%20%20%23%20keeps%20this%20working%20on%20a%20GPU%3A%20the%20default%20one%20is%20made%20on%20the%20CPU.%0A%20%20%20%20excitation%20%3D%20pyFDN.impulse_excitation(1%2C%20nfft%2C%20device%3Ddevice%2C%20dtype%3Ddtype)%0A%0A%20%20%20%20def%20render(m)%3A%0A%20%20%20%20%20%20%20%20%22%22%22The%20FDN's%20impulse%20response%2C%20straight%20out%20of%20the%20FLAMO%20model.%22%22%22%0A%20%20%20%20%20%20%20%20h%20%3D%20pyFDN.model_response(m%2C%20excitation).h.detach().cpu()%0A%20%20%20%20%20%20%20%20return%20np.asarray(h%2C%20dtype%3Dnp.float64).reshape(-1)%5B%3Arir_len%5D%0A%0A%20%20%20%20def%20read(name)%3A%0A%20%20%20%20%20%20%20%20%22%22%22A%20parameter%20and%20the%20filter%20it%20designs%2C%20as%20numpy.%22%22%22%0A%20%20%20%20%20%20%20%20p%20%3D%20pyFDN.param(model%2C%20name)%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20p.raw().detach().cpu().numpy().copy().ravel()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20p.value().detach().cpu().numpy().copy()%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%23%20the%20only%20thing%20the%20target%20tells%20the%20initial%20model%3A%20how%20loud%20it%20is%0A%20%20%20%20energy_gain%20%3D%20float(np.linalg.norm(rir%5B%3Anfft%5D)%20%2F%20np.linalg.norm(render(model)))%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%0A%20%20%20%20%23%20the%20untrained%20FDN%2C%20before%20the%20optimizer%20touches%20it.%20Taken%20here%2C%20in%20this%0A%20%20%20%20%23%20same%20cell%2C%20because%20train_fdn%20steps%20%60model%60%20in%20place%20--%20once%20the%20next%20cell%0A%20%20%20%20%23%20has%20run%20there%20is%20no%20%22before%22%20left.%0A%20%20%20%20ir_init%20%3D%20render(model)%0A%20%20%20%20_%2C%20init_sos%20%3D%20read(%22post_delay%22)%0A%0A%20%20%20%20for%20_p%20in%20pyFDN.params(model)%3A%0A%20%20%20%20%20%20%20%20print(f%22%7B_p%7D%20%20raw%20%7Btuple(_p.raw().shape)%7D%22)%0A%20%20%20%20print(f%22%5Cnenergy%20match%3A%20output%20gain%20x%20%7Benergy_gain%3A.2f%7D%22)%0A%0A%20%20%20%20%23%20Try%20this%3A%20comment%20out%20the%20energy%20match%20and%20re-run.%20The%20fit%20spends%20its%0A%20%20%20%20%23%20first%20steps%20on%20a%20volume%20knob%2C%20and%20the%20loss%20is%20normalized%20by%20the%20target's%0A%20%20%20%20%23%20energy%2C%20so%20an%20FDN%20two%20decades%20too%20quiet%20starts%20on%20the%20flat%20part%20of%20the%0A%20%20%20%20%23%20compression%20curve%20where%20there%20is%20little%20gradient%20to%20follow.%20What%20the%0A%20%20%20%20%23%20scalar%20cannot%20do%20is%20say%20*when*%20that%20energy%20arrives%2C%20which%20is%20the%20problem.%0A%20%20%20%20return%20init_sos%2C%20ir_init%2C%20model%2C%20read%2C%20render%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%20a%20loss%20that%20can%20see%20a%20decay%0A%0A%20%20%20%20This%20is%20the%20step%20worth%20dwelling%20on%2C%20because%20the%20obvious%20objective%20gets%20it%0A%20%20%20%20confidently%20wrong.%20%60pyFDN.MatchCumulativeEnergy%60%20compares%20the%20short-time%0A%20%20%20%20energy%20of%20the%20two%20responses%2C%20**cumulated%20twice**%20%E2%80%94%20backwards%20in%20time%2C%20and%0A%20%20%20%20along%20frequency%3A%0A%0A%20%20%20%20%24%24E%5Bf%2C%20t%5D%20%5C%3B%3D%5C%3B%20%5Csum_%7Bt'%20%5Cge%20t%7D%20%5C%3B%20%5Csum_%7Bf'%20%5Cge%20f%7D%20%5Cbig%7C%20S%5Bf'%2C%20t'%5D%20%5Cbig%7C%5E2%24%24%0A%0A%20%20%20%20The%20time%20direction%20is%20Schroeder%20backward%20integration%20%E2%80%94%20the%20decay%20itself.%20The%0A%20%20%20%20frequency%20direction%20does%20the%20job%20that%20**splitting%20into%20octave%20bands**%20usually%0A%20%20%20%20does%2C%20and%20does%20it%20without%20band%20edges%3A%20a%20cumulative%20sum%20compares%20every%20bin%0A%20%20%20%20against%20every%20wider%20band%20that%20contains%20it%2C%20all%20at%20once%2C%20and%20is%20monotone%20and%0A%20%20%20%20smooth%20in%20both%20axes%2C%20which%20is%20worth%20a%20great%20deal%20to%20a%20gradient.%20Read%20down%20the%0A%20%20%20%20%24t%20%3D%200%24%20edge%20and%20the%20surface%20is%20the%20integrated%20spectrum%3B%20read%20across%20the%0A%20%20%20%20%24f%20%3D%200%24%20edge%20and%20it%20is%20the%20full-band%20energy%20decay%20curve%3B%20the%20interior%20ties%0A%20%20%20%20the%20two%20together.%20One%20loss%2C%20both%20quantities%2C%20no%20bands.%0A%0A%20%20%20%20There%20is%20one%20term%20and%20no%20weight%20to%20tune%2C%20because%20there%20is%20nothing%20to%20weigh%20it%0A%20%20%20%20against%3A%20the%20surface%20carries%20decay%20and%20colour%20together%2C%20so%20a%20second%20term%0A%20%20%20%20would%20only%20be%20a%20second%20opinion%20about%20the%20same%20data.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.accordion(%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Why%20not%20a%20spectrogram%20distance%20%E2%80%94%20the%20table%20where%20it%20fails%22%3A%20mo.md(r%22%22%22%0A%20%20%20%20Freeze%20everything%20except%20the%20decay%2C%20scale%20a%20measured%20RT%20by%20a%20constant%2C%20and%0A%20%20%20%20score%20the%20result%20on%20a%20mel%20multi-resolution%20spectrogram%20distance%20alone%3A%0A%0A%20%20%20%20%7C%20RT%20scale%20%7C%200.4%20%7C%200.6%20%7C%20**1.0**%20%7C%201.3%20%7C%201.6%20%7C%0A%20%20%20%20%7C---%7C---%7C---%7C---%7C---%7C---%7C%0A%20%20%20%20%7C%20mel%20MSS%20(%24%5Ctimes%2010%5E%7B-5%7D%24)%20%7C%201.819%20%7C%20**1.804**%20%7C%201.855%20%7C%201.924%20%7C%202.009%20%7C%0A%20%20%20%20%7C%20energy%20decay%20at%201%20s%20%7C%20-67%20dB%20%7C%20-47%20dB%20%7C%20**-31%20dB**%20%7C%20-25%20dB%20%7C%20-21%20dB%20%7C%0A%20%20%20%20%7C%20the%20room%2C%20at%201%20s%20%7C%20%7C%20%7C%20**-29%20dB**%20%7C%20%7C%20%7C%0A%0A%20%20%20%20The%20minimum%20is%20at%200.6%20%E2%80%94%20an%20FDN%20whose%20tail%20is%2018%20dB%20below%20the%20room's%20at%20one%0A%20%20%20%20second%20scores%20*better*%20than%20the%20one%20that%20tracks%20it%20to%20within%202%20dB.%20That%20is%0A%20%20%20%20not%20a%20bug%20in%20the%20loss%2C%20it%20is%20what%20a%20magnitude%20distance%20does%3A%20two%20rooms%20with%0A%20%20%20%20the%20same%20decay%20still%20have%20uncorrelated%20fine%20structure%2C%20and%20against%20detail%20you%0A%20%20%20%20cannot%20predict%2C%20silence%20is%20a%20better%20guess%20than%20the%20right%20amount%20of%20the%20wrong%0A%20%20%20%20detail.%0A%20%20%20%20%22%22%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22power%3D0.5%20%E2%80%94%20compression%2C%20not%20decibels%22%3A%20mo.md(r%22%22%22%0A%20%20%20%20A%20cumulated%20energy%20surface%20spans%20the%20entire%20dynamic%20range%20of%20the%20decay%2C%20so%20a%0A%20%20%20%20plain%20MSE%20on%20it%20sees%20the%20first%20few%20frames%20and%20nothing%20else.%20%60power%60%0A%20%20%20%20compresses%20that%20range%20%E2%80%94%20the%20surface%20is%20normalized%20by%20the%20target's%20total%0A%20%20%20%20energy%20and%20raised%20to%20a%20fractional%20power%2C%200.5%20here%2C%20so%20that%20what%20is%20compared%0A%20%20%20%20is%20amplitude%20rather%20than%20energy.%20Lower%20values%20compress%20harder%20and%20move%20weight%0A%20%20%20%20onto%20the%20quiet%20end.%0A%0A%20%20%20%20A%20logarithm%20is%20the%20obvious%20alternative%20and%20is%20worse%3A%20it%20turns%20the%20silence%0A%20%20%20%20*below*%20the%20response%20into%20an%20unbounded%20penalty%2C%20so%20whichever%20bin%20is%20nearest%0A%20%20%20%20zero%20owns%20the%20gradient.%20A%20power%20keeps%20the%20compressed%20surface%20bounded%20and%20its%0A%20%20%20%20gradient%20finite%2C%20and%20leaves%200%20an%20ordinary%20value%20to%20predict.%0A%20%20%20%20%22%22%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20'frequency%3D%22both%22%20%E2%80%94%20which%20way%20the%20cumulation%20runs%20is%20not%20a%20detail'%3A%20mo.md(r%22%22%22%0A%20%20%20%20Cumulating%20from%20high%20to%20low%20puts%20every%20bin's%20energy%20into%20the%20rows%20*below*%20it%2C%0A%20%20%20%20so%20an%20error%20in%20a%20low%20band%20moves%20only%20the%20largest%20values%20on%20the%20surface%20%E2%80%94%20the%0A%20%20%20%20ones%20compression%20weights%20least%20%E2%80%94%20while%20a%20high%20band%20gets%20rows%20of%20its%20own.%20On%0A%20%20%20%20this%20fit%2C%20with%20everything%20else%20held%20fixed%20at%20300%20steps%2C%20that%20asymmetry%20is%0A%20%20%20%20worth%20more%20than%20the%20compression%20exponent%3A%0A%0A%20%20%20%20%7C%20%60frequency%60%20%7C%20%60power%60%20%7C%20mean%20RT%20error%20%7C%20level%20shape%20%7C%20trained%20RT%20at%2063%20Hz%20%7C%0A%20%20%20%20%7C---%7C---%7C---%7C---%7C---%7C%0A%20%20%20%20%7C%20%60%22descending%22%60%20(high%20%E2%86%92%20low)%20%7C%200.5%20%7C%2016.8%20%25%20%7C%202.56%20dB%20%7C%200.26%20s%20%7C%0A%20%20%20%20%7C%20%60%22descending%22%60%20%7C%200.25%20%7C%2019.0%20%25%20%7C%201.46%20dB%20%7C%200.44%20s%20%7C%0A%20%20%20%20%7C%20%60%22ascending%22%60%20%7C%200.5%20%7C%2012.8%20%25%20%7C%201.12%20dB%20%7C%202.35%20s%20%7C%0A%20%20%20%20%7C%20**%60%22both%22%60**%20%7C%20**0.5**%20%7C%20**10.3%20%25**%20%7C%20**0.88%20dB**%20%7C%20**2.17%20s**%20%7C%0A%0A%20%20%20%20(the%20room%20is%202.8%20s%20at%2063%20Hz).%20Cumulating%20downwards%20alone%20leaves%20the%20bottom%0A%20%20%20%20octave%20with%20essentially%20no%20gradient%20and%20the%20fit%20abandons%20it%3B%20%60%22both%22%60%20scores%0A%20%20%20%20the%20two%20directions%20separately%20%E2%80%94%20each%20normalized%20and%20compressed%20on%20its%20own%2C%0A%20%20%20%20then%20averaged%20%E2%80%94%20and%20the%20low%20bands%20come%20back.%20Lengthening%20the%20analysis%20window%0A%20%20%20%20does%20*not*%20fix%20it%20(15.4%20%25%20at%20%60window%3D4096%60%2C%2063%20Hz%20still%20at%200.20%20s)%2C%20which%20is%0A%20%20%20%20the%20evidence%20that%20the%20problem%20is%20weighting%20rather%20than%20frequency%20resolution.%0A%0A%20%20%20%20Those%20rows%20were%20measured%20at%20a%20different%20feedback-matrix%20seed%20and%20a%20shorter%0A%20%20%20%20%60nfft%60%20than%20this%20notebook%20now%20uses.%20Compare%20the%20rows%20against%20each%20other%2C%20not%0A%20%20%20%20against%20the%20result%20further%20down%3A%20the%20gaps%20between%20them%20are%20far%20larger%20than%0A%20%20%20%20the%20offset.%0A%20%20%20%20%22%22%22)%2C%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20)%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%0A%20%20%20%20%23%20Try%20this%20%E2%80%94%20each%20one%20is%20a%20row%20of%20the%20tables%20above%2C%20run%20for%20yourself%3A%0A%20%20%20%20%23%20%20%20frequency%3D%22descending%22%20%20-%3E%20the%20bottom%20octave%20is%20abandoned%3B%20look%20at%20the%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20trained%20RT%20curve%20at%2063%20Hz%0A%20%20%20%20%23%20%20%20power%3D0.25%20%20%20%20%20%20%20%20%20%20%20%20%20%20-%3E%20harder%20compression%2C%20more%20weight%20on%20the%20tail%0A%20%20%20%20%23%20%20%20window%3D4096%20%20%20%20%20%20%20%20%20%20%20%20%20-%3E%20longer%20analysis%20window%3B%20slower%2C%20and%20no%20better%0A%20%20%20%20%23%0A%20%20%20%20%23%20Or%20replace%20the%20objective%20outright%20and%20watch%20the%20decay%20go%20wrong%3A%0A%20%20%20%20%23%20%20%20loss%20%3D%20pyFDN.MatchMelSpectrogram(rir)%0A%20%20%20%20%23%20%20%20loss%20%3D%20pyFDN.MatchEnergyDecay(rir%2C%20window%3D4096)%20%20%20%23%20bands%2C%20not%20cumulation%0A%20%20%20%20%23%20%20%20loss%20%3D%20pyFDN.MatchCumulativeEnergy(rir)%20%2B%200.1%20*%20pyFDN.MatchMelSpectrogram(rir)%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%20300%20Adam%20steps%20at%20%60lr%3D3e-2%60.%20On%20a%20CPU%20that%20is%20a%20minute%20or%20two%3B%20watch%20the%20loss%0A%20%20%20%20curve%20below%20rather%20than%20the%20clock.%0A%0A%20%20%20%20The%20trained%20response%20is%20rendered%20at%20the%20end%20of%20the%20same%20cell%2C%20out%20of%20the%20same%0A%20%20%20%20model%2C%20through%20the%20same%20%60render%60%20the%20untrained%20one%20went%20through%3A%20two%20FDNs%0A%20%20%20%20being%20compared%20on%20a%20metric%20should%20not%20be%20reaching%20it%20by%20two%20different%20routes.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(device%2C%20dtype%2C%20loss%2C%20model%2C%20pyFDN%2C%20read%2C%20render)%3A%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%20optimizer%3D%22adam%22%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%20dtype%3Ddtype%2C%0A%20%20%20%20%20%20%20%20rng%3D0%2C%0A%20%20%20%20)%0A%0A%20%20%20%20trained_rt%2C%20trained_sos%20%3D%20read(%22post_delay%22)%0A%20%20%20%20trained_eq_db%2C%20trained_eq_sos%20%3D%20read(%22post_output%22)%0A%20%20%20%20ir_trained%20%3D%20render(model)%0A%0A%20%20%20%20print(%0A%20%20%20%20%20%20%20%20f%22ran%20%7Blog.steps_run%7D%20steps%2C%20loss%20%7Blog.train_loss%5B0%5D%3A.4g%7D%20-%3E%20%22%0A%20%20%20%20%20%20%20%20f%22%7Blog.train_loss%5B-1%5D%3A.4g%7D%20%22%0A%20%20%20%20%20%20%20%20f%22(%7B100%20*%20(1%20-%20log.train_loss%5B-1%5D%20%2F%20log.train_loss%5B0%5D)%3A.0f%7D%25%20down)%22%0A%20%20%20%20)%0A%20%20%20%20print(f%22%5Cntrained%20RT%20(s)%3A%20%20%20%20%20%20%20%20%20%7Btrained_rt.round(2)%7D%22)%0A%20%20%20%20print(f%22trained%20output%20EQ%20(dB)%3A%20%7Btrained_eq_db.round(1)%7D%22)%0A%20%20%20%20print(f%22%5Cnin-loop%20filter%3A%20%7Btrained_sos.shape%7D%20%E2%80%94%20per%20delay%20line%22)%0A%20%20%20%20print(f%22output%20filter%3A%20%20%7Btrained_eq_sos.shape%7D%22)%0A%0A%20%20%20%20%23%20Try%20this%20%E2%80%94%20break%20it%20on%20purpose%2C%20and%20say%20from%20the%20loss%20curve%20what%20failed%3A%0A%20%20%20%20%23%20%20%20max_steps%3D20%20%20%20%20%20-%3E%20stopped%2C%20not%20converged%0A%20%20%20%20%23%20%20%20lr%3D1.0%20%20%20%20%20%20%20%20%20%20%20-%3E%20the%20step%20size%20overshoots%20every%20minimum%20it%20finds%0A%20%20%20%20%23%20%20%20optimizer%3D%22lbfgs%22%0A%20%20%20%20%23%20%20%20rng%3D1%2C%20rng%3D2%20%20%20%20%20-%3E%20a%20different%20random%20start.%20The%20mean%20RT%20error%20moves%20by%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20a%20few%20tenths%20of%20a%20point%2C%20not%20by%20points%3A%20the%20result%20is%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20a%20property%20of%20the%20fit%2C%20not%20of%20one%20lucky%20matrix.%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20ir_trained%2C%0A%20%20%20%20%20%20%20%20log%2C%0A%20%20%20%20%20%20%20%20trained_eq_db%2C%0A%20%20%20%20%20%20%20%20trained_eq_sos%2C%0A%20%20%20%20%20%20%20%20trained_rt%2C%0A%20%20%20%20%20%20%20%20trained_sos%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell%0Adef%20_(go%2C%20log)%3A%0A%20%20%20%20_fig%20%3D%20go.Figure()%0A%20%20%20%20_fig.add_trace(go.Scatter(y%3Dlog.train_loss%2C%20mode%3D%22lines%22))%0A%20%20%20%20_fig.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22Training%20loss%22%2C%0A%20%20%20%20%20%20%20%20xaxis%3D%7B%22title%22%3A%20%22step%22%7D%2C%0A%20%20%20%20%20%20%20%20yaxis%3D%7B%22title%22%3A%20%22cumulative-energy%20RMS%20error%22%2C%20%22type%22%3A%20%22log%22%7D%2C%0A%20%20%20%20%20%20%20%20template%3D%22plotly_white%22%2C%0A%20%20%20%20%20%20%20%20height%3D380%2C%0A%20%20%20%20)%0A%20%20%20%20_fig.show()%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%20the%20two%20filters%20are%20the%20answer%0A%0A%20%20%20%20Both%20trained%20quantities%20are%20things%20you%20would%20plot%20anyway%20%E2%80%94%20a%20reverberation%0A%20%20%20%20time%20in%20seconds%20and%20an%20output%20EQ%20in%20dB%20%E2%80%94%20and%20neither%20needs%20an%20estimator%20to%0A%20%20%20%20read.%20But%20the%20thing%20to%20look%20at%20is%20the%20**curve**%20each%20set%20of%20numbers%20designs%2C%0A%20%20%20%20because%20that%20is%20what%20the%20FDN%20runs.%20The%20parameters%20are%20drawn%20on%20top%20of%20it%20as%0A%20%20%20%20markers%2C%20at%20the%20frequencies%20they%20actually%20sit%20at.%0A%0A%20%20%20%20The%20decay%20is%20plotted%20as%20reverberation%20time%20against%20frequency%2C%20read%20off%20the%0A%20%20%20%20fitted%20filter's%20gain%20per%20sample%2C%0A%20%20%20%20%24%5Cmathrm%7BRT%7D(f)%20%3D%20-60%20%2F%20(f_s%20%5Ccdot%2020%5Clog_%7B10%7D%20g(f))%24%2C%20which%20is%0A%20%20%20%20delay-line-independent%20because%20the%20decay%20is%20homogeneous.%0A%0A%20%20%20%20Read%20the%20ends%20of%20either%20curve%20with%20care.%20The%20outermost%20parameters%20sit%20at%20DC%0A%20%20%20%20and%20at%20**24%20kHz**%20%E2%80%94%20an%20octave%20and%20a%20half%20above%20the%20highest%20band%20the%20estimator%0A%20%20%20%20reports%2C%20and%20well%20into%20where%20the%20recording%20has%20nothing%20left%20%E2%80%94%20so%20they%20are%0A%20%20%20%20free%20to%20go%20anywhere.%20The%20shelf's%20Nyquist%20endpoint%20settles%20around%200.2%20s%3B%20the%0A%20%20%20%20graphic%20EQ's%20goes%20negative%20outright%2C%20and%20the%20parametrization%20floors%20it.%0A%20%20%20%20Neither%20is%20a%20claim%20about%208%20kHz%3A%20what%20the%20FDN%20does%20there%20is%20the%20*curve*%20at%0A%20%20%20%208%20kHz.%20At%20the%20edges%20of%20a%20design%2C%20read%20the%20filter%2C%20not%20the%20parameter.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20delays%2C%0A%20%20%20%20est_rt%2C%0A%20%20%20%20f_centre%2C%0A%20%20%20%20fs%2C%0A%20%20%20%20go%2C%0A%20%20%20%20init_sos%2C%0A%20%20%20%20np%2C%0A%20%20%20%20param_frequencies%2C%0A%20%20%20%20pyFDN%2C%0A%20%20%20%20trained_rt%2C%0A%20%20%20%20trained_sos%2C%0A)%3A%0A%20%20%20%20def%20_rt_curve(sos)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Reverberation%20time%20vs%20frequency%20implied%20by%20a%20homogeneous%20decay%20filter.%22%22%22%0A%20%20%20%20%20%20%20%20angles%2C%20magnitude%20%3D%20pyFDN.sos_gain_per_sample_curves(sos%2C%20delays%2C%20512)%0A%20%20%20%20%20%20%20%20freqs%20%3D%20angles%20%2F%20np.pi%20*%20(fs%20%2F%202)%0A%20%20%20%20%20%20%20%20return%20freqs%2C%20-60.0%20%2F%20(fs%20*%2020.0%20*%20np.log10(magnitude%5B%3A%2C%200%5D))%0A%0A%20%20%20%20curve_f%2C%20rt_curve%20%3D%20_rt_curve(trained_sos)%0A%20%20%20%20_%2C%20_rt_init_curve%20%3D%20_rt_curve(init_sos)%0A%0A%20%20%20%20_fig%20%3D%20go.Figure()%0A%20%20%20%20_fig.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dcurve_f%2C%20y%3D_rt_init_curve%2C%20name%3D%22initial%20(flat%201%20s)%22%2C%20line%3D%7B%22dash%22%3A%20%22dot%22%7D%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20_fig.add_trace(go.Scatter(x%3Dcurve_f%2C%20y%3Drt_curve%2C%20name%3D%22trained%20filter%22))%0A%20%20%20%20_fig.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dparam_frequencies%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dtrained_rt%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22the%20trained%20parameters%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22markers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20marker%3D%7B%22size%22%3A%2011%2C%20%22symbol%22%3A%20%22diamond%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20_fig.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Df_centre%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dest_rt%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22measured%20in%20the%20RIR%20(never%20seen%20by%20the%20fit)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22lines%2Bmarkers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20line%3D%7B%22dash%22%3A%20%22dash%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20_fig.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22The%20absorption%20parameter%3A%20reverberation%20time%20vs%20frequency%22%2C%0A%20%20%20%20%20%20%20%20xaxis%3D%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22title%22%3A%20%22Frequency%20(Hz)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22log%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22range%22%3A%20%5B0%2C%20np.log10(fs%20%2F%202)%5D%2C%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20yaxis%3D%7B%22title%22%3A%20%22RT%20(s)%22%2C%20%22rangemode%22%3A%20%22tozero%22%7D%2C%0A%20%20%20%20%20%20%20%20template%3D%22plotly_white%22%2C%0A%20%20%20%20%20%20%20%20height%3D380%2C%0A%20%20%20%20)%0A%20%20%20%20_fig.show()%0A%0A%20%20%20%20print(%0A%20%20%20%20%20%20%20%20f%22filter%20RT%20at%20the%20octave%20centres%20(s)%3A%20%22%0A%20%20%20%20%20%20%20%20f%22%7Bnp.interp(f_centre%2C%20curve_f%2C%20rt_curve).round(2)%7D%22%0A%20%20%20%20)%0A%20%20%20%20print(f%22measured%2C%20octave%20bands%20(s)%3A%20%20%20%20%20%20%20%20%20%20%7Best_rt.round(2)%7D%22)%0A%20%20%20%20return%20(rt_curve%2C)%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20go%2C%20np%2C%20param_frequencies%2C%20pyFDN%2C%20trained_eq_db%2C%20trained_eq_sos)%3A%0A%20%20%20%20_probe%20%3D%20np.logspace(0%2C%20np.log10(fs%20%2F%202)%2C%20400)%0A%20%20%20%20%23%20probe_sos%20evaluates%20each%20biquad%20on%20its%20own%2C%20so%20the%20cascade%20is%20their%20sum%20in%20dB%0A%20%20%20%20_db%2C%20_%2C%20_%20%3D%20pyFDN.probe_sos(trained_eq_sos%5B%3A%2C%20%3A%2C%200%5D%2C%20_probe%2C%202**14%2C%20fs)%0A%0A%20%20%20%20_fig%20%3D%20go.Figure()%0A%20%20%20%20_fig.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3D_probe%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dnp.zeros_like(_probe)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22initial%20(flat)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20line%3D%7B%22dash%22%3A%20%22dot%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20_fig.add_trace(go.Scatter(x%3D_probe%2C%20y%3D_db.sum(axis%3D1)%2C%20name%3D%22trained%20filter%22))%0A%20%20%20%20_fig.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dparam_frequencies%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dtrained_eq_db%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22the%20trained%20parameters%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22markers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20marker%3D%7B%22size%22%3A%2011%2C%20%22symbol%22%3A%20%22diamond%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20_fig.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22The%20output%20EQ%20parameter%3A%20gain%22%2C%0A%20%20%20%20%20%20%20%20xaxis%3D%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22title%22%3A%20%22Frequency%20(Hz)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22log%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22range%22%3A%20%5B0%2C%20np.log10(fs%20%2F%202)%5D%2C%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20yaxis%3D%7B%22title%22%3A%20%22Gain%20(dB)%22%7D%2C%0A%20%20%20%20%20%20%20%20template%3D%22plotly_white%22%2C%0A%20%20%20%20%20%20%20%20height%3D380%2C%0A%20%20%20%20)%0A%20%20%20%20_fig.show()%0A%0A%20%20%20%20print(f%22trained%20output%20EQ%20(dB)%3A%20%7Btrained_eq_db.round(1)%7D%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%20Step%207%20%E2%80%94%20did%20you%20get%20what%20you%20asked%20for%3F%0A%0A%20%20%20%20The%20same%20two%20estimators%2C%20now%20applied%20to%20both%20rendered%20FDNs%20%E2%80%94%20against%20a%0A%20%20%20%20measurement%20neither%20of%20them%20saw.%20This%20is%20the%20loop%20that%20matters%20and%20it%20is%20the%0A%20%20%20%20same%20one%20the%20analytic%20notebook%20runs%3A%20*state%20a%20target%2C%20render%2C%20measure%2C%0A%20%20%20%20compare.*%0A%0A%20%20%20%20Two%20things%20are%20worth%20being%20clear%20about%20before%20reading%20the%20numbers.%20The%20level%0A%20%20%20%20**offset**%20is%20not%20an%20achievement%3A%20the%20energy%20match%20set%20it%20before%20the%0A%20%20%20%20optimizer%20ran%2C%20and%20the%20fit%20merely%20kept%20it.%20The%20level%20**shape**%20is%2C%20because%0A%20%20%20%20nothing%20in%20the%20FDN%20proper%20can%20bend%20it%20%E2%80%94%20that%20is%20the%20trained%20output%20EQ%2C%20and%20it%0A%20%20%20%20is%20the%20only%20part%20of%20this%20that%20the%20analytic%20pipeline%20would%20have%20had%20to%20design.%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)%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%20go%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%20pyFDN%2C%0A%20%20%20%20rt_init%2C%0A%20%20%20%20rt_trained%2C%0A)%3A%0A%20%20%20%20from%20plotly.subplots%20import%20make_subplots%0A%0A%20%20%20%20_fig%20%3D%20make_subplots(%0A%20%20%20%20%20%20%20%20rows%3D1%2C%20cols%3D2%2C%20subplot_titles%3D(%22Reverberation%20time%22%2C%20%22Initial%20level%22)%0A%20%20%20%20)%0A%20%20%20%20for%20_name%2C%20_rt%2C%20_lv%2C%20_dash%20in%20(%0A%20%20%20%20%20%20%20%20(%22Target%20RIR%22%2C%20est_rt%2C%20est_level%2C%20None)%2C%0A%20%20%20%20%20%20%20%20(%22FDN%2C%20untrained%22%2C%20rt_init%2C%20level_init%2C%20%22dot%22)%2C%0A%20%20%20%20%20%20%20%20(%22FDN%2C%20trained%22%2C%20rt_trained%2C%20level_trained%2C%20None)%2C%0A%20%20%20%20)%3A%0A%20%20%20%20%20%20%20%20_style%20%3D%20%7B%22dash%22%3A%20_dash%7D%20if%20_dash%20else%20%7B%7D%0A%20%20%20%20%20%20%20%20_fig.add_trace(%0A%20%20%20%20%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%3Df_centre%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20y%3D_rt%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3D_name%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22lines%2Bmarkers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20line%3D_style%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20legendgroup%3D_name%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20row%3D1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20col%3D1%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_fig.add_trace(%0A%20%20%20%20%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%3Df_centre%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20y%3DpyFDN.lin_to_db(_lv)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3D_name%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22lines%2Bmarkers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20line%3D_style%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20legendgroup%3D_name%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20showlegend%3DFalse%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20row%3D1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20col%3D2%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20_fig.update_xaxes(title_text%3D%22Frequency%20(Hz)%22%2C%20type%3D%22log%22)%0A%20%20%20%20_fig.update_yaxes(title_text%3D%22RT%20(s)%22%2C%20rangemode%3D%22tozero%22%2C%20row%3D1%2C%20col%3D1)%0A%20%20%20%20_fig.update_yaxes(title_text%3D%22Initial%20level%20(dB)%22%2C%20row%3D1%2C%20col%3D2)%0A%20%20%20%20_fig.update_layout(template%3D%22plotly_white%22%2C%20height%3D400)%0A%20%20%20%20_fig.show()%0A%0A%20%20%20%20def%20_report(name%2C%20rt%2C%20level)%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%7Bname%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%0A%20%20%20%20_report(%22FDN%2C%20untrained%22%2C%20rt_init%2C%20level_init)%0A%20%20%20%20_report(%22FDN%2C%20trained%22%2C%20rt_trained%2C%20level_trained)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.accordion(%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22What%20ten%20times%20the%20parameters%20actually%20buys%20%E2%80%94%20both%20designs%2C%20side%20by%20side%22%3A%20mo.md(r%22%22%22%0A%20%20%20%20From%20a%20flat%201%20s%20decay%20and%20a%20flat%20EQ%2C%20one%20energy%20match%20and%20300%20gradient%20steps%2C%0A%20%20%20%20with%20the%20design%20switch%20in%20each%20of%20its%20two%20positions%20and%20everything%20else%0A%20%20%20%20identical%3A%0A%0A%20%20%20%20%7C%20%7C%20mean%20RT%20error%20%7C%20level%20offset%20%7C%20level%20shape%20%7C%20final%20loss%20%7C%0A%20%20%20%20%7C---%7C---%7C---%7C---%7C---%7C%0A%20%20%20%20%7C%20untrained%20(flat%201%20s)%20%7C%2052.4%20%25%20%7C%20-1.3%20dB%20%7C%201.71%20dB%20%7C%200.0932%20%7C%0A%20%20%20%20%7C%20trained%2C%20**first-order%20shelf**%20(2%20numbers)%20%7C%209.8%20%25%20%7C%20%2B0.4%20dB%20%7C%200.79%20dB%20%7C%200.00393%20%7C%0A%20%20%20%20%7C%20trained%2C%20**ten-band%20graphic%20EQ**%20(10%20numbers)%20%7C%2010.6%20%25%20%7C%20%2B0.2%20dB%20%7C%200.71%20dB%20%7C%200.00256%20%7C%0A%0A%20%20%20%20Read%20the%20two%20trained%20rows%20against%20each%20other%20and%20the%20headline%20is%20not%20the%20one%0A%20%20%20%20you%20would%20expect.%20Five%20times%20the%20parameters%20and%20eleven%20times%20the%20biquads%0A%20%20%20%20reach%20a%20**lower%20loss**%20%E2%80%94%2035%20%25%20lower%20%E2%80%94%20and%20a%20better%20level%20shape%2C%20and%20a%0A%20%20%20%20*higher*%20RT%20error.%20The%20optimizer%20did%20its%20job%3B%20the%20extra%20freedom%20went%0A%20%20%20%20somewhere%20the%20exam%20paper%20does%20not%20award%20marks%20for.%0A%0A%20%20%20%20Per%20band%20it%20is%20obvious%20where%3A%0A%0A%20%20%20%20%7C%20%7C%2063%20%7C%20125%20%7C%20250%20%7C%20500%20%7C%201k%20%7C%202k%20%7C%204k%20%7C%208k%20%7C%0A%20%20%20%20%7C---%7C---%7C---%7C---%7C---%7C---%7C---%7C---%7C---%7C%0A%20%20%20%20%7C%20RT%20error%2C%20shelf%20%7C%2010%20%25%20%7C%207%20%25%20%7C%2013%20%25%20%7C%205%20%25%20%7C%200%20%25%20%7C%205%20%25%20%7C%2014%20%25%20%7C%20**25%20%25**%20%7C%0A%20%20%20%20%7C%20RT%20error%2C%20graphic%20EQ%20%7C%20**30%20%25**%20%7C%208%20%25%20%7C%2013%20%25%20%7C%204%20%25%20%7C%207%20%25%20%7C%205%20%25%20%7C%20**9%20%25**%20%7C%20**9%20%25**%20%7C%0A%0A%20%20%20%20The%20shelf%20carries%20its%20error%20at%20the%20two%20ends%2C%20which%20is%20what%20a%20monotone%20tilt%0A%20%20%20%20pinned%20at%20two%20endpoints%20has%20to%20do%3A%20the%20room%20holds%20a%202.8%20s%20plateau%20across%2063%0A%20%20%20%20and%20125%20Hz%20and%20has%20already%20dropped%20to%202.5%20s%20by%20250%20Hz%2C%20and%20a%20shelf%20cannot%0A%20%20%20%20hold%20a%20plateau%20and%20then%20step%20down.%20The%20graphic%20EQ%20spends%20its%20extra%20freedom%0A%20%20%20%20exactly%20where%20you%20would%20expect%20%E2%80%94%20the%20top%20two%20octaves%20go%20from%2014%20%25%20and%2025%20%25%20to%0A%20%20%20%209%20%25%20and%209%20%25%20%E2%80%94%20and%20then%20throws%20it%20all%20away%20in%20the%20bottom%20octave%2C%20which%20goes%0A%20%20%20%20from%2010%20%25%20to%2030%20%25.%0A%0A%20%20%20%20That%20is%20not%20a%20defect%20of%20the%20design.%20It%20is%20the%20%60frequency%60%20table%20restated%3A%0A%20%20%20%20cumulating%20the%20energy%20leaves%20the%20bottom%20octave%20with%20the%20least%20gradient%20of%20any%0A%20%20%20%20band%2C%20and%20%60%22both%22%60%20improves%20that%20rather%20than%20curing%20it.%20The%20shelf%20gets%2063%20Hz%0A%20%20%20%20nearly%20right%20by%20*not%20being%20free*%20%E2%80%94%20its%20low%20plateau%20is%20pinned%20by%20the%20whole%0A%20%20%20%20midrange%2C%20which%20happens%20to%20sit%20near%20the%20room's%20bottom-octave%20value%20%E2%80%94%20while%0A%20%20%20%20the%20graphic%20EQ%2C%20free%20to%20move%20that%20band%20on%20its%20own%2C%20is%20moved%20by%20a%20loss%20that%0A%20%20%20%20barely%20sees%20it.%20**Extra%20parameters%20are%20only%20worth%20what%20the%20objective%20can%0A%20%20%20%20supervise**%2C%20and%20a%20mean%20over%20eight%20octave%20bands%20is%20not%20the%20thing%20the%0A%20%20%20%20optimizer%20was%20minimizing.%0A%20%20%20%20%22%22%22)%2C%0A%20%20%20%20%20%20%20%20%7D%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%20Energy%20decay%20and%20spectrograms%0A%0A%20%20%20%20The%20band%20metrics%20above%20are%20a%20summary%3B%20the%20surfaces%20the%20loss%20actually%20compares%0A%20%20%20%20are%20below.%20The%20energy%20decay%20curves%20are%20the%20%24f%20%3D%200%24%20edge%20of%20that%20surface%2C%20and%0A%20%20%20%20the%20spectrograms%20are%20what%20it%20integrates.%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).show()%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%20All%20three%20peak-normalized%2C%20so%20the%20A%2FB%20compares%20timbre%20rather%20than%20level.%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%23%20What%20is%20left%20for%20a%20designed%20EQ%20to%20fix%0A%0A%20%20%20%20The%20analytic%20pipeline%20ends%20by%20designing%20an%20output%20GEQ%20from%20the%20residual%0A%20%20%20%20between%20the%20target's%20band%20levels%20and%20the%20FDN's.%20That%20filter%20is%20now%20a%20trained%0A%20%20%20%20parameter%2C%20so%20the%20same%20residual%20is%20a%20test%20of%20it%3A%20whatever%20a%0A%20%20%20%20%60gain_to_bounded_geq%60%20call%20would%20still%20be%20asked%20to%20correct%20is%20what%20the%20fit%0A%20%20%20%20did%20not%20manage.%0A%0A%20%20%20%20It%20takes%20the%20band-level%20shape%20error%20from%201.71%20dB%20down%20to%20about%200.8%20dB.%20So%20the%0A%20%20%20%20answer%20is%20%22most%20of%20it%2C%20not%20all%20of%20it%22%20%E2%80%94%20a%20designed%20GEQ%20on%20the%20residual%20would%0A%20%20%20%20still%20buy%20the%20remainder%2C%20and%20nothing%20stops%20you%20from%20running%20one%20afterwards.%0A%20%20%20%20What%20the%20fit%20does%20buy%20is%20that%20the%20EQ%20was%20chosen%20*while*%20the%20decay%20and%20the%0A%20%20%20%20matrix%20were%20still%20moving%2C%20rather%20than%20as%20a%20correction%20applied%20to%20something%0A%20%20%20%20already%20fixed.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(est_level%2C%20f_centre%2C%20level_trained%2C%20np%2C%20pyFDN%2C%20trained_eq_db)%3A%0A%20%20%20%20residual_db%20%3D%20pyFDN.lin_to_db(est_level)%20-%20pyFDN.lin_to_db(level_trained)%0A%20%20%20%20print(f%22bands%20(Hz)%3A%20%20%20%20%20%20%20%20%20%7Bf_centre.round(0)%7D%22)%0A%20%20%20%20print(f%22residual%20left%20(dB)%3A%20%7Bresidual_db.round(1)%7D%22)%0A%20%20%20%20print(%0A%20%20%20%20%20%20%20%20f%22%5Cntrained%20output%20EQ%3A%20%7Btrained_eq_db.round(1)%7D%20dB%22%0A%20%20%20%20%20%20%20%20f%22%5Cnresidual%3A%20%7Bresidual_db.mean()%3A%2B.1f%7D%20dB%20offset%2C%20%22%0A%20%20%20%20%20%20%20%20f%22%7Bnp.abs(residual_db%20-%20residual_db.mean()).mean()%3A.2f%7D%20dB%20of%20shape%22%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%20The%20deliverable%20%E2%80%94%20back%20to%20plain%20NumPy%0A%0A%20%20%20%20%60extract_build%60%20reads%20the%20trained%20model%20back%20out%20as%20an%20%60FDNBuild%60%20%E2%80%94%20no%20torch%0A%20%20%20%20%E2%80%94%20with%20the%20two%20trained%20filters%20baked%20into%20the%20%60post_delay%60%20and%20%60post_output%60%0A%20%20%20%20hooks%20as%20ordinary%20SOS%20banks.%20That%20is%20the%20thing%20%60build_to_impz%60%2C%0A%20%20%20%20%60build_to_flamo%60%20or%20%60process_fdn%60%20would%20take%2C%20and%20it%20is%20what%20makes%20the%0A%20%20%20%20assertions%20below%20a%20test%20of%20the%20FDN%20you%20would%20ship%20rather%20than%20of%20a%20torch%0A%20%20%20%20graph.%0A%0A%20%20%20%20Nothing%20about%20a%20build%20remembers%20which%20design%20produced%20it%2C%20which%20is%20the%20point%3A%0A%20%20%20%20%60n_sections%60%20biquads%20are%20%60n_sections%60%20biquads%20however%20they%20were%20chosen.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(model%2C%20np%2C%20pyFDN%2C%20trained_eq_sos%2C%20trained_sos)%3A%0A%20%20%20%20trained_build%20%3D%20pyFDN.extract_build(model)%0A%0A%20%20%20%20print(f%22post_delay%3A%20%20%7Btrained_build.post_delay.shape%7D%22)%0A%20%20%20%20print(f%22post_output%3A%20%7Btrained_build.post_output.shape%7D%22)%0A%20%20%20%20np.testing.assert_allclose(trained_build.post_delay%2C%20trained_sos%2C%20atol%3D1e-12)%0A%20%20%20%20np.testing.assert_allclose(trained_build.post_output%2C%20trained_eq_sos%2C%20atol%3D1e-12)%0A%0A%20%20%20%20%23%20Try%20this%3A%20run%20dry%20audio%20through%20the%20reverb%20you%20just%20trained.%0A%20%20%20%20%23%20%20%20from%20pyFDN%20import%20td%0A%20%20%20%20%23%20%20%20b%20%3D%20trained_build%0A%20%20%20%20%23%20%20%20dry%2C%20_%20%3D%20pyFDN.load_audio(%22synth_dry%22%2C%20fs%3Db.fs)%0A%20%20%20%20%23%20%20%20wet%20%3D%20pyFDN.process_fdn(%0A%20%20%20%20%23%20%20%20%20%20%20%20np.pad(dry%2C%20(0%2C%203%20*%20int(b.fs)))%2C%20b.delays%2C%20b.A%2C%20b.B%2C%20b.C%2C%20b.D%2C%0A%20%20%20%20%23%20%20%20%20%20%20%20post_delay%3Dtd.SOSBank(b.post_delay)%2C%0A%20%20%20%20%23%20%20%20%20%20%20%20post_output%3Dtd.SOSBank(b.post_output)%2C%0A%20%20%20%20%23%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%20Test%20%E2%80%94%20the%20fit%20stays%20stable%2C%20and%20finds%20most%20of%20the%20room's%20decay%20from%201%20s%0A%0A%20%20%20%20Six%20assertions%2C%20and%20none%20of%20them%20names%20a%20design.%20The%20first%20three%20are%20what%20the%0A%20%20%20%20RT%20parametrization%20exists%20for%3A%20the%20extracted%20FDN%20still%20renders%2C%20the%20decay%20it%0A%20%20%20%20settled%20on%20is%20a%20contractive%20filter%20at%20**every**%20frequency%20rather%20than%20only%20at%0A%20%20%20%20the%20parameter%20points%2C%20and%20the%20filter%20has%20the%20number%20of%20sections%20its%20design%0A%20%20%20%20promised.%20The%20last%20three%20are%20the%20fit%20itself%20%E2%80%94%20a%20decay%20that%20started%20flat%20and%0A%20%20%20%20knew%20nothing%20about%20the%20room%20ends%20up%20substantially%20closer%20to%20it%2C%20in%20the%20mean%0A%20%20%20%20and%20in%20every%20band%20the%20loss%20can%20resolve.%0A%0A%20%20%20%20Note%20that%20the%20stability%20check%20is%20on%20the%20*curve*%2C%20not%20on%20the%20parameters.%20On%0A%20%20%20%20the%20graphic%20EQ%20the%20Nyquist%20parameter%20is%20allowed%20to%20go%20negative%2C%20and%20does%3B%0A%20%20%20%20what%20has%20to%20stay%20positive%20is%20the%20RT%20the%20designed%20filter%20actually%20realizes.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(est_rt%2C%20n_sections%2C%20np%2C%20rt_curve%2C%20rt_init%2C%20rt_trained%2C%20trained_sos)%3A%0A%20%20%20%20assert%20np.all(np.isfinite(rt_trained))%2C%20%22trained%20FDN%20did%20not%20render%22%0A%20%20%20%20assert%20np.all(np.isfinite(rt_curve))%20and%20np.all(rt_curve%20%3E%200)%2C%20(%0A%20%20%20%20%20%20%20%20%22the%20decay%20filter%20is%20not%20contractive%20at%20every%20frequency%22%0A%20%20%20%20)%0A%20%20%20%20assert%20trained_sos.shape%5B0%5D%20%3D%3D%20n_sections%2C%20%22the%20design%20changed%20its%20mind%22%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%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%20Checkpoint%0A%0A%20%20%20%20Get%20the%20mean%20RT%20error%20below%209%20%25.%20Then%20say%20which%20of%20the%20three%20things%20you%0A%20%20%20%20changed%20did%20it%20%E2%80%94%20the%20start%2C%20the%20objective%2C%20or%20the%20parametrization%20%E2%80%94%20and%20which%0A%20%20%20%20band%20paid%20for%20it.%0A%0A%20%20%20%20Then%20break%20it%20on%20purpose%2C%20and%20read%20the%20failure%20off%20the%20loss%20curve%20rather%20than%0A%20%20%20%20off%20the%20audio%3A%20%60max_steps%3D20%60%2C%20%60lr%3D1.0%60%2C%20%60frequency%3D%22descending%22%60%2C%0A%20%20%20%20%60loss%20%3D%20pyFDN.MatchMelSpectrogram(rir)%60.%20Each%20fails%20differently%2C%20and%20each%0A%20%20%20%20failure%20is%20one%20of%20the%20four%20sections%20above.%0A%0A%20%20%20%20Where%20to%20go%20from%20here%3A%0A%0A%20%20%20%20-%20%60example_process_fdn%60%20%E2%80%94%20the%20same%20room%2C%20matched%20by%20hand%20with%20no%20gradients%0A%20%20%20%20-%20%60example_rir_to_fdn%60%20%E2%80%94%20the%20same%20room%2C%20designed%20analytically%20from%20its%0A%20%20%20%20%20%20measured%20decay%20and%20level.%20Both%20notebooks%20and%20this%20one%20end%20with%20the%20same%0A%20%20%20%20%20%20three%20plots%2C%20on%20purpose%0A%20%20%20%20-%20%60example_train_colorless_FDN%60%20%E2%80%94%20a%20different%20objective%20on%20the%20same%0A%20%20%20%20%20%20machinery%3A%20flatness%20instead%20of%20a%20target%20response%0A%20%20%20%20-%20%60example_multislope_rir_to_fdn%60%20%E2%80%94%20a%20coupled%20room%2C%20whose%20decay%20is%20not%20one%0A%20%20%20%20%20%20exponential%20and%20cannot%20be%20fitted%20as%20one%0A%20%20%20%20-%20%60example_fdn_to_faust%60%20%E2%80%94%20take%20the%20build%20you%20just%20trained%20to%20real%20time%0A%20%20%20%20%22%22%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
115c59dcce430b0722c063a3287ece9c