1use std::{cell::RefCell, f64::consts::PI};
6
7use faer::{Accum, Mat, Par, linalg::matmul::matmul};
8use secs_compute::Context;
9use secs_dsp::{
10 DspError, fft, optimize,
11 optimize::{JacobianOperator, NormalEquations, NormalOptions, OperatorOptions, XScale},
12 pairwise_sum, response,
13};
14
15use super::{MimoDesign, MimoRequest, design::third_octave_bands, support_path};
16use crate::{
17 C, Design, EngineConfig, EngineError, MeasurementSet, Note, Tuning, decay,
18 fit::{FitRequest, PeqDerivatives, fit_peq_with},
19 grid::design_freqs,
20};
21
22const FS: f64 = 600.0;
23const SEED_RESIDUALS: usize = 320;
25const N: usize = 720;
26const MU: f64 = 10.0;
27
28#[derive(Debug, Clone, PartialEq)]
29pub struct IirSupport {
30 pub gain_db: f64,
31 pub delay_ms: f64,
32 pub polarity: i8,
33 pub peq: Vec<(f64, f64, f64)>,
34 pub allpass: Vec<(f64, f64)>,
35}
36
37impl IirSupport {
38 pub fn response(&self, f: &[f64]) -> Vec<C> {
39 self.response_z(f, &frequency_z(f))
40 }
41
42 fn response_z(&self, f: &[f64], z: &[C]) -> Vec<C> {
43 let gain = 10f64.powf(self.gain_db / 20.0) * f64::from(self.polarity);
44 let mut out: Vec<_> = f
45 .iter()
46 .map(|&f| C::from_polar(1.0, -2.0 * PI * f * self.delay_ms / 1000.0) * gain)
47 .collect();
48 for &(f0, q, g) in &self.peq {
49 for (h, p) in out
50 .iter_mut()
51 .zip(response::peaking_response_z(z, f0, q, g, 48000.0))
52 {
53 *h *= p;
54 }
55 }
56 for &(f0, q) in &self.allpass {
57 for (h, p) in out.iter_mut().zip(response::allpass2(f, f0, q)) {
58 *h *= p;
59 }
60 }
61 out
62 }
63}
64
65fn frequency_z(f: &[f64]) -> Vec<C> {
66 f.iter()
67 .map(|&f| C::from_polar(1.0, (-2.0 * PI * f) * (1.0 / 48000.0)))
68 .collect()
69}
70
71#[derive(Debug, Clone, PartialEq, Default)]
72pub struct MimoIir {
73 pub supports: Vec<(String, Vec<(String, IirSupport)>)>,
74 pub removed: Vec<(String, Vec<String>)>,
75 pub notes: Vec<Note>,
76}
77
78#[derive(Debug, Clone, Copy, Default, PartialEq)]
81pub enum IirSolver {
82 #[default]
83 Dense,
84 MatrixFree {
85 max_cg_iterations: usize,
86 cg_rtol: f64,
87 },
88}
89
90#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
94pub enum IirDerivatives {
95 #[default]
97 CentralDifference,
98 Analytic,
100}
101
102#[derive(Debug, Clone, Copy)]
103pub struct IirSolveOptions {
104 pub solver: IirSolver,
105 pub dense_fallback: bool,
108 pub derivatives: IirDerivatives,
109 pub seed_peq_derivatives: PeqDerivatives,
110}
111
112impl Default for IirSolveOptions {
113 fn default() -> Self {
114 Self {
115 solver: IirSolver::Dense,
116 dense_fallback: true,
117 derivatives: IirDerivatives::CentralDifference,
118 seed_peq_derivatives: PeqDerivatives::TwoPoint,
119 }
120 }
121}
122
123impl IirSolveOptions {
124 fn validate(&self) -> Result<(), EngineError> {
125 if let IirSolver::MatrixFree {
126 max_cg_iterations,
127 cg_rtol,
128 } = self.solver
129 && (max_cg_iterations == 0 || !cg_rtol.is_finite() || cg_rtol <= 0.0 || cg_rtol >= 1.0)
130 {
131 return Err(EngineError::Input("invalid matrix-free IIR options".into()));
132 }
133 Ok(())
134 }
135}
136
137fn check(ctx: &Context) -> Result<(), EngineError> {
138 ctx.cancel().check().map_err(|_| EngineError::Cancelled)
139}
140
141fn per(t: &Tuning) -> usize {
142 2 + 3 * t.mimo_k_bands + 2 * t.mimo_a_bands
143}
144
145pub(crate) fn band_of(lo: f64, hi: f64, fsiso: f64, t: &Tuning) -> (f64, f64) {
146 let low = 15f64.max(lo * 0.9);
147 let high = 300f64.min(hi.min(fsiso) * 1.1);
148 if t.mimo_in_band && high > low * 1.5 {
149 (low, high)
150 } else {
151 (15.0, 300.0)
152 }
153}
154
155pub(crate) fn bounds(band: (f64, f64), t: &Tuning) -> (Vec<f64>, Vec<f64>) {
156 let mut lb = vec![-80.0, 0.0];
157 let mut ub = vec![t.mimo_gain_max_db, 250.0];
158 for _ in 0..t.mimo_k_bands {
159 lb.extend([band.0.ln(), 0.5f64.ln(), -t.mimo_band_db]);
160 ub.extend([band.1.ln(), t.mimo_q_max.ln(), t.mimo_band_db]);
161 }
162 for _ in 0..t.mimo_a_bands {
163 lb.extend([band.0.ln(), 0.3f64.ln()]);
164 ub.extend([band.1.ln(), 20f64.ln()]);
165 }
166 (lb, ub)
167}
168
169pub(crate) fn unpack(x: &[f64], pols: &[i8], t: &Tuning) -> Vec<IirSupport> {
170 x.chunks_exact(per(t))
171 .zip(pols)
172 .map(|(v, &polarity)| IirSupport {
173 gain_db: v[0],
174 delay_ms: v[1],
175 polarity,
176 peq: (0..t.mimo_k_bands)
177 .map(|k| (v[2 + 3 * k].exp(), v[3 + 3 * k].exp(), v[4 + 3 * k]))
178 .collect(),
179 allpass: (0..t.mimo_a_bands)
180 .map(|a| {
181 let b = 2 + 3 * t.mimo_k_bands + 2 * a;
182 (v[b].exp(), v[b + 1].exp())
183 })
184 .collect(),
185 })
186 .collect()
187}
188
189pub(crate) fn late_modes(xs: &[Vec<f64>], count: usize, lo: f64, hi: f64) -> Vec<f64> {
190 let mut spec = vec![0.0; 2049];
191 for x in xs {
192 let mut late = vec![0.0; 4096];
193 let begin = (decay::onset(x) + 90).min(x.len().min(4096));
194 let end = x.len().min(4096);
195 late[begin..end].copy_from_slice(&x[begin..end]);
196 for (s, h) in spec.iter_mut().zip(fft::rfft(&late)) {
197 let a = response::magnitude(h);
198 *s += a * a;
199 }
200 }
201 let f = secs_dsp::grid::freqs(4096, FS).expect("MIMO grid");
202 let masked: Vec<_> = spec
203 .iter()
204 .zip(&f)
205 .map(|(&s, &f)| if (lo..=hi).contains(&f) { s } else { 0.0 })
206 .collect();
207 let mut indices = secs_dsp::peaks::find_peaks(&masked, None);
208 indices.sort_by(|&a, &b| spec[b].total_cmp(&spec[a]));
209 indices.into_iter().take(count).map(|i| f[i]).collect()
210}
211
212pub(crate) struct SeedResult {
213 pub x: Vec<f64>,
214 pub polarity: i8,
215 pub cost: f64,
216 pub nfev: usize,
217 pub status: i32,
218}
219
220#[allow(clippy::too_many_arguments)]
221pub(crate) fn fit_one(
222 s: &[f64],
223 lo: f64,
224 hi: f64,
225 modes: &[f64],
226 band: (f64, f64),
227 t: &Tuning,
228 derivatives: IirDerivatives,
229 seed_peq_derivatives: PeqDerivatives,
230 ctx: &Context,
231) -> Result<SeedResult, EngineError> {
232 check(ctx)?;
233 let lo = lo.max(15.0);
234 let step = (hi.log10() - lo.log10()) / 159.0;
235 let mut f: Vec<_> = (0..160)
236 .map(|k| 10f64.powf(lo.log10() + k as f64 * step))
237 .collect();
238 f[0] = lo;
239 f[159] = hi;
240 let target: Vec<C> = f
241 .iter()
242 .map(|&f| {
243 s.iter()
244 .enumerate()
245 .map(|(k, &s)| C::from_polar(1.0, -2.0 * PI * f * k as f64 / FS) * s)
246 .sum()
247 })
248 .collect();
249 let k = decay::first_max(&s.iter().map(|v| v.abs()).collect::<Vec<_>>());
250 let polarity = if s[k] >= 0.0 { 1 } else { -1 };
251 let db: Vec<_> = target
252 .iter()
253 .map(|&h| 20.0 * (response::magnitude(h) + 1e-12).log10())
254 .collect();
255 let base = decay::median(&db);
256 let corr: Vec<_> = db.iter().map(|v| v - base).collect();
257 let mut peq = fit_peq_with(
258 &FitRequest {
259 g: &f,
260 corr_db: &corr,
261 n: t.mimo_k_bands,
262 lo,
263 hi,
264 boost: t.mimo_band_db,
265 ring_ms: None,
266 f_max: None,
267 },
268 &|| ctx.cancel().is_cancelled(),
269 seed_peq_derivatives,
270 );
271 check(ctx)?;
272 let middle = (lo * hi).sqrt();
273 peq.resize(t.mimo_k_bands, (middle, 1.0, 0.0));
274 let mut x0 = vec![base, 1000.0 * k as f64 / FS];
275 for (f, q, g) in peq {
276 x0.extend([f.ln(), q.ln(), g]);
277 }
278 for a in 0..t.mimo_a_bands {
279 x0.extend([
280 modes
281 .get(a)
282 .copied()
283 .unwrap_or(middle)
284 .clamp(band.0 * 1.03, band.1 * 0.97)
285 .ln(),
286 2f64.ln(),
287 ]);
288 }
289 let (lb, ub) = bounds(band, t);
290 for (k, v) in x0.iter_mut().enumerate() {
291 *v = v.clamp(lb[k] + 1e-6, ub[k] - 1e-6);
292 }
293 let norm = pairwise_sum(
294 &target
295 .iter()
296 .map(|&h| response::magnitude(h))
297 .collect::<Vec<_>>(),
298 ) / 160.0
299 + 1e-12;
300 let z = frequency_z(&f);
301 let cache = RefCell::new(Parts::default());
302 let residual = |x: &[f64]| {
303 if ctx.cancel().is_cancelled() {
304 return vec![0.0; SEED_RESIDUALS];
305 }
306 let mut cache = cache.borrow_mut();
307 cache.response(x, polarity, &f, &z, t);
308 let r: Vec<_> = cache
309 .s
310 .iter()
311 .zip(&target)
312 .map(|(&h, &v)| (h - v) / norm)
313 .collect();
314 r.iter()
315 .map(|v| v.re)
316 .chain(r.iter().map(|v| v.im))
317 .collect::<Vec<_>>()
318 };
319 let result = match derivatives {
320 IirDerivatives::CentralDifference => optimize::least_squares(residual, &x0, &lb, &ub, 300),
321 IirDerivatives::Analytic => {
322 let jacobian = |x: &[f64]| {
323 if ctx.cancel().is_cancelled() {
324 return Mat::zeros(SEED_RESIDUALS, x.len());
325 }
326 let mut cache = cache.borrow_mut();
327 cache.derivatives(x, polarity, &f, &z, t, IirDerivatives::Analytic);
328 let half = SEED_RESIDUALS / 2;
329 Mat::from_fn(SEED_RESIDUALS, x.len(), |i, j| {
330 if i < half {
331 cache.d[j][i].re / norm
332 } else {
333 cache.d[j][i - half].im / norm
334 }
335 })
336 };
337 optimize::least_squares_analytic(
338 residual,
339 jacobian,
340 &x0,
341 &lb,
342 &ub,
343 optimize::Options {
344 max_nfev: 300,
345 ..optimize::Options::default()
346 },
347 None,
348 )
349 }
350 };
351 check(ctx)?;
352 Ok(match result {
353 Ok(sol) => SeedResult {
354 x: sol.x,
355 polarity,
356 cost: sol.cost,
357 nfev: sol.nfev,
358 status: sol.status,
359 },
360 Err(_) => SeedResult {
361 cost: decay::energy(&residual(&x0)) * 0.5,
362 x: x0,
363 polarity,
364 nfev: 0,
365 status: -1,
366 },
367 })
368}
369
370#[derive(Clone)]
371pub(crate) struct Problem {
372 pub roles: Vec<String>,
373 pub f: Vec<f64>,
374 pub lp: Vec<C>,
375 pub n: usize,
376 pub nf: usize,
377 pub xs: Vec<Vec<f64>>,
378 pub wl: Vec<Vec<f64>>,
379 pub we: Vec<Vec<f64>>,
380 pub g: Vec<Vec<Vec<C>>>,
381 pub fir: Vec<Vec<f64>>,
382 pub ranges: Vec<(f64, f64)>,
383 pub modes: Vec<f64>,
384 pub fsiso: f64,
385 pub direct: f64,
386 pub level_db: f64,
387}
388
389#[derive(Default)]
390struct Parts {
391 key: Vec<u64>,
392 s: Vec<C>,
393 d: Vec<Vec<C>>,
394}
395
396impl Parts {
397 fn response(&mut self, v: &[f64], pol: i8, f: &[f64], z: &[C], t: &Tuning) {
398 if self.key.len() == v.len() && self.key.iter().zip(v).all(|(&a, &b)| a == b.to_bits()) {
399 return;
400 }
401 self.s = unpack(v, &[pol], t).remove(0).response_z(f, z);
402 self.key.clear();
403 self.key.extend(v.iter().map(|v| v.to_bits()));
404 self.d.clear();
405 }
406
407 fn derivatives(
408 &mut self,
409 v: &[f64],
410 pol: i8,
411 f: &[f64],
412 z: &[C],
413 t: &Tuning,
414 derivatives: IirDerivatives,
415 ) {
416 self.response(v, pol, f, z, t);
417 if !self.d.is_empty() {
418 return;
419 }
420 if derivatives == IirDerivatives::CentralDifference {
421 self.d = finite_difference_derivatives(v, &self.s, f, t);
422 return;
423 }
424 self.d = vec![vec![C::new(0.0, 0.0); f.len()]; per(t)];
425 for (k, &f) in f.iter().enumerate() {
426 self.d[0][k] = self.s[k] * 10f64.ln() / 20.0;
427 self.d[1][k] = self.s[k] * C::new(0.0, -2.0 * PI * f / 1000.0);
428 }
429 for k in 0..t.mimo_k_bands {
430 let base = 2 + 3 * k;
431 let derivatives = response::peaking_log_derivatives_z(
432 z,
433 v[base].exp(),
434 v[base + 1].exp(),
435 v[base + 2],
436 48000.0,
437 );
438 for (c, derivative) in derivatives.into_iter().enumerate() {
439 for (i, value) in derivative.into_iter().enumerate() {
440 self.d[base + c][i] = self.s[i] * value;
441 }
442 }
443 }
444 for a in 0..t.mimo_a_bands {
445 let base = 2 + 3 * t.mimo_k_bands + 2 * a;
446 let derivatives =
447 response::allpass_log_derivatives(f, v[base].exp(), v[base + 1].exp());
448 for (c, derivative) in derivatives.into_iter().enumerate() {
449 for (i, value) in derivative.into_iter().enumerate() {
450 self.d[base + c][i] = self.s[i] * value;
451 }
452 }
453 }
454 }
455}
456
457fn finite_difference_derivatives(v: &[f64], s: &[C], f: &[f64], t: &Tuning) -> Vec<Vec<C>> {
461 let mut d = vec![vec![C::new(0.0, 0.0); f.len()]; per(t)];
462 for (k, &f) in f.iter().enumerate() {
463 d[0][k] = s[k] * 10f64.ln() / 20.0;
464 d[1][k] = s[k] * C::new(0.0, -2.0 * PI * f / 1000.0);
465 }
466 let h = 1e-4;
467 for k in 0..t.mimo_k_bands {
468 let base = 2 + 3 * k;
469 let factor =
470 response::peaking_response(f, v[base].exp(), v[base + 1].exp(), v[base + 2], 48000.0);
471 for c in 0..3 {
472 let mut up = [v[base], v[base + 1], v[base + 2]];
473 let mut dn = up;
474 up[c] += h;
475 dn[c] -= h;
476 let u = response::peaking_response(f, up[0].exp(), up[1].exp(), up[2], 48000.0);
477 let l = response::peaking_response(f, dn[0].exp(), dn[1].exp(), dn[2], 48000.0);
478 for i in 0..f.len() {
479 d[base + c][i] = secs_dsp::complex_div(s[i] * (u[i] - l[i]) / (2.0 * h), factor[i]);
480 }
481 }
482 }
483 for a in 0..t.mimo_a_bands {
484 let base = 2 + 3 * t.mimo_k_bands + 2 * a;
485 let factor = response::allpass2(f, v[base].exp(), v[base + 1].exp());
486 for c in 0..2 {
487 let mut up = [v[base], v[base + 1]];
488 let mut dn = up;
489 up[c] += h;
490 dn[c] -= h;
491 let u = response::allpass2(f, up[0].exp(), up[1].exp());
492 let l = response::allpass2(f, dn[0].exp(), dn[1].exp());
493 for i in 0..f.len() {
494 d[base + c][i] = secs_dsp::complex_div(s[i] * (u[i] - l[i]) / (2.0 * h), factor[i]);
495 }
496 }
497 }
498 d
499}
500
501fn time_product(a: &[C], b: &[C], nf: usize, n: usize) -> Vec<f64> {
502 let mut out = fft::irfft(&a.iter().zip(b).map(|(a, b)| a * b).collect::<Vec<_>>(), nf);
503 out.truncate(n);
504 out
505}
506
507pub(crate) struct Objective<'a> {
508 pb: &'a Problem,
509 t: &'a Tuning,
510 ctx: &'a Context,
511 pols: &'a [i8],
512 weff: Vec<Vec<f64>>,
513 scale: f64,
514 z: Vec<C>,
515 cache: Vec<Parts>,
516 derivatives: IirDerivatives,
517 operator: Option<OperatorState>,
518}
519
520struct OperatorState {
521 derivatives: Vec<Vec<Vec<C>>>,
524 level: Vec<f64>,
525}
526
527impl<'a> Objective<'a> {
528 pub(crate) fn new(pb: &'a Problem, t: &'a Tuning, ctx: &'a Context, pols: &'a [i8]) -> Self {
529 let band: Vec<_> =
530 pb.f.iter()
531 .map(|&f| f >= 15.0 && f <= pb.fsiso * 1.2)
532 .collect();
533 let count = band.iter().filter(|&&b| b).count().max(1);
534 let energy = decay::energy(&pb.xs[0]);
535 let weff = (0..pb.roles.len())
536 .map(|r| {
537 let mut gbar = vec![0.0; pb.f.len()];
538 for g in &pb.g {
539 for (sum, &h) in gbar.iter_mut().zip(&g[r]) {
540 *sum += response::magnitude(h);
541 }
542 }
543 for v in &mut gbar {
544 *v /= pb.xs.len() as f64;
545 }
546 let reference = decay::median(
547 &gbar
548 .iter()
549 .zip(&band)
550 .filter(|(_, b)| **b)
551 .map(|(&v, _)| v)
552 .collect::<Vec<_>>(),
553 );
554 let factor = t.mimo_effort * (energy / count as f64).sqrt();
555 gbar.iter()
556 .zip(&band)
557 .map(|(&g, &b)| {
558 factor
559 * if b {
560 reference / (g + 0.05 * reference)
561 } else {
562 0.0
563 }
564 })
565 .collect()
566 })
567 .collect();
568 Self {
569 pb,
570 t,
571 ctx,
572 pols,
573 weff,
574 scale: energy.sqrt(),
575 z: frequency_z(&pb.f),
576 cache: (0..pb.roles.len()).map(|_| Parts::default()).collect(),
577 derivatives: IirDerivatives::CentralDifference,
578 operator: None,
579 }
580 }
581
582 pub(crate) fn set_derivatives(&mut self, derivatives: IirDerivatives) {
583 self.derivatives = derivatives;
584 for part in &mut self.cache {
585 part.d.clear();
586 }
587 self.operator = None;
588 }
589
590 fn evaluate(&mut self, x: &[f64]) {
591 for ((part, v), &pol) in self
592 .cache
593 .iter_mut()
594 .zip(x.chunks_exact(per(self.t)))
595 .zip(self.pols)
596 {
597 part.response(v, pol, &self.pb.f, &self.z, self.t);
598 }
599 }
600
601 fn derivatives(&mut self, x: &[f64]) {
602 for ((part, v), &pol) in self
603 .cache
604 .iter_mut()
605 .zip(x.chunks_exact(per(self.t)))
606 .zip(self.pols)
607 {
608 part.derivatives(v, pol, &self.pb.f, &self.z, self.t, self.derivatives);
609 }
610 }
611
612 fn level(&self) -> (Vec<Vec<f64>>, f64, f64) {
613 let st: Vec<_> = self
614 .cache
615 .iter()
616 .map(|p| time_product(&p.s, &self.pb.lp, self.pb.nf, self.pb.n))
617 .collect();
618 let lvl = st.iter().map(|s| decay::energy(s)).sum::<f64>() / self.pb.direct.max(1e-30);
619 let over = 10.0 * (lvl + 1e-30).log10() - self.pb.level_db;
620 (st, lvl, over)
621 }
622
623 fn rows(&self) -> usize {
624 2 * self.pb.xs.len() * self.pb.n
625 + 1
626 + 2 * self.pb.roles.len() * self.pb.f.len()
627 + if self.t.mimo_band_reg > 0.0 {
628 self.pb.roles.len() * self.t.mimo_k_bands
629 } else {
630 0
631 }
632 }
633}
634
635pub(crate) fn solve_joint(
636 pb: &Problem,
637 t: &Tuning,
638 ctx: &Context,
639 x0: &[f64],
640 pols: &[i8],
641) -> Result<optimize::LeastSquaresResult, EngineError> {
642 solve_joint_with_options(pb, t, ctx, x0, pols, &IirSolveOptions::default())
643}
644
645pub(crate) fn solve_joint_with_options(
646 pb: &Problem,
647 t: &Tuning,
648 ctx: &Context,
649 x0: &[f64],
650 pols: &[i8],
651 options: &IirSolveOptions,
652) -> Result<optimize::LeastSquaresResult, EngineError> {
653 check(ctx)?;
654 let mut lb = Vec::new();
655 let mut ub = Vec::new();
656 for &(lo, hi) in &pb.ranges {
657 let (l, u) = bounds(band_of(lo, hi, pb.fsiso, t), t);
658 lb.extend(l);
659 ub.extend(u);
660 }
661 let x0: Vec<_> = x0
662 .iter()
663 .enumerate()
664 .map(|(k, &v)| v.clamp(lb[k] + 1e-6, ub[k] - 1e-6))
665 .collect();
666 let mut objective = Objective::new(pb, t, ctx, pols);
667 objective.derivatives = options.derivatives;
668 if let IirSolver::MatrixFree {
669 max_cg_iterations,
670 cg_rtol,
671 } = options.solver
672 {
673 let result = optimize::least_squares_operator(
674 &mut objective,
675 &x0,
676 &lb,
677 &ub,
678 &OperatorOptions {
679 max_nfev: t.mimo_max_nfev,
680 max_cg_iterations,
681 cg_rtol,
682 ..OperatorOptions::default()
683 },
684 );
685 check(ctx)?;
686 match result {
687 Ok(solution) => return Ok(solution),
688 Err(error) if !options.dense_fallback => return Err(error.into()),
689 Err(_) => objective.operator = None,
690 }
691 }
692 for x_scale in [XScale::Jac, XScale::Unit] {
693 let result = optimize::least_squares_normal(
694 &mut objective,
695 &x0,
696 &lb,
697 &ub,
698 &NormalOptions {
699 max_nfev: t.mimo_max_nfev,
700 x_scale,
701 ..NormalOptions::default()
702 },
703 );
704 check(ctx)?;
705 if let Ok(sol) = result {
706 return Ok(sol);
707 }
708 }
709 let cost = 0.5 * decay::energy(&NormalEquations::residual(&mut objective, &x0));
710 Ok(optimize::LeastSquaresResult {
711 x: x0,
712 cost,
713 nfev: 0,
714 njev: 0,
715 status: -1,
716 optimality: f64::NAN,
717 })
718}
719
720pub(crate) fn solve_problem(
721 pb: &Problem,
722 t: &Tuning,
723 ctx: &Context,
724) -> Result<(Vec<f64>, Vec<i8>), EngineError> {
725 solve_problem_with_options(pb, t, ctx, &IirSolveOptions::default())
726}
727
728fn solve_problem_with_options(
729 pb: &Problem,
730 t: &Tuning,
731 ctx: &Context,
732 options: &IirSolveOptions,
733) -> Result<(Vec<f64>, Vec<i8>), EngineError> {
734 let mut starts = Vec::new();
735 let mut pols = Vec::new();
736 for (r, &(lo, hi)) in pb.ranges.iter().enumerate() {
737 let modes: Vec<_> = pb
738 .modes
739 .iter()
740 .copied()
741 .filter(|m| (lo..=hi).contains(m))
742 .collect();
743 let modes = if modes.is_empty() { &pb.modes } else { &modes };
744 let seed = fit_one(
745 &pb.fir[r],
746 lo,
747 hi.min(pb.fsiso),
748 modes,
749 band_of(lo, hi, pb.fsiso, t),
750 t,
751 options.derivatives,
752 options.seed_peq_derivatives,
753 ctx,
754 )?;
755 starts.extend(seed.x);
756 pols.push(seed.polarity);
757 }
758 Ok((
759 solve_joint_with_options(pb, t, ctx, &starts, &pols, options)?.x,
760 pols,
761 ))
762}
763
764pub(crate) fn applied(pb: &Problem, supports: &[(usize, IirSupport)]) -> Vec<Vec<f64>> {
765 let ss: Vec<_> = supports.iter().map(|(_, s)| s.response(&pb.f)).collect();
766 pb.xs
767 .iter()
768 .enumerate()
769 .map(|(p, x)| {
770 let mut sum = vec![C::new(0.0, 0.0); pb.f.len()];
771 for ((r, _), s) in supports.iter().zip(&ss) {
772 for (k, v) in sum.iter_mut().enumerate() {
773 *v += pb.g[p][*r][k] * s[k];
774 }
775 }
776 let add = fft::irfft(&sum, pb.nf);
777 x.iter().zip(add).map(|(x, a)| x + a).collect()
778 })
779 .collect()
780}
781
782struct BandGuard {
783 bounds: Vec<(usize, usize)>,
784 before: Vec<f64>,
785}
786
787impl BandGuard {
788 fn new(pb: &Problem) -> Self {
789 let mut bands = third_octave_bands(pb.fsiso);
790 bands.extend(
791 late_modes(&pb.xs, 6, 20.0, 160.0)
792 .iter()
793 .map(|f| (f - 1.5, f + 1.5)),
794 );
795 let bounds = bands
796 .into_iter()
797 .map(|(lo, hi)| {
798 (
799 pb.f.partition_point(|&f| f < lo),
800 pb.f.partition_point(|&f| f < hi),
801 )
802 })
803 .collect::<Vec<_>>();
804 let before = Self::power(pb, &pb.xs, &bounds);
805 Self { bounds, before }
806 }
807
808 fn power(pb: &Problem, ys: &[Vec<f64>], bounds: &[(usize, usize)]) -> Vec<f64> {
809 let mut total = vec![0.0; bounds.len()];
810 for (p, y) in ys.iter().enumerate() {
811 let mut windowed: Vec<_> = y.iter().zip(&pb.wl[p]).map(|(y, w)| y * w).collect();
812 windowed.resize(pb.nf, 0.0);
813 let power: Vec<_> = fft::rfft(&windowed)
814 .iter()
815 .map(|&h| {
816 let a = response::magnitude(h);
817 a * a
818 })
819 .collect();
820 for (total, &(a, b)) in total.iter_mut().zip(bounds) {
821 *total += pairwise_sum(&power[a..b]);
822 }
823 }
824 total
825 }
826
827 fn evaluate(&self, pb: &Problem, ys: &[Vec<f64>]) -> f64 {
828 self.before
829 .iter()
830 .zip(Self::power(pb, ys, &self.bounds))
831 .filter(|(b, _)| **b > 0.0)
832 .map(|(b, a)| 10.0 * ((a + 1e-30) / b).log10())
833 .reduce(f64::max)
834 .unwrap_or(-99.0)
835 }
836}
837
838pub(crate) fn worst_band_db(pb: &Problem, supports: &[(usize, IirSupport)]) -> f64 {
839 BandGuard::new(pb).evaluate(pb, &applied(pb, supports))
840}
841
842struct OutcomeGuard<'a> {
843 pb: &'a Problem,
844 band: BandGuard,
845 tail: super::design::T60Guard<'a>,
846}
847
848impl<'a> OutcomeGuard<'a> {
849 fn new(pb: &'a Problem) -> Self {
850 Self {
851 pb,
852 band: BandGuard::new(pb),
853 tail: super::design::T60Guard::new(&pb.xs, 6),
854 }
855 }
856
857 fn evaluate(&mut self, supports: &[(usize, IirSupport)]) -> Result<(f64, f64), EngineError> {
858 let ys = applied(self.pb, supports);
861 Ok((
862 if supports.is_empty() {
863 -99.0
864 } else {
865 self.band.evaluate(self.pb, &ys)
866 },
867 self.tail.evaluate(&ys)?,
868 ))
869 }
870}
871
872pub(crate) fn outcome(
873 pb: &Problem,
874 supports: &[(usize, IirSupport)],
875) -> Result<(f64, f64), EngineError> {
876 OutcomeGuard::new(pb).evaluate(supports)
877}
878
879#[derive(Debug)]
880pub(crate) struct PostResult {
881 pub supports: Vec<(usize, IirSupport)>,
882 pub removed: Vec<String>,
883 pub level_cut: f64,
884 pub violations: Vec<f64>,
885 pub gain_steps: usize,
886}
887
888pub(crate) fn postprocess(
889 pb: &Problem,
890 x: &[f64],
891 pols: &[i8],
892 t: &Tuning,
893 ctx: &Context,
894) -> Result<PostResult, EngineError> {
895 let mut sups = unpack(x, pols, t);
896 let lvl = sups
897 .iter()
898 .map(|s| decay::energy(&time_product(&s.response(&pb.f), &pb.lp, pb.nf, pb.n)))
899 .sum::<f64>()
900 / pb.direct.max(1e-30);
901 let target = 10f64.powf(pb.level_db / 10.0);
902 let level_cut = if lvl > target {
903 10.0 * (lvl / target).log10()
904 } else {
905 0.0
906 };
907 for s in &mut sups {
908 s.gain_db -= level_cut;
909 }
910 let mut cur: Vec<_> = sups.into_iter().enumerate().collect();
911 let mut violations = Vec::new();
912 let mut guard = OutcomeGuard::new(pb);
913 let mut violation = |s: &[(usize, IirSupport)]| -> Result<f64, EngineError> {
914 check(ctx)?;
915 let (band, tail) = guard.evaluate(s)?;
916 let v = (band - t.mimo_worse_db).max(tail - 0.15 * t.mimo_worse_db);
917 violations.push(v);
918 Ok(v)
919 };
920 let mut v = violation(&cur)?;
921 let mut removed = Vec::new();
922 while v > 0.0 && removed.len() < (pb.roles.len() / 2).max(1) && cur.len() > 1 {
923 let mut best = (0, f64::INFINITY);
924 for index in 0..cur.len() {
925 let trial: Vec<_> = cur
926 .iter()
927 .enumerate()
928 .filter(|(k, _)| *k != index)
929 .map(|(_, s)| s.clone())
930 .collect();
931 let value = violation(&trial)?;
932 if value < best.1 {
933 best = (index, value);
934 }
935 }
936 if best.1 >= v {
937 break;
938 }
939 removed.push(pb.roles[cur.remove(best.0).0].clone());
940 v = best.1;
941 }
942 let mut gain_steps = 0;
943 for step in 0..5 {
944 if v <= 0.0 {
945 break;
946 }
947 if step == 4 {
948 cur.clear();
949 break;
950 }
951 for (_, s) in &mut cur {
952 s.gain_db -= 3.0;
953 }
954 gain_steps += 1;
955 v = violation(&cur)?;
956 }
957 Ok(PostResult {
958 supports: cur,
959 removed,
960 level_cut,
961 violations,
962 gain_steps,
963 })
964}
965
966#[allow(clippy::too_many_arguments)]
967pub(crate) fn build_problem(
968 design: &Design,
969 spectra: &crate::Spectra,
970 align_delay_ms: &[(String, f64)],
971 ideal: &MimoDesign,
972 input: &str,
973 supports: &[(String, Vec<f64>)],
974 req: &MimoRequest,
975 t: &Tuning,
976 ctx: &Context,
977) -> Result<Problem, EngineError> {
978 let f = secs_dsp::grid::freqs(2 * N, FS)?;
979 let lp = response::lr4(&f, req.fsiso, "LP");
980 let roles: Vec<_> = supports.iter().map(|(r, _)| r.clone()).collect();
981 let mut pb = Problem {
982 roles,
983 f,
984 lp,
985 n: N,
986 nf: 2 * N,
987 xs: Vec::new(),
988 wl: Vec::new(),
989 we: Vec::new(),
990 g: Vec::new(),
991 fir: supports.iter().map(|(_, s)| s.clone()).collect(),
992 ranges: Vec::new(),
993 modes: Vec::new(),
994 fsiso: req.fsiso,
995 direct: 0.0,
996 level_db: req.level_db,
997 };
998 for role in &pb.roles {
999 pb.ranges.push(
1000 ideal
1001 .ranges
1002 .iter()
1003 .find(|(r, _)| r == role)
1004 .and_then(|(_, v)| *v)
1005 .ok_or_else(|| EngineError::Input(format!("missing MIMO range {role}")))?,
1006 );
1007 }
1008 for &p in &design.spectra.positions {
1009 check(ctx)?;
1010 let h = crate::input_response(design, input, spectra, p)
1011 .ok_or_else(|| EngineError::Input(format!("missing MIMO input {p}/{input}")))?;
1012 let x = secs_dsp::lowrate::to_low(&h, design_freqs(), N)?;
1013 let onset = decay::onset(&x) + 36;
1014 pb.wl
1015 .push((0..N).map(|i| if i >= onset { 1.0 } else { 0.0 }).collect());
1016 pb.we
1017 .push(pb.wl.last().unwrap().iter().map(|w| 1.0 - w).collect());
1018 pb.xs.push(x);
1019 let mut paths = Vec::new();
1020 for role in &pb.roles {
1021 let h = support_path(ideal, role, spectra, p, align_delay_ms)?;
1022 let mut low = secs_dsp::lowrate::to_low(&h, design_freqs(), N)?;
1023 low.resize(2 * N, 0.0);
1024 paths.push(
1025 fft::rfft(&low)
1026 .iter()
1027 .zip(&pb.lp)
1028 .map(|(h, l)| h * l)
1029 .collect(),
1030 );
1031 }
1032 pb.g.push(paths);
1033 }
1034 let main = design
1035 .mains
1036 .iter()
1037 .find(|m| m.role == input)
1038 .ok_or_else(|| EngineError::Input(format!("missing MIMO main {input}")))?;
1039 pb.direct = decay::energy(&secs_dsp::lowrate::to_low(
1040 &main.response,
1041 design_freqs(),
1042 N,
1043 )?);
1044 pb.modes = late_modes(&pb.xs, t.mimo_a_bands, 20.0, req.fsiso);
1045 Ok(pb)
1046}
1047
1048fn validate_request(req: &MimoRequest, t: &Tuning) -> Result<(), EngineError> {
1049 if !(0.0..300.0).contains(&req.fsiso)
1050 || req.fsiso == 0.0
1051 || !req.level_db.is_finite()
1052 || !t.mimo_band_db.is_finite()
1053 || t.mimo_band_db <= 0.0
1054 || !t.mimo_q_max.is_finite()
1055 || t.mimo_q_max <= 0.5
1056 || !t.mimo_gain_max_db.is_finite()
1057 || t.mimo_gain_max_db <= -80.0
1058 || t.mimo_max_nfev == 0
1059 || [t.mimo_band_reg, t.mimo_effort, t.mimo_worse_db]
1060 .iter()
1061 .any(|v| !v.is_finite() || *v < 0.0)
1062 {
1063 return Err(EngineError::Input("invalid MIMO IIR tuning".into()));
1064 }
1065 let parameters = t
1069 .mimo_k_bands
1070 .checked_mul(3)
1071 .zip(t.mimo_a_bands.checked_mul(2))
1072 .and_then(|(k, a)| k.checked_add(a)?.checked_add(2));
1073 if parameters.is_none_or(|n| n > SEED_RESIDUALS) {
1074 return Err(EngineError::Input(format!(
1075 "invalid MIMO IIR tuning: {} peaking and {} all-pass bands exceed {SEED_RESIDUALS} parameters per support",
1076 t.mimo_k_bands, t.mimo_a_bands
1077 )));
1078 }
1079 let (lb, ub) = bounds((15.0, 300.0), t);
1085 if lb
1086 .iter()
1087 .zip(&ub)
1088 .any(|(&l, &u)| u - l <= 2e-6 || l + 1e-6 > u - 1e-6)
1089 {
1090 return Err(EngineError::Input(
1091 "invalid MIMO IIR tuning: a parameter range is not wider than 2e-6".into(),
1092 ));
1093 }
1094 Ok(())
1095}
1096
1097#[allow(clippy::too_many_arguments)]
1098pub fn design_mimo_iir(
1099 design: &Design,
1100 measurements: &MeasurementSet,
1101 config: &EngineConfig,
1102 ideal: &MimoDesign,
1103 req: &MimoRequest,
1104 t: &Tuning,
1105 ctx: &Context,
1106) -> Result<MimoIir, EngineError> {
1107 design_mimo_iir_with_options(
1108 design,
1109 measurements,
1110 config,
1111 ideal,
1112 req,
1113 t,
1114 &IirSolveOptions::default(),
1115 ctx,
1116 )
1117}
1118
1119#[allow(clippy::too_many_arguments)]
1124pub fn design_mimo_iir_with_options(
1125 design: &Design,
1126 measurements: &MeasurementSet,
1127 config: &EngineConfig,
1128 ideal: &MimoDesign,
1129 req: &MimoRequest,
1130 t: &Tuning,
1131 options: &IirSolveOptions,
1132 ctx: &Context,
1133) -> Result<MimoIir, EngineError> {
1134 check(ctx)?;
1135 validate_request(req, t)?;
1136 options.validate()?;
1137 if ideal.supports.iter().all(|(_, s)| s.is_empty()) {
1138 let _progress = ctx.stage("engine.mimo_iir", 0);
1139 return Ok(MimoIir::default());
1140 }
1141 let prepared = super::prepared::Prepared::new(design, measurements, config, ctx)?;
1142 design_mimo_iir_prepared_with_options(design, &prepared, ideal, req, t, options, ctx)
1143}
1144
1145pub(crate) fn design_mimo_iir_prepared(
1146 design: &Design,
1147 prepared: &super::prepared::Prepared<'_>,
1148 ideal: &MimoDesign,
1149 req: &MimoRequest,
1150 t: &Tuning,
1151 ctx: &Context,
1152) -> Result<MimoIir, EngineError> {
1153 design_mimo_iir_prepared_with_options(
1154 design,
1155 prepared,
1156 ideal,
1157 req,
1158 t,
1159 &IirSolveOptions::default(),
1160 ctx,
1161 )
1162}
1163
1164#[allow(clippy::too_many_arguments)]
1165pub(crate) fn design_mimo_iir_prepared_with_options(
1166 design: &Design,
1167 prepared: &super::prepared::Prepared<'_>,
1168 ideal: &MimoDesign,
1169 req: &MimoRequest,
1170 t: &Tuning,
1171 options: &IirSolveOptions,
1172 ctx: &Context,
1173) -> Result<MimoIir, EngineError> {
1174 check(ctx)?;
1175 validate_request(req, t)?;
1176 options.validate()?;
1177 let inputs: Vec<_> = ideal
1178 .supports
1179 .iter()
1180 .filter(|(_, s)| !s.is_empty())
1181 .collect();
1182 let progress = ctx.stage("engine.mimo_iir", inputs.len() as u64);
1183 if inputs.is_empty() {
1184 return Ok(MimoIir::default());
1185 }
1186 let spectra = &prepared.spectra;
1187 let positions = &design.spectra.positions;
1188 let results = crate::memory::try_map(
1189 ctx,
1190 inputs,
1191 prepared.reserved,
1192 |(_, supports)| {
1193 let r = supports.len();
1194 let dim = r * per(t);
1195 let rows = 2 * positions.len() * N + 1 + 2 * r * (N + 1) + r * t.mimo_k_bands;
1196 let solver_bytes = if options.solver == IirSolver::Dense || options.dense_fallback {
1197 4 * N * dim + 12 * dim * dim
1198 } else {
1199 8 * rows + 24 * dim + 8 * r * (N + 1)
1200 };
1201 let bytes = 8
1205 * (positions.len() * N * (3 + 4 * r)
1206 + 4 * dim * (N + 1)
1207 + solver_bytes
1208 + 320 * per(t) * 4
1209 + 32 * design_freqs().len()
1210 + 32768);
1211 bytes as u64
1212 },
1213 |(input, supports)| {
1214 let pb = build_problem(
1215 design,
1216 spectra,
1217 &prepared.align_delay_ms,
1218 ideal,
1219 input,
1220 supports,
1221 req,
1222 t,
1223 ctx,
1224 )?;
1225 let (x, pols) = solve_problem_with_options(&pb, t, ctx, options)?;
1226 let post = postprocess(&pb, &x, &pols, t, ctx)?;
1227 let paths = post
1228 .supports
1229 .into_iter()
1230 .map(|(r, s)| (pb.roles[r].clone(), s))
1231 .collect();
1232 progress.advance(1);
1233 Ok((input.clone(), paths, post.removed))
1234 },
1235 )?;
1236 let mut out = MimoIir::default();
1237 for (input, paths, removed) in results {
1238 out.supports.push((input.clone(), paths));
1239 out.removed.push((input, removed));
1240 }
1241 Ok(out)
1242}
1243
1244impl NormalEquations for Objective<'_> {
1245 fn residual(&mut self, x: &[f64]) -> Vec<f64> {
1246 if self.ctx.cancel().is_cancelled() {
1247 return vec![0.0; self.rows()];
1248 }
1249 self.evaluate(x);
1250 let pb = self.pb;
1251 let mut out = Vec::with_capacity(self.rows());
1252 for p in 0..pb.xs.len() {
1253 let mut sum = vec![C::new(0.0, 0.0); pb.f.len()];
1254 for (r, part) in self.cache.iter().enumerate() {
1255 for (k, v) in sum.iter_mut().enumerate() {
1256 *v += pb.g[p][r][k] * part.s[k];
1257 }
1258 }
1259 let add = fft::irfft(&sum, pb.nf);
1260 out.extend((0..pb.n).map(|i| pb.wl[p][i] * (pb.xs[p][i] + add[i])));
1261 out.extend((0..pb.n).map(|i| MU.sqrt() * pb.we[p][i] * add[i]));
1262 }
1263 out.push(self.level().2.max(0.0) * self.scale);
1264 for (r, part) in self.cache.iter().enumerate() {
1265 out.extend(part.s.iter().zip(&self.weff[r]).map(|(h, w)| w * h.re));
1266 out.extend(part.s.iter().zip(&self.weff[r]).map(|(h, w)| w * h.im));
1267 }
1268 if self.t.mimo_band_reg > 0.0 {
1269 for r in 0..pb.roles.len() {
1270 for k in 0..self.t.mimo_k_bands {
1271 out.push(
1272 self.t.mimo_band_reg * self.scale * x[r * per(self.t) + 4 + 3 * k] / 30.0,
1273 );
1274 }
1275 }
1276 }
1277 out
1278 }
1279
1280 fn normal(&mut self, x: &[f64], f: &[f64]) -> Result<(Mat<f64>, Vec<f64>), DspError> {
1281 let dim = x.len();
1282 if self.ctx.cancel().is_cancelled() {
1283 return Ok((Mat::zeros(dim, dim), vec![0.0; dim]));
1284 }
1285 self.derivatives(x);
1286 let pb = self.pb;
1287 let width = per(self.t);
1288 let mut h = Mat::zeros(dim, dim);
1289 let mut grad = vec![0.0; dim];
1290 for p in 0..pb.xs.len() {
1293 let mut c = Mat::zeros(pb.n, dim);
1294 for (r, part) in self.cache.iter().enumerate() {
1295 for (a, d) in part.d.iter().enumerate() {
1296 let column = time_product(&pb.g[p][r], d, pb.nf, pb.n);
1297 for (i, &v) in column.iter().enumerate() {
1298 c[(i, r * width + a)] = v;
1299 }
1300 }
1301 }
1302 let weighted = Mat::from_fn(pb.n, dim, |i, j| {
1303 c[(i, j)] * (pb.wl[p][i] * pb.wl[p][i] + MU * pb.we[p][i] * pb.we[p][i])
1304 });
1305 matmul(
1306 h.as_mut(),
1307 Accum::Add,
1308 c.transpose(),
1309 &weighted,
1310 1.0,
1311 Par::Seq,
1312 );
1313 for j in 0..dim {
1314 for i in 0..pb.n {
1315 grad[j] += c[(i, j)]
1316 * (pb.wl[p][i] * f[2 * p * pb.n + i]
1317 + MU.sqrt() * pb.we[p][i] * f[(2 * p + 1) * pb.n + i]);
1318 }
1319 }
1320 }
1321 let (st, lvl, over) = self.level();
1322 let mut level = vec![0.0; dim];
1323 for (r, part) in self.cache.iter().enumerate() {
1324 let effort = Mat::from_fn(2 * pb.f.len(), width, |i, j| {
1325 let k = i % pb.f.len();
1326 self.weff[r][k]
1327 * if i < pb.f.len() {
1328 part.d[j][k].re
1329 } else {
1330 part.d[j][k].im
1331 }
1332 });
1333 let e0 = 2 * pb.xs.len() * pb.n + 1 + 2 * r * pb.f.len();
1334 let mut block = Mat::zeros(width, width);
1335 matmul(
1336 &mut block,
1337 Accum::Replace,
1338 effort.transpose(),
1339 &effort,
1340 1.0,
1341 Par::Seq,
1342 );
1343 for a in 0..width {
1344 for b in 0..width {
1345 h[(r * width + a, r * width + b)] += block[(a, b)];
1346 }
1347 for i in 0..2 * pb.f.len() {
1348 grad[r * width + a] += effort[(i, a)] * f[e0 + i];
1349 }
1350 if over > 0.0 {
1351 let ct = time_product(&part.d[a], &pb.lp, pb.nf, pb.n);
1352 let dot = ct.iter().zip(&st[r]).map(|(a, b)| a * b).sum::<f64>();
1353 level[r * width + a] = self.scale * 10.0 / (10f64.ln() * (lvl + 1e-30))
1354 * (2.0 * dot / pb.direct.max(1e-30));
1355 }
1356 }
1357 }
1358 if over > 0.0 {
1359 for a in 0..dim {
1360 grad[a] += level[a] * f[2 * pb.xs.len() * pb.n];
1361 for b in 0..dim {
1362 h[(a, b)] += level[a] * level[b];
1363 }
1364 }
1365 }
1366 if self.t.mimo_band_reg > 0.0 {
1367 let factor = self.t.mimo_band_reg * self.scale / 30.0;
1368 let row = 2 * pb.xs.len() * pb.n + 1 + 2 * pb.roles.len() * pb.f.len();
1369 for r in 0..pb.roles.len() {
1370 for k in 0..self.t.mimo_k_bands {
1371 let a = r * width + 4 + 3 * k;
1372 h[(a, a)] += factor * factor;
1373 grad[a] += factor * f[row + r * self.t.mimo_k_bands + k];
1374 }
1375 }
1376 }
1377 Ok((h, grad))
1378 }
1379}
1380
1381fn inverse_adjoint(time: &[f64], nf: usize) -> Vec<C> {
1385 let mut padded = time.to_vec();
1386 padded.resize(nf, 0.0);
1387 let mut spectrum = fft::rfft(&padded);
1388 for (k, h) in spectrum.iter_mut().enumerate() {
1389 let endpoint = k == 0 || (nf.is_multiple_of(2) && k == nf / 2);
1390 *h *= if endpoint {
1391 1.0 / nf as f64
1392 } else {
1393 2.0 / nf as f64
1394 };
1395 if endpoint {
1396 h.im = 0.0;
1397 }
1398 }
1399 spectrum
1400}
1401
1402impl JacobianOperator for Objective<'_> {
1403 fn residual(&mut self, x: &[f64]) -> Vec<f64> {
1404 NormalEquations::residual(self, x)
1405 }
1406
1407 fn linearize(&mut self, x: &[f64]) -> Result<Vec<f64>, DspError> {
1408 let width = per(self.t);
1409 let dim = x.len();
1410 if self.ctx.cancel().is_cancelled() {
1411 self.operator = Some(OperatorState {
1412 derivatives: Vec::new(),
1413 level: vec![0.0; dim],
1414 });
1415 return Ok(vec![0.0; dim]);
1416 }
1417 self.derivatives(x);
1418 let pb = self.pb;
1419 let mut diagonal = vec![0.0; dim];
1420 for p in 0..pb.xs.len() {
1423 for (r, part) in self.cache.iter().enumerate() {
1424 for (a, d) in part.d.iter().enumerate() {
1425 let column = time_product(&pb.g[p][r], d, pb.nf, pb.n);
1426 for (i, value) in column.into_iter().enumerate() {
1427 diagonal[r * width + a] += value
1428 * value
1429 * (pb.wl[p][i] * pb.wl[p][i] + MU * pb.we[p][i] * pb.we[p][i]);
1430 }
1431 }
1432 }
1433 }
1434 let (st, lvl, over) = self.level();
1435 let mut level = vec![0.0; dim];
1436 for (r, part) in self.cache.iter().enumerate() {
1437 for (a, derivative) in part.d.iter().enumerate() {
1438 let j = r * width + a;
1439 for component in [false, true] {
1440 for (k, d) in derivative.iter().enumerate() {
1441 let v = self.weff[r][k] * if component { d.im } else { d.re };
1442 diagonal[j] += v * v;
1443 }
1444 }
1445 if over > 0.0 {
1446 let ct = time_product(derivative, &pb.lp, pb.nf, pb.n);
1447 let dot = ct.iter().zip(&st[r]).map(|(a, b)| a * b).sum::<f64>();
1448 level[j] = self.scale * 10.0 / (10f64.ln() * (lvl + 1e-30))
1449 * (2.0 * dot / pb.direct.max(1e-30));
1450 diagonal[j] += level[j] * level[j];
1451 }
1452 }
1453 }
1454 if self.t.mimo_band_reg > 0.0 {
1455 let factor = self.t.mimo_band_reg * self.scale / 30.0;
1456 for r in 0..pb.roles.len() {
1457 for k in 0..self.t.mimo_k_bands {
1458 diagonal[r * width + 4 + 3 * k] += factor * factor;
1459 }
1460 }
1461 }
1462 self.operator = Some(OperatorState {
1465 derivatives: self
1466 .cache
1467 .iter_mut()
1468 .map(|part| std::mem::take(&mut part.d))
1469 .collect(),
1470 level,
1471 });
1472 Ok(diagonal)
1473 }
1474
1475 fn jvp(&self, direction: &[f64], out: &mut [f64]) -> Result<(), DspError> {
1476 let pb = self.pb;
1477 let width = per(self.t);
1478 if direction.len() != width * pb.roles.len() || out.len() != self.rows() {
1479 return Err(DspError::InvalidArgument(
1480 "invalid IIR Jacobian product shape".into(),
1481 ));
1482 }
1483 out.fill(0.0);
1484 if self.ctx.cancel().is_cancelled() {
1485 return Ok(());
1486 }
1487 let state = self
1488 .operator
1489 .as_ref()
1490 .ok_or_else(|| DspError::InvalidArgument("IIR operator has no linearization".into()))?;
1491 let mut responses = vec![vec![C::new(0.0, 0.0); pb.f.len()]; pb.roles.len()];
1492 for (r, derivatives) in state.derivatives.iter().enumerate() {
1493 for (a, derivative) in derivatives.iter().enumerate() {
1494 for (k, d) in derivative.iter().enumerate() {
1495 responses[r][k] += d * direction[r * width + a];
1496 }
1497 }
1498 }
1499 for p in 0..pb.xs.len() {
1500 let mut sum = vec![C::new(0.0, 0.0); pb.f.len()];
1501 for (r, response) in responses.iter().enumerate() {
1502 for (k, value) in sum.iter_mut().enumerate() {
1503 *value += pb.g[p][r][k] * response[k];
1504 }
1505 }
1506 let time = fft::irfft(&sum, pb.nf);
1507 for i in 0..pb.n {
1508 out[2 * p * pb.n + i] = pb.wl[p][i] * time[i];
1509 out[(2 * p + 1) * pb.n + i] = MU.sqrt() * pb.we[p][i] * time[i];
1510 }
1511 }
1512 let level_row = 2 * pb.xs.len() * pb.n;
1513 out[level_row] = state.level.iter().zip(direction).map(|(g, v)| g * v).sum();
1514 for (r, response) in responses.iter().enumerate() {
1515 let base = level_row + 1 + 2 * r * pb.f.len();
1516 for (k, h) in response.iter().enumerate() {
1517 out[base + k] = self.weff[r][k] * h.re;
1518 out[base + pb.f.len() + k] = self.weff[r][k] * h.im;
1519 }
1520 }
1521 if self.t.mimo_band_reg > 0.0 {
1522 let base = level_row + 1 + 2 * pb.roles.len() * pb.f.len();
1523 let factor = self.t.mimo_band_reg * self.scale / 30.0;
1524 for r in 0..pb.roles.len() {
1525 for k in 0..self.t.mimo_k_bands {
1526 out[base + r * self.t.mimo_k_bands + k] =
1527 factor * direction[r * width + 4 + 3 * k];
1528 }
1529 }
1530 }
1531 Ok(())
1532 }
1533
1534 fn vjp(&self, residual_direction: &[f64], out: &mut [f64]) -> Result<(), DspError> {
1535 let pb = self.pb;
1536 let width = per(self.t);
1537 if out.len() != width * pb.roles.len() || residual_direction.len() != self.rows() {
1538 return Err(DspError::InvalidArgument(
1539 "invalid IIR transpose product shape".into(),
1540 ));
1541 }
1542 out.fill(0.0);
1543 if self.ctx.cancel().is_cancelled() {
1544 return Ok(());
1545 }
1546 let state = self
1547 .operator
1548 .as_ref()
1549 .ok_or_else(|| DspError::InvalidArgument("IIR operator has no linearization".into()))?;
1550 let mut adjoints = vec![vec![C::new(0.0, 0.0); pb.f.len()]; pb.roles.len()];
1551 for p in 0..pb.xs.len() {
1552 let time: Vec<_> = (0..pb.n)
1553 .map(|i| {
1554 pb.wl[p][i] * residual_direction[2 * p * pb.n + i]
1555 + MU.sqrt() * pb.we[p][i] * residual_direction[(2 * p + 1) * pb.n + i]
1556 })
1557 .collect();
1558 let spectrum = inverse_adjoint(&time, pb.nf);
1559 for (r, adjoint) in adjoints.iter_mut().enumerate() {
1560 for (k, value) in adjoint.iter_mut().enumerate() {
1561 *value += pb.g[p][r][k].conj() * spectrum[k];
1562 }
1563 }
1564 }
1565 let level_row = 2 * pb.xs.len() * pb.n;
1566 for (r, adjoint) in adjoints.iter_mut().enumerate() {
1567 let base = level_row + 1 + 2 * r * pb.f.len();
1568 for (k, value) in adjoint.iter_mut().enumerate() {
1569 *value += C::new(
1570 self.weff[r][k] * residual_direction[base + k],
1571 self.weff[r][k] * residual_direction[base + pb.f.len() + k],
1572 );
1573 }
1574 for (a, derivative) in state.derivatives[r].iter().enumerate() {
1575 let j = r * width + a;
1576 out[j] = derivative
1577 .iter()
1578 .zip(&*adjoint)
1579 .map(|(d, a)| d.re * a.re + d.im * a.im)
1580 .sum::<f64>()
1581 + state.level[j] * residual_direction[level_row];
1582 }
1583 }
1584 if self.t.mimo_band_reg > 0.0 {
1585 let base = level_row + 1 + 2 * pb.roles.len() * pb.f.len();
1586 let factor = self.t.mimo_band_reg * self.scale / 30.0;
1587 for r in 0..pb.roles.len() {
1588 for k in 0..self.t.mimo_k_bands {
1589 out[r * width + 4 + 3 * k] +=
1590 factor * residual_direction[base + r * self.t.mimo_k_bands + k];
1591 }
1592 }
1593 }
1594 Ok(())
1595 }
1596}
1597
1598#[cfg(test)]
1599mod operator_tests {
1600 use super::*;
1601
1602 fn fixture(level_db: f64) -> (Problem, Tuning, Vec<f64>, Vec<i8>) {
1603 let (n, nf) = (64, 128);
1604 let f = secs_dsp::grid::freqs(nf, FS).unwrap();
1605 let t = Tuning {
1606 mimo_k_bands: 2,
1607 mimo_a_bands: 1,
1608 mimo_max_nfev: 25,
1609 ..Tuning::default()
1610 };
1611 let lp = response::lr4(&f, 100.0, "LP");
1612 let xs: Vec<Vec<f64>> = (0..2)
1613 .map(|p| {
1614 (0..n)
1615 .map(|i| {
1616 let time = i as f64;
1617 (time * 0.6 + p as f64 * 0.1).cos() * (-time / 40.0).exp()
1618 })
1619 .collect()
1620 })
1621 .collect();
1622 let wl: Vec<Vec<f64>> = (0..2)
1623 .map(|p| (0..n).map(|i| if i >= 8 + p { 1.0 } else { 0.0 }).collect())
1624 .collect();
1625 let we = wl
1626 .iter()
1627 .map(|w| w.iter().map(|w| 1.0 - w).collect())
1628 .collect();
1629 let g = (0..2)
1630 .map(|p| {
1631 (0..2)
1632 .map(|r| {
1633 let mut ir = vec![0.0; nf];
1634 for (i, value) in ir.iter_mut().enumerate().take(n).skip(2 + p + r) {
1635 *value = (-((i - 2 - p - r) as f64) / 12.0).exp()
1636 * (i as f64 * (0.4 + r as f64 * 0.1)).cos();
1637 }
1638 fft::rfft(&ir).iter().zip(&lp).map(|(a, b)| a * b).collect()
1639 })
1640 .collect()
1641 })
1642 .collect();
1643 let x = (0..2)
1644 .flat_map(|r| {
1645 [
1646 -6.0 - r as f64,
1647 12.0 + r as f64,
1648 50.0_f64.ln(),
1649 2.0_f64.ln(),
1650 3.0,
1651 110.0_f64.ln(),
1652 1.3_f64.ln(),
1653 -4.0,
1654 70.0_f64.ln(),
1655 1.5_f64.ln(),
1656 ]
1657 })
1658 .collect();
1659 (
1660 Problem {
1661 roles: vec!["SW1".into(), "SW2".into()],
1662 f,
1663 lp,
1664 n,
1665 nf,
1666 xs,
1667 wl,
1668 we,
1669 g,
1670 fir: vec![vec![0.0; n]; 2],
1671 ranges: vec![(20.0, 150.0); 2],
1672 modes: vec![50.0, 80.0],
1673 fsiso: 100.0,
1674 direct: 1.0,
1675 level_db,
1676 },
1677 t,
1678 x,
1679 vec![1, -1],
1680 )
1681 }
1682
1683 fn dot(a: &[f64], b: &[f64]) -> f64 {
1684 a.iter().zip(b).map(|(a, b)| a * b).sum()
1685 }
1686
1687 #[test]
1688 fn real_inverse_adjoint_handles_dc_nyquist_and_truncation() {
1689 for nf in [63, 64] {
1690 let spectrum: Vec<_> = (0..nf / 2 + 1)
1691 .map(|k| C::new((k as f64 * 0.3).sin(), (k as f64 * 0.7).cos()))
1692 .collect();
1693 let time: Vec<_> = (0..17).map(|i| (i as f64 * 0.5).cos()).collect();
1694 let transformed = fft::irfft(&spectrum, nf);
1695 let lhs = dot(&transformed[..time.len()], &time);
1696 let adjoint = inverse_adjoint(&time, nf);
1697 let rhs: f64 = spectrum
1698 .iter()
1699 .zip(&adjoint)
1700 .map(|(a, b)| a.re * b.re + a.im * b.im)
1701 .sum();
1702 assert!((lhs - rhs).abs() < 1e-12 * lhs.abs().max(rhs.abs()).max(1.0));
1703 }
1704 }
1705
1706 #[test]
1707 fn iir_operator_matches_derivatives_normal_equations_and_adjoint() {
1708 for level_db in [-80.0, 80.0] {
1709 let (pb, tuning, x, pols) = fixture(level_db);
1710 let ctx = Context::serial();
1711 let mut objective = Objective::new(&pb, &tuning, &ctx, &pols);
1712 let f = NormalEquations::residual(&mut objective, &x);
1713 assert!(
1714 objective.cache.iter().all(|part| part.d.is_empty()),
1715 "residual calculated unused derivatives"
1716 );
1717 let (normal, gradient) = objective.normal(&x, &f).unwrap();
1718 let diagonal = objective.linearize(&x).unwrap();
1719 let direction: Vec<_> = (0..x.len()).map(|i| (i as f64 * 0.4).sin()).collect();
1720 let row_direction: Vec<_> = (0..f.len()).map(|i| (i as f64 * 0.13).cos()).collect();
1721 let mut jv = vec![0.0; f.len()];
1722 objective.jvp(&direction, &mut jv).unwrap();
1723 let mut jtu = vec![0.0; x.len()];
1724 objective.vjp(&row_direction, &mut jtu).unwrap();
1725 let lhs = dot(&jv, &row_direction);
1726 let rhs = dot(&direction, &jtu);
1727 assert!((lhs - rhs).abs() < 1e-10 * lhs.abs().max(rhs.abs()).max(1.0));
1728 let mut jt_jv = vec![0.0; x.len()];
1729 objective.vjp(&jv, &mut jt_jv).unwrap();
1730 let mut jt_f = vec![0.0; x.len()];
1731 objective.vjp(&f, &mut jt_f).unwrap();
1732 for j in 0..x.len() {
1733 let expected: f64 = (0..x.len()).map(|k| normal[(j, k)] * direction[k]).sum();
1734 assert!((jt_jv[j] - expected).abs() < 2e-10 * expected.abs().max(1.0));
1735 assert!((jt_f[j] - gradient[j]).abs() < 2e-10 * gradient[j].abs().max(1.0));
1736 assert!(
1737 (diagonal[j] - normal[(j, j)]).abs() < 2e-10 * normal[(j, j)].abs().max(1.0)
1738 );
1739 }
1740 let up: Vec<_> = x
1741 .iter()
1742 .zip(&direction)
1743 .map(|(x, v)| x + 1e-5 * v)
1744 .collect();
1745 let down: Vec<_> = x
1746 .iter()
1747 .zip(&direction)
1748 .map(|(x, v)| x - 1e-5 * v)
1749 .collect();
1750 let up = NormalEquations::residual(&mut objective, &up);
1751 let down = NormalEquations::residual(&mut objective, &down);
1752 let error = jv
1753 .iter()
1754 .zip(up.iter().zip(down))
1755 .map(|(d, (u, l))| (d - (u - l) / 2e-5).abs())
1756 .fold(0.0_f64, f64::max);
1757 let scale = jv.iter().map(|v| v.abs()).fold(1.0_f64, f64::max);
1758 assert!(
1759 error < 5e-6 * scale,
1760 "analytic directional derivative {error}/{scale}"
1761 );
1762 let mut unchanged = vec![0.0; f.len()];
1763 objective.jvp(&direction, &mut unchanged).unwrap();
1764 assert!(
1765 unchanged
1766 .iter()
1767 .zip(&jv)
1768 .all(|(a, b)| a.to_bits() == b.to_bits()),
1769 "trial residual invalidated the retained operator"
1770 );
1771 }
1772 }
1773
1774 #[test]
1775 fn iir_matrix_free_solve_is_reachable_and_reduces_the_same_objective() {
1776 let (pb, tuning, x, pols) = fixture(80.0);
1777 let ctx = Context::serial();
1778 let mut objective = Objective::new(&pb, &tuning, &ctx, &pols);
1779 let initial = 0.5 * decay::energy(&NormalEquations::residual(&mut objective, &x));
1780 let options = IirSolveOptions {
1781 solver: IirSolver::MatrixFree {
1782 max_cg_iterations: 20,
1783 cg_rtol: 1e-4,
1784 },
1785 dense_fallback: false,
1786 ..IirSolveOptions::default()
1787 };
1788 let result = solve_joint_with_options(&pb, &tuning, &ctx, &x, &pols, &options).unwrap();
1789 assert!(result.cost < initial * 0.95, "{initial} -> {}", result.cost);
1790 assert!(result.nfev > 1 && result.nfev <= tuning.mimo_max_nfev);
1791 for (r, &(lo, hi)) in pb.ranges.iter().enumerate() {
1792 let (lb, ub) = bounds(band_of(lo, hi, pb.fsiso, &tuning), &tuning);
1793 assert!(
1794 result.x[r * per(&tuning)..(r + 1) * per(&tuning)]
1795 .iter()
1796 .zip(lb)
1797 .zip(ub)
1798 .all(|((&x, l), u)| x >= l && x <= u)
1799 );
1800 }
1801 }
1802
1803 #[test]
1804 fn iir_level_derivative_respects_the_additive_energy_floor() {
1805 let (mut pb, tuning, x, pols) = fixture(-400.0);
1806 for h in &mut pb.lp {
1807 *h *= 1e-15;
1808 }
1809 let ctx = Context::serial();
1810 let mut objective = Objective::new(&pb, &tuning, &ctx, &pols);
1811 let _ = NormalEquations::residual(&mut objective, &x);
1812 objective.linearize(&x).unwrap();
1813 let mut direction = vec![0.0; x.len()];
1814 direction[0] = 1.0;
1815 let mut jv = vec![0.0; objective.rows()];
1816 objective.jvp(&direction, &mut jv).unwrap();
1817 let (mut up, mut down) = (x.clone(), x);
1818 up[0] += 1e-3;
1819 down[0] -= 1e-3;
1820 let up = NormalEquations::residual(&mut objective, &up);
1821 let down = NormalEquations::residual(&mut objective, &down);
1822 let row = 2 * pb.xs.len() * pb.n;
1823 let central = (up[row] - down[row]) / 2e-3;
1824 assert!((jv[row] - central).abs() < 1e-7 * central.abs().max(1.0));
1825 }
1826
1827 #[test]
1828 fn iir_operator_error_preserves_dense_fallback() {
1829 let (pb, tuning, x, pols) = fixture(80.0);
1830 let ctx = Context::serial();
1831 let options = IirSolveOptions {
1834 solver: IirSolver::MatrixFree {
1835 max_cg_iterations: 0,
1836 cg_rtol: 1e-3,
1837 },
1838 dense_fallback: true,
1839 ..IirSolveOptions::default()
1840 };
1841 assert!(options.validate().is_err());
1842 let recovered = solve_joint_with_options(&pb, &tuning, &ctx, &x, &pols, &options).unwrap();
1843 let dense = solve_joint(&pb, &tuning, &ctx, &x, &pols).unwrap();
1844 assert_eq!(recovered.cost.to_bits(), dense.cost.to_bits());
1845 assert_eq!(recovered.nfev, dense.nfev);
1846 assert!(
1847 recovered
1848 .x
1849 .iter()
1850 .zip(dense.x)
1851 .all(|(a, b)| a.to_bits() == b.to_bits())
1852 );
1853 assert!(
1854 solve_joint_with_options(
1855 &pb,
1856 &tuning,
1857 &ctx,
1858 &x,
1859 &pols,
1860 &IirSolveOptions {
1861 dense_fallback: false,
1862 ..options
1863 }
1864 )
1865 .is_err()
1866 );
1867 }
1868}
1869
1870#[cfg(test)]
1871mod tuning_tests {
1872 use super::*;
1873
1874 #[test]
1875 fn iir_tuning_ranges_are_checked_at_entry() {
1876 let req = MimoRequest::default();
1879 let ok = |t: Tuning| validate_request(&req, &t).is_ok();
1880 assert!(ok(Tuning::default()));
1881 assert!(ok(Tuning {
1883 mimo_k_bands: 106,
1884 mimo_a_bands: 0,
1885 ..Tuning::default()
1886 }));
1887 for (k, a) in [(106, 1), (0, 160), (usize::MAX, 0), (0, usize::MAX)] {
1888 let t = Tuning {
1889 mimo_k_bands: k,
1890 mimo_a_bands: a,
1891 ..Tuning::default()
1892 };
1893 assert!(
1894 matches!(validate_request(&req, &t), Err(EngineError::Input(_))),
1895 "{k} peaking and {a} all-pass bands"
1896 );
1897 }
1898 assert!(ok(Tuning {
1899 mimo_band_db: 2e-6,
1900 ..Tuning::default()
1901 }));
1902 for t in [
1903 Tuning {
1904 mimo_band_db: 1e-6,
1905 ..Tuning::default()
1906 },
1907 Tuning {
1908 mimo_q_max: 0.5 * 1e-6f64.exp(),
1909 ..Tuning::default()
1910 },
1911 Tuning {
1912 mimo_gain_max_db: -80.0 + 1e-6,
1913 ..Tuning::default()
1914 },
1915 ] {
1916 assert!(
1917 matches!(validate_request(&req, &t), Err(EngineError::Input(_))),
1918 "{t:?}"
1919 );
1920 }
1921 }
1922}