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