1use secs_dsp::{
5 fft,
6 fir::firwin,
7 iir::{BType, Sos, butter, sosfilt},
8 interp, pairwise_sum,
9 response::hp2,
10 smooth::{Fraction, smooth_power},
11};
12
13use crate::{
14 C, Design, EngineError, Note, Spectra,
15 decay::{self, onset},
16 grid::design_freqs,
17 measurement::delay_response,
18};
19
20const FS_LOW: f64 = 600.0;
21const EARLY: usize = 36;
22
23use super::solve::solve;
24use crate::{EngineConfig, MeasurementSet};
25#[cfg(test)]
26use secs_compute::MapError;
27use secs_compute::{Context, MemoryError};
28
29#[derive(Debug, Clone, PartialEq)]
30pub struct MimoRequest {
31 pub fsiso: f64,
32 pub level_db: f64,
33 pub f_low: Option<f64>,
34 pub max_supports: Option<usize>,
35 pub exclude: Vec<String>,
36 pub groups: Option<Vec<String>>,
37 pub taps_low: usize,
38}
39
40impl Default for MimoRequest {
41 fn default() -> Self {
42 Self {
43 fsiso: 150.0,
44 level_db: -12.0,
45 f_low: None,
46 max_supports: None,
47 exclude: Vec::new(),
48 groups: None,
49 taps_low: 180,
50 }
51 }
52}
53
54pub type SupportRanges = Vec<(String, Option<(f64, f64)>)>;
55pub type InputSupports = Vec<(String, Vec<(String, Vec<f64>)>)>;
56
57#[derive(Debug, Clone, PartialEq)]
58pub struct MimoDesign {
59 pub fs_low: f64,
60 pub ranges: SupportRanges,
61 pub supports: InputSupports,
62 pub level_db: Vec<(String, f64)>,
63 pub notes: Vec<Note>,
64}
65
66impl Default for MimoDesign {
67 fn default() -> Self {
68 Self {
69 fs_low: FS_LOW,
70 ranges: Vec::new(),
71 supports: Vec::new(),
72 level_db: Vec::new(),
73 notes: Vec::new(),
74 }
75 }
76}
77
78pub fn lowpass_fir(fc: f64, taps: usize) -> Result<Vec<f64>, EngineError> {
79 Ok(firwin(taps, fc, FS_LOW)?)
80}
81
82fn geomspace(start: f64, end: f64) -> Vec<f64> {
83 let step = (end.log10() - start.log10()) / 199.0;
84 let mut values: Vec<_> = (0..200)
85 .map(|k| 10f64.powf(start.log10() + k as f64 * step))
86 .collect();
87 values[0] = start;
88 values[199] = end;
89 values
90}
91
92pub fn support_ranges(
95 design: &Design,
96 spectra: &Spectra,
97 fsiso: f64,
98 positions: &[u32],
99) -> Result<SupportRanges, EngineError> {
100 if positions.is_empty() || !fsiso.is_finite() || fsiso <= 0.0 {
101 return Err(EngineError::Input("invalid MIMO range inputs".into()));
102 }
103 let f = design_freqs();
104 let mut out = Vec::new();
105 for (role, sub) in design
106 .mains
107 .iter()
108 .map(|m| (m.role.as_str(), None))
109 .chain(design.chain.subs.iter().map(|s| (s.role.as_str(), Some(s))))
110 {
111 let Some(paths): Option<Vec<_>> = positions.iter().map(|&p| spectra.get(p, role)).collect()
112 else {
113 continue;
114 };
115 let mut power = vec![0.0; f.len()];
116 for h in paths {
117 if h.len() != f.len() {
118 return Err(EngineError::Input("invalid MIMO spectrum length".into()));
119 }
120 for (sum, h) in power.iter_mut().zip(h.iter()) {
121 let a = h.re.hypot(h.im);
122 *sum += a * a;
123 }
124 }
125 for p in &mut power {
126 *p /= positions.len() as f64;
127 }
128 let db: Vec<_> = smooth_power(f, &power, Fraction::Scalar(3.0))?
129 .iter()
130 .map(|p| 10.0 * (p + 1e-30).log10())
131 .collect();
132 let (lo_ref, hi_ref) = if sub.is_some() {
133 (30.0, 80.0)
134 } else {
135 (200.0, 1000.0)
136 };
137 let mut reference = decay::median(
138 &f.iter()
139 .zip(&db)
140 .filter(|(f, _)| (lo_ref..=hi_ref).contains(*f))
141 .map(|(_, &v)| v)
142 .collect::<Vec<_>>(),
143 );
144 let mut start = if sub.is_some() {
145 50.0
146 } else {
147 fsiso.min(200.0)
148 };
149 if let Some(s) = sub.filter(|s| s.hp_hz != 0.0 || s.lp_hz != 0.0) {
150 let lo = s.hp_hz.max(15.0);
151 let hi = (if s.lp_hz != 0.0 {
152 s.lp_hz.min(80.0)
153 } else {
154 80.0
155 })
156 .max(lo * 1.5);
157 let indices: Vec<_> = f
158 .iter()
159 .enumerate()
160 .filter(|(_, f)| (lo..=hi).contains(f))
161 .map(|(k, _)| k)
162 .collect();
163 let values: Vec<_> = indices.iter().map(|&k| db[k]).collect();
164 let k = indices[decay::first_max(&values)];
165 reference = db[k];
166 start = f[k];
167 }
168 let low = geomspace(start, 15.0)
169 .into_iter()
170 .find(|&v| interp(v, f, &db) < reference - 10.0)
171 .unwrap_or(15.0)
172 .max(if sub.is_some() { 20.0 } else { 50.0 });
173 let high = geomspace(start, fsiso)
174 .into_iter()
175 .find(|&v| interp(v, f, &db) < reference - 10.0)
176 .unwrap_or(fsiso);
177 out.push((
178 role.to_owned(),
179 (low < high.min(fsiso) * 0.8).then(|| {
180 (
181 (low * 10.0).round_ties_even() / 10.0,
182 (high * 10.0).round_ties_even() / 10.0,
183 )
184 }),
185 ));
186 }
187 Ok(out)
188}
189
190pub fn support_path(
192 mimo: &MimoDesign,
193 role: &str,
194 spectra: &Spectra,
195 position: u32,
196 align_delay_ms: &[(String, f64)],
197) -> Result<Vec<C>, EngineError> {
198 let h = spectra
199 .get(position, role)
200 .ok_or_else(|| EngineError::Input(format!("missing MIMO path {position}/{role}")))?;
201 let delay = align_delay_ms
202 .iter()
203 .find(|(r, _)| r == role)
204 .map_or(0.0, |(_, ms)| *ms);
205 let low = mimo
206 .ranges
207 .iter()
208 .find(|(r, _)| r == role)
209 .and_then(|(_, r)| *r)
210 .map_or(50.0, |r| r.0);
211 Ok(h.iter()
212 .zip(delay_response(delay))
213 .zip(hp2(design_freqs(), low))
214 .map(|((&h, d), hp)| h * d * (hp * hp))
215 .collect())
216}
217
218pub(crate) fn convolve(a: &[f64], b: &[f64]) -> Vec<f64> {
219 if a.is_empty() || b.is_empty() {
220 return Vec::new();
221 }
222 let (a, b) = if a.len() < b.len() { (b, a) } else { (a, b) };
225 (0..a.len() + b.len() - 1)
226 .map(|t| {
227 let mut sum = 0.0;
228 for k in t.saturating_sub(b.len() - 1)..=(t.min(a.len() - 1)) {
229 sum += a[k] * b[t - k];
230 }
231 sum
232 })
233 .collect()
234}
235
236fn block_pair(
241 xs: &[Vec<f64>],
242 paths: &[Vec<Vec<f64>>],
243 (j, jj): (usize, usize),
244 taps: usize,
245 early_weight: f64,
246) -> (Vec<f64>, Vec<f64>) {
247 let mut total = vec![0.0; taps * taps];
248 let mut rhs = vec![0.0; if j == jj { taps } else { 0 }];
249 for (x, gs) in xs.iter().zip(paths) {
250 let n = x.len();
251 let e = (onset(x) + EARLY).min(n);
252 let c = |t: usize| if t < e { early_weight } else { 1.0 };
253 let (g, h) = (&gs[j], &gs[jj]);
254 for (k, b) in rhs.iter_mut().enumerate() {
255 for t in e.max(k)..n {
256 *b -= x[t] * g[t - k];
257 }
258 }
259 let mut block = vec![0.0; taps * taps];
260 for k in 0..taps {
261 for t in k..n {
262 block[k * taps] += c(t) * g[t - k] * h[t];
263 }
264 }
265 for l in 1..taps {
266 for t in l..n {
267 block[l] += c(t) * g[t] * h[t - l];
268 }
269 }
270 for k in 0..taps - 1 {
271 for l in 0..taps - 1 {
272 let mut v = block[k * taps + l] - c(n - 1) * g[n - 1 - k] * h[n - 1 - l];
273 if e < n && e > k.max(l) {
274 v += (1.0 - early_weight) * g[e - 1 - k] * h[e - 1 - l];
275 }
276 block[(k + 1) * taps + l + 1] = v;
277 }
278 }
279 for (sum, v) in total.iter_mut().zip(block) {
280 *sum += v;
281 }
282 }
283 (total, rhs)
284}
285
286type PairBlock = ((usize, usize), (Vec<f64>, Vec<f64>));
288
289fn block_pairs(candidates: usize) -> Vec<(usize, usize)> {
290 (0..candidates)
291 .flat_map(|j| (j..candidates).map(move |jj| (j, jj)))
292 .collect()
293}
294
295fn place(blocks: Vec<PairBlock>, ata: &mut [f64], atb: &mut [f64], taps: usize) {
298 let dim = atb.len();
299 for ((j, jj), (block, rhs)) in blocks {
300 if j == jj {
301 atb[j * taps..(j + 1) * taps].copy_from_slice(&rhs);
302 }
303 for k in 0..taps {
304 for l in 0..taps {
305 if j != jj || k <= l {
306 let (row, col) = (j * taps + k, jj * taps + l);
307 ata[row * dim + col] = block[k * taps + l];
308 ata[col * dim + row] = block[k * taps + l];
309 }
310 }
311 }
312 }
313}
314
315pub(crate) fn normal_equations(
318 xs: &[Vec<f64>],
319 paths: &[Vec<Vec<f64>>],
320 taps: usize,
321 early_weight: f64,
322) -> (Vec<f64>, Vec<f64>) {
323 let candidates = paths[0].len();
324 let dim = candidates * taps;
325 let (mut ata, mut atb) = (vec![0.0; dim * dim], vec![0.0; dim]);
326 for pair in block_pairs(candidates) {
327 let block = block_pair(xs, paths, pair, taps, early_weight);
328 place(vec![(pair, block)], &mut ata, &mut atb, taps);
329 }
330 (ata, atb)
331}
332
333pub(crate) fn normal_equations_mapped(
337 xs: &[Vec<f64>],
338 paths: &[Vec<Vec<f64>>],
339 taps: usize,
340 early_weight: f64,
341 ctx: &Context,
342) -> Result<(Vec<f64>, Vec<f64>), EngineError> {
343 check(ctx)?;
344 let candidates = paths[0].len();
345 let dim = candidates * taps;
346 let (mut ata, mut atb) = (vec![0.0; dim * dim], vec![0.0; dim]);
347 for pairs in block_pairs(candidates).chunks(8) {
350 let blocks = ctx
351 .map(pairs.to_vec(), |pair| {
352 (pair, block_pair(xs, paths, pair, taps, early_weight))
353 })
354 .map_err(|_| EngineError::Cancelled)?;
355 place(blocks, &mut ata, &mut atb, taps);
356 }
357 Ok((ata, atb))
358}
359
360#[cfg(any(test, feature = "test-support"))]
362pub(crate) fn worst_band_db(
363 xs: &[Vec<f64>],
364 paths: &[Vec<Vec<f64>>],
365 supports: &[Vec<f64>],
366 fsiso: f64,
367) -> f64 {
368 let n = xs[0].len();
369 let nf = 2 * n;
370 let bins = secs_dsp::grid::freqs(nf, FS_LOW).expect("MIMO grid");
371 let mut bands = third_octave_bands(fsiso);
372 let refs: Vec<_> = xs.iter().map(Vec::as_slice).collect();
373 bands.extend(
374 decay::late_peaks(&refs, 6)
375 .iter()
376 .map(|&f| (f - 1.5, f + 1.5)),
377 );
378 let mut before = vec![0.0; bands.len()];
379 let mut after = before.clone();
380 for (x, gs) in xs.iter().zip(paths) {
381 let mut add = vec![0.0; n];
382 for (g, s) in gs.iter().zip(supports) {
383 for (a, v) in add.iter_mut().zip(convolve(g, s)) {
384 *a += v;
385 }
386 }
387 let e = (onset(x) + EARLY).min(n);
388 for (acc, addition) in [(&mut before, false), (&mut after, true)] {
389 let mut y = vec![0.0; nf];
390 for t in e..n {
391 y[t] = x[t] + if addition { add[t] } else { 0.0 };
392 }
393 let spectrum: Vec<_> = fft::rfft(&y)
394 .iter()
395 .map(|h| {
396 let a = h.re.hypot(h.im);
397 a * a
398 })
399 .collect();
400 for (total, &(lo, hi)) in acc.iter_mut().zip(&bands) {
401 let begin = bins.partition_point(|&f| f < lo);
402 let end = bins.partition_point(|&f| f < hi);
403 *total += pairwise_sum(&spectrum[begin..end]);
404 }
405 }
406 }
407 before
408 .iter()
409 .zip(after)
410 .filter(|(a, _)| **a > 0.0)
411 .map(|(a, b)| 10.0 * ((b + 1e-30) / a).log10())
412 .reduce(f64::max)
413 .unwrap_or(-99.0)
414}
415
416pub(crate) fn third_octave_bands(fsiso: f64) -> Vec<(f64, f64)> {
420 let stop = (fsiso / 20.0).log2() + 1e-9;
421 let edges: Vec<_> = (0..)
422 .map(|k| k as f64 / 3.0)
423 .take_while(|&v| v < stop)
424 .map(|v| 20.0 * 2f64.powf(v))
425 .collect();
426 edges.windows(2).map(|v| (v[0], v[1])).collect()
427}
428
429#[derive(Debug)]
430pub(crate) struct BandGuard {
431 n: usize,
432 starts: Vec<usize>,
433 hz: Vec<(f64, f64)>,
434 bands: Vec<(usize, usize)>,
435 before: Vec<f64>,
436}
437
438impl BandGuard {
439 pub(crate) fn new(xs: &[Vec<f64>], fsiso: f64) -> Self {
440 let n = xs[0].len();
441 let bins = secs_dsp::grid::freqs(2 * n, FS_LOW).expect("MIMO grid");
442 let refs: Vec<_> = xs.iter().map(Vec::as_slice).collect();
443 let hz: Vec<_> = third_octave_bands(fsiso)
444 .into_iter()
445 .chain(
446 decay::late_peaks(&refs, 6)
447 .into_iter()
448 .map(|f| (f - 1.5, f + 1.5)),
449 )
450 .collect();
451 let bands = hz
452 .iter()
453 .map(|&(lo, hi)| {
454 (
455 bins.partition_point(|&f| f < lo),
456 bins.partition_point(|&f| f < hi),
457 )
458 })
459 .collect();
460 let mut guard = Self {
461 n,
462 starts: xs.iter().map(|x| (onset(x) + EARLY).min(n)).collect(),
463 hz,
464 bands,
465 before: Vec::new(),
466 };
467 guard.before = guard.energies(xs, true);
468 guard
469 }
470
471 fn energies(&self, responses: &[Vec<f64>], baseline: bool) -> Vec<f64> {
472 let mut totals = vec![0.0; self.bands.len()];
473 for (response, &start) in responses.iter().zip(&self.starts) {
474 let mut window = vec![0.0; 2 * self.n];
475 for t in start..self.n {
476 window[t] = if baseline {
478 response[t] + 0.0
479 } else {
480 response[t]
481 };
482 }
483 let spectrum: Vec<_> = fft::rfft(&window)
484 .iter()
485 .map(|h| {
486 let a = h.re.hypot(h.im);
487 a * a
488 })
489 .collect();
490 for (total, &(begin, end)) in totals.iter_mut().zip(&self.bands) {
491 *total += pairwise_sum(&spectrum[begin..end]);
492 }
493 }
494 totals
495 }
496
497 pub(crate) fn evaluate(&self, ys: &[Vec<f64>]) -> f64 {
498 self.evaluate_band(ys).0
499 }
500
501 pub(crate) fn evaluate_band(&self, ys: &[Vec<f64>]) -> (f64, Option<(f64, f64)>) {
503 let changes: Vec<_> = self
504 .before
505 .iter()
506 .zip(self.energies(ys, false))
507 .map(|(a, b)| (*a > 0.0).then(|| 10.0 * ((b + 1e-30) / a).log10()))
508 .collect();
509 let worst = changes
510 .iter()
511 .flatten()
512 .copied()
513 .reduce(f64::max)
514 .unwrap_or(-99.0);
515 let band = changes
516 .iter()
517 .position(|change| *change == Some(worst))
518 .map(|k| self.hz[k]);
519 (worst, band)
520 }
521}
522
523pub fn worst_t60_change(
526 xs: &[Vec<f64>],
527 ys: &[Vec<f64>],
528 count: usize,
529) -> Result<f64, EngineError> {
530 T60Guard::new(xs, count).evaluate(ys)
531}
532
533#[derive(Debug)]
534struct TailBaseline {
535 frequency: f64,
536 positions: usize,
537 sos: Sos,
538 energy: f64,
539}
540
541#[derive(Debug)]
545pub(crate) struct T60Guard<'a> {
546 xs: &'a [Vec<f64>],
547 count: usize,
548 original_modes: Vec<f64>,
549 starts: Vec<usize>,
550 baselines: Vec<TailBaseline>,
551}
552
553impl<'a> T60Guard<'a> {
554 pub(crate) fn new(xs: &'a [Vec<f64>], count: usize) -> Self {
555 Self {
556 xs,
557 count,
558 original_modes: decay::late_peaks(
559 &xs.iter().map(Vec::as_slice).collect::<Vec<_>>(),
560 count,
561 ),
562 starts: xs.iter().map(|x| onset(x) + 180).collect(),
563 baselines: Vec::new(),
564 }
565 }
566
567 pub(crate) fn evaluate(&mut self, ys: &[Vec<f64>]) -> Result<f64, EngineError> {
568 Ok(self.evaluate_mode(ys)?.0)
569 }
570
571 pub(crate) fn evaluate_mode(
573 &mut self,
574 ys: &[Vec<f64>],
575 ) -> Result<(f64, Option<f64>), EngineError> {
576 let mut modes: Vec<_> = self
577 .original_modes
578 .iter()
579 .copied()
580 .chain(decay::late_peaks(
581 &ys.iter().map(Vec::as_slice).collect::<Vec<_>>(),
582 self.count,
583 ))
584 .map(|f| (f * 10.0).round_ties_even() / 10.0)
585 .collect();
586 modes.sort_by(f64::total_cmp);
587 modes.dedup();
588 let positions = self.xs.len().min(ys.len());
589 let mut tails = Vec::new();
590 for fm in modes {
591 let index = self
592 .baselines
593 .iter()
594 .position(|entry| entry.frequency == fm && entry.positions == positions);
595 let index = if let Some(index) = index {
596 index
597 } else {
598 let sos = butter(2, &[(fm - 1.5).max(1.0), fm + 1.5], BType::Bandpass, FS_LOW)?;
599 let mut energy = 0.0;
600 for (x, &t) in self.xs.iter().zip(&self.starts).take(positions) {
601 let xf = sosfilt(&sos, x)?;
602 energy += decay::energy(&xf[t.min(xf.len())..]);
603 }
604 self.baselines.push(TailBaseline {
605 frequency: fm,
606 positions,
607 sos,
608 energy,
609 });
610 self.baselines.len() - 1
611 };
612 let baseline = &self.baselines[index];
613 let mut after = 0.0;
614 for (y, &t) in ys.iter().zip(&self.starts).take(positions) {
615 let yf = sosfilt(&baseline.sos, y)?;
616 after += decay::energy(&yf[t.min(yf.len())..]);
617 }
618 tails.push((fm, baseline.energy, after));
619 }
620 let top = tails.iter().map(|&(_, a, _)| a).fold(0.0, f64::max);
621 let mut worst: f64 = -99.0;
622 let mut mode = None;
623 for (fm, a, b) in tails {
624 if a > 0.0 && a >= top * 10f64.powf(-20.0 / 10.0) {
625 let change = 10.0 * ((b + 1e-30) / a).log10();
626 if change > worst {
627 mode = Some(fm);
628 }
629 worst = worst.max(change);
630 }
631 }
632 Ok((worst * 0.15, mode))
633 }
634}
635
636#[cfg(test)]
637pub(crate) fn worst_t60_change_before(
638 xs: &[Vec<f64>],
639 ys: &[Vec<f64>],
640 count: usize,
641) -> Result<f64, EngineError> {
642 let refs =
643 |v: &[Vec<f64>]| decay::late_peaks(&v.iter().map(Vec::as_slice).collect::<Vec<_>>(), count);
644 let mut modes: Vec<_> = refs(xs)
645 .into_iter()
646 .chain(refs(ys))
647 .map(|f| (f * 10.0).round_ties_even() / 10.0)
648 .collect();
649 modes.sort_by(f64::total_cmp);
650 modes.dedup();
651 let mut tails = Vec::new();
652 for fm in modes {
653 let sos = butter(2, &[(fm - 1.5).max(1.0), fm + 1.5], BType::Bandpass, FS_LOW)?;
654 let (mut a, mut b) = (0.0, 0.0);
655 for (x, y) in xs.iter().zip(ys) {
656 let t = onset(x) + 180;
657 let xf = sosfilt(&sos, x)?;
658 let yf = sosfilt(&sos, y)?;
659 a += decay::energy(&xf[t.min(xf.len())..]);
660 b += decay::energy(&yf[t.min(yf.len())..]);
661 }
662 tails.push((a, b));
663 }
664 let top = tails.iter().map(|&(a, _)| a).fold(0.0, f64::max);
665 let mut worst: f64 = -99.0;
666 for (a, b) in tails {
667 if a > 0.0 && a >= top * 10f64.powf(-20.0 / 10.0) {
668 worst = worst.max(10.0 * ((b + 1e-30) / a).log10());
669 }
670 }
671 Ok(worst * 0.15)
672}
673
674#[derive(Debug, Clone, PartialEq)]
675pub(crate) struct LambdaStep {
676 pub mid: f64,
677 pub ratio: f64,
678 pub accepted: bool,
679}
680#[derive(Debug, Clone, PartialEq)]
681pub(crate) struct GuardStep {
682 pub scale: f64,
683 pub band_db: f64,
684 pub t60_change: f64,
685 pub band_hz: Option<(f64, f64)>,
688 pub mode_hz: Option<f64>,
689}
690
691impl GuardStep {
692 fn passes(&self) -> bool {
693 self.band_db <= 1.0 && self.t60_change <= 0.15
694 }
695}
696
697pub(crate) fn guard_cause(guards: &[GuardStep]) -> Option<crate::notes::GuardCause> {
700 let step = guards.iter().rev().find(|step| !step.passes())?;
701 Some(crate::notes::GuardCause {
702 rejected_db: 20.0 * step.scale.max(1e-9).log10(),
703 band: (step.band_db > 1.0)
704 .then_some(step.band_hz)
705 .flatten()
706 .map(|(lo, hi)| (lo, hi, step.band_db)),
707 tail: (step.t60_change > 0.15)
708 .then_some(step.mode_hz)
709 .flatten()
710 .map(|hz| (hz, step.t60_change / 0.15)),
711 })
712}
713#[derive(Debug, Clone, PartialEq)]
714pub(crate) struct InputTrace {
715 pub candidates: Vec<String>,
716 pub ranking: Vec<(String, f64)>,
717 pub trajectory: Vec<LambdaStep>,
718 pub log_lambda: f64,
719 pub ratio: f64,
720 pub guards: Vec<GuardStep>,
721 pub guard_scale: f64,
722}
723#[derive(Debug)]
724pub(crate) struct InputResult {
725 pub supports: Vec<(String, Vec<f64>)>,
726 pub level_db: f64,
727 pub trace: InputTrace,
728}
729
730fn check(ctx: &Context) -> Result<(), EngineError> {
731 ctx.cancel().check().map_err(|_| EngineError::Cancelled)
732}
733
734pub(crate) fn rank_supports(
735 candidates: &[String],
736 xs: &[Vec<f64>],
737 paths: &[Vec<Vec<f64>>],
738 taps: usize,
739 ctx: &Context,
740) -> Result<Vec<(String, f64)>, EngineError> {
741 let late_input: Vec<_> = xs
742 .iter()
743 .flat_map(|x| {
744 let e = onset(x) + EARLY;
745 x.iter()
746 .enumerate()
747 .map(move |(t, &x)| if t >= e { x } else { 0.0 })
748 })
749 .collect();
750 let input_energy = decay::energy(&late_input);
751 let work = xs.iter().map(Vec::len).sum::<usize>() + 3 * taps;
752 let mut scored = Vec::new();
753 for (j, role) in candidates.iter().enumerate() {
754 check(ctx)?;
755 let candidate_paths: Vec<_> = paths.iter().map(|g| vec![g[j].clone()]).collect();
756 let (ata, atb) = normal_equations(xs, &candidate_paths, taps, 0.0);
757 let diagonal: Vec<_> = (0..taps).map(|k| ata[k * taps + k]).collect();
758 let lambda = 1e-2 * pairwise_sum(&diagonal) / taps as f64;
759 let u = solve(&ata, &atb, lambda, false)?;
760 let path_energy = candidate_paths.iter().map(|gs| decay::energy(&gs[0])).sum();
761 let (score, uncertainty) =
762 rank_score(input_energy, path_energy, &ata, &atb, &u, lambda, work);
763 let direct = !score.is_finite() || !uncertainty.is_finite() || score <= 8.0 * uncertainty;
766 scored.push((
767 role.clone(),
768 if direct {
769 rank_score_direct(xs, paths, j, &u)
770 } else {
771 score
772 },
773 if direct { 0.0 } else { uncertainty },
774 u,
775 ));
776 }
777 let mut direct = vec![false; scored.len()];
781 for i in 0..scored.len() {
782 for j in i + 1..scored.len() {
783 if (scored[i].1 - scored[j].1).abs() <= scored[i].2 + scored[j].2 {
784 direct[i] = true;
785 direct[j] = true;
786 }
787 }
788 }
789 let mut scores = Vec::with_capacity(scored.len());
790 for (j, ((role, score, _, u), direct)) in scored.into_iter().zip(direct).enumerate() {
791 check(ctx)?;
792 scores.push((
793 role,
794 if direct {
795 rank_score_direct(xs, paths, j, &u)
796 } else {
797 score
798 },
799 ));
800 }
801 scores.sort_by(|a, b| a.1.total_cmp(&b.1));
802 Ok(scores)
803}
804
805fn rank_score(
806 input_energy: f64,
807 path_energy: f64,
808 ata: &[f64],
809 atb: &[f64],
810 u: &[f64],
811 lambda: f64,
812 work: usize,
813) -> (f64, f64) {
814 let products: Vec<_> = u.iter().zip(atb).map(|(u, b)| u * b).collect();
815 let ub = pairwise_sum(&products);
816 let regularization = lambda * decay::energy(u);
817 let u_l1 = u.iter().map(|v| v.abs()).sum::<f64>();
818 let mut magnitude = input_energy
821 + 2.0 * products.iter().map(|v| v.abs()).sum::<f64>()
822 + regularization.abs()
823 + path_energy * u_l1 * u_l1;
824 let mut row_products = vec![0.0; u.len()];
825 let mut corrections = Vec::with_capacity(u.len());
826 for (i, row) in ata.chunks_exact(u.len()).enumerate() {
827 for ((v, a), u) in row_products.iter_mut().zip(row).zip(u) {
828 *v = a * u;
829 }
830 let residual = (pairwise_sum(&row_products) + lambda * u[i]) - atb[i];
831 corrections.push(u[i] * residual);
832 magnitude += u[i].abs() * row_products.iter().map(|v| v.abs()).sum::<f64>();
833 }
834 let score = ((input_energy - ub) - regularization) + pairwise_sum(&corrections);
839 let uncertainty = 64.0 * f64::EPSILON * work.max(1) as f64 * magnitude;
843 (score, uncertainty)
844}
845
846pub(crate) fn rank_score_direct(
847 xs: &[Vec<f64>],
848 paths: &[Vec<Vec<f64>>],
849 candidate: usize,
850 u: &[f64],
851) -> f64 {
852 let mut residual = Vec::new();
853 for (x, gs) in xs.iter().zip(paths) {
854 let y = convolve(&gs[candidate], u);
855 let e = onset(x) + EARLY;
856 residual.extend(
857 x.iter()
858 .enumerate()
859 .map(|(t, &x)| if t >= e { y[t] + x } else { 0.0 }),
860 );
861 }
862 decay::energy(&residual)
863}
864
865pub(crate) fn design_input(
868 xs: &[Vec<f64>],
869 raw_paths: &[Vec<Vec<f64>>],
870 direct: &[f64],
871 candidates: &[String],
872 req: &MimoRequest,
873 ctx: &Context,
874) -> Result<InputResult, EngineError> {
875 design_input_observed(xs, raw_paths, direct, candidates, req, ctx, &|_, _| {})
876}
877
878pub(crate) fn design_input_observed(
879 xs: &[Vec<f64>],
880 raw_paths: &[Vec<Vec<f64>>],
881 direct: &[f64],
882 candidates: &[String],
883 req: &MimoRequest,
884 ctx: &Context,
885 observe: &dyn Fn(&str, std::time::Duration),
886) -> Result<InputResult, EngineError> {
887 design_input_accelerated(xs, raw_paths, direct, candidates, req, ctx, observe, None)
888}
889
890#[allow(clippy::too_many_arguments)]
891fn design_input_accelerated(
892 xs: &[Vec<f64>],
893 raw_paths: &[Vec<Vec<f64>>],
894 direct: &[f64],
895 candidates: &[String],
896 req: &MimoRequest,
897 ctx: &Context,
898 observe: &dyn Fn(&str, std::time::Duration),
899 acceleration: Option<&super::acceleration::RunAcceleration<'_>>,
900) -> Result<InputResult, EngineError> {
901 let started = std::time::Instant::now();
902 let taps = req.taps_low;
903 let lp = lowpass_fir(req.fsiso, 31)?;
904 let n = xs[0].len();
905 let all_paths: Vec<Vec<Vec<f64>>> = raw_paths
906 .iter()
907 .map(|gs| {
908 gs.iter()
909 .map(|g| {
910 let mut h = convolve(g, &lp);
911 h.truncate(n);
912 h
913 })
914 .collect()
915 })
916 .collect();
917 observe("path_building", started.elapsed());
918 let started = std::time::Instant::now();
919 let ranking = if req
920 .max_supports
921 .is_some_and(|cap| cap > 0 && candidates.len() > cap)
922 {
923 rank_supports(candidates, xs, &all_paths, taps, ctx)?
924 } else {
925 Vec::new()
926 };
927 observe("ranking", started.elapsed());
928 let selected: Vec<_> = if ranking.is_empty() {
929 candidates.to_vec()
930 } else {
931 ranking
932 .iter()
933 .take(req.max_supports.unwrap())
934 .map(|(r, _)| r.clone())
935 .collect()
936 };
937 let indices: Vec<_> = selected
938 .iter()
939 .map(|r| candidates.iter().position(|v| v == r).unwrap())
940 .collect();
941 let select = |paths: &[Vec<Vec<f64>>]| {
942 paths
943 .iter()
944 .map(|gs| indices.iter().map(|&j| gs[j].clone()).collect())
945 .collect::<Vec<_>>()
946 };
947 design_selected(
948 xs,
949 &select(raw_paths),
950 direct,
951 &select(&all_paths),
952 &selected,
953 &ranking,
954 req,
955 ctx,
956 observe,
957 acceleration,
958 )
959}
960
961#[allow(clippy::too_many_arguments)]
962fn design_selected(
963 xs: &[Vec<f64>],
964 raw_paths: &[Vec<Vec<f64>>],
965 direct: &[f64],
966 paths: &[Vec<Vec<f64>>],
967 candidates: &[String],
968 ranking: &[(String, f64)],
969 req: &MimoRequest,
970 ctx: &Context,
971 observe: &dyn Fn(&str, std::time::Duration),
972 acceleration: Option<&super::acceleration::RunAcceleration<'_>>,
973) -> Result<InputResult, EngineError> {
974 let taps = req.taps_low;
975 let lp = lowpass_fir(req.fsiso, 31)?;
976 let n = xs[0].len();
977 let started = std::time::Instant::now();
978 let (ata, atb) = normal_equations_mapped(xs, paths, taps, 10.0, ctx)?;
979 observe("normal_equations", started.elapsed());
980 let started = std::time::Instant::now();
981 let dim = atb.len();
982 if dim == 0 {
983 return Err(EngineError::Input("no MIMO support candidates".into()));
984 }
985 let diagonal: Vec<_> = (0..dim).map(|k| ata[k * dim + k]).collect();
986 let normalization = pairwise_sum(&diagonal) / dim as f64;
987 let target = 10f64.powf(req.level_db / 10.0);
988 let direct_energy = decay::energy(direct).max(1e-30);
989 let result = super::acceleration::run_sweep(
990 super::acceleration::RidgeProblem {
991 ata: &ata,
992 atb: &atb,
993 lp: &lp,
994 taps,
995 normalization,
996 target,
997 direct_energy,
998 },
999 ctx.cancel(),
1000 acceleration,
1001 )?;
1002 let (u, ratio, hi, trajectory) = (result.u, result.ratio, result.log_lambda, result.trajectory);
1003 observe("sweep", started.elapsed());
1004 let started = std::time::Instant::now();
1005 let supports: Vec<_> = (0..candidates.len())
1006 .map(|k| convolve(&lp, &u[k * taps..(k + 1) * taps]))
1007 .collect();
1008 let mut scale = 1.0;
1009 let mut guards = Vec::new();
1010 let band_guard = BandGuard::new(xs, req.fsiso);
1011 let mut tail_guard = T60Guard::new(xs, 6);
1012 for step in 0..5 {
1013 check(ctx)?;
1014 let trial: Vec<Vec<_>> = supports
1015 .iter()
1016 .map(|s| s.iter().map(|v| v * scale).collect())
1017 .collect();
1018 let ys: Vec<_> = xs
1019 .iter()
1020 .zip(raw_paths)
1021 .map(|(x, gs)| {
1022 let mut add = vec![0.0; n];
1023 for (g, s) in gs.iter().zip(&trial) {
1024 for (v, h) in add.iter_mut().zip(convolve(g, s)) {
1025 *v += h;
1026 }
1027 }
1028 x.iter().zip(add).map(|(x, a)| x + a).collect()
1029 })
1030 .collect();
1031 let (band_db, band_hz) = band_guard.evaluate_band(&ys);
1032 let (t60_change, mode_hz) = tail_guard.evaluate_mode(&ys)?;
1033 let guard = GuardStep {
1034 scale,
1035 band_db,
1036 t60_change,
1037 band_hz,
1038 mode_hz,
1039 };
1040 let passes = guard.passes();
1041 guards.push(guard);
1042 if passes {
1043 break;
1044 }
1045 scale = if step == 4 {
1046 0.0
1047 } else {
1048 scale * 10f64.powf(-3.0 / 20.0)
1049 };
1050 }
1051 observe("guard_steps", started.elapsed());
1052 Ok(InputResult {
1053 supports: if scale > 0.0 {
1054 candidates
1055 .iter()
1056 .cloned()
1057 .zip(
1058 supports
1059 .into_iter()
1060 .map(|s| s.into_iter().map(|v| v * scale).collect()),
1061 )
1062 .collect()
1063 } else {
1064 Vec::new()
1065 },
1066 level_db: 10.0 * (ratio * scale * scale).max(1e-30).log10(),
1067 trace: InputTrace {
1068 candidates: candidates.to_vec(),
1069 ranking: ranking.to_vec(),
1070 trajectory,
1071 log_lambda: hi,
1072 ratio,
1073 guards,
1074 guard_scale: scale,
1075 },
1076 })
1077}
1078
1079pub fn design_mimo(
1080 design: &Design,
1081 measurements: &MeasurementSet,
1082 config: &EngineConfig,
1083 req: &MimoRequest,
1084 ctx: &Context,
1085) -> Result<MimoDesign, EngineError> {
1086 design_mimo_traced(design, measurements, config, req, ctx).map(|(out, _)| out)
1087}
1088
1089pub(crate) fn design_mimo_traced(
1090 design: &Design,
1091 measurements: &MeasurementSet,
1092 config: &EngineConfig,
1093 req: &MimoRequest,
1094 ctx: &Context,
1095) -> Result<(MimoDesign, Vec<(String, InputTrace)>), EngineError> {
1096 design_mimo_inputs(design, measurements, config, req, ctx, None)
1097}
1098
1099pub(crate) fn design_mimo_inputs(
1100 design: &Design,
1101 measurements: &MeasurementSet,
1102 config: &EngineConfig,
1103 req: &MimoRequest,
1104 ctx: &Context,
1105 input: Option<&str>,
1106) -> Result<(MimoDesign, Vec<(String, InputTrace)>), EngineError> {
1107 design_mimo_inputs_observed(design, measurements, config, req, ctx, input, &|_, _| {})
1108}
1109
1110#[allow(clippy::too_many_arguments)]
1111pub(crate) fn design_mimo_inputs_observed(
1112 design: &Design,
1113 measurements: &MeasurementSet,
1114 config: &EngineConfig,
1115 req: &MimoRequest,
1116 ctx: &Context,
1117 input: Option<&str>,
1118 observe: &(dyn Fn(&str, std::time::Duration) + Sync),
1119) -> Result<(MimoDesign, Vec<(String, InputTrace)>), EngineError> {
1120 check(ctx)?;
1121 let mut out = MimoDesign::default();
1122 let positions = &design.spectra.positions;
1123 if positions.len() < 3 {
1124 out.notes
1125 .push(Note::Mimo(crate::notes::MimoNote::InsufficientPositions {
1126 measured: positions.len(),
1127 }));
1128 return Ok((out, Vec::new()));
1129 }
1130 validate_request(req)?;
1131 let started = std::time::Instant::now();
1132 let prepared = super::prepared::Prepared::new(design, measurements, config, ctx)?;
1133 observe("preparation", started.elapsed());
1134 design_mimo_prepared(design, &prepared, req, ctx, input, observe)
1135}
1136
1137#[allow(clippy::too_many_arguments)]
1138pub(crate) fn design_mimo_prepared(
1139 design: &Design,
1140 prepared: &super::prepared::Prepared<'_>,
1141 req: &MimoRequest,
1142 ctx: &Context,
1143 input: Option<&str>,
1144 observe: &(dyn Fn(&str, std::time::Duration) + Sync),
1145) -> Result<(MimoDesign, Vec<(String, InputTrace)>), EngineError> {
1146 design_mimo_prepared_accelerated(design, prepared, req, ctx, input, observe, None)
1147}
1148
1149#[allow(clippy::too_many_arguments)]
1150pub(crate) fn design_mimo_prepared_accelerated(
1151 design: &Design,
1152 prepared: &super::prepared::Prepared<'_>,
1153 req: &MimoRequest,
1154 ctx: &Context,
1155 input: Option<&str>,
1156 observe: &(dyn Fn(&str, std::time::Duration) + Sync),
1157 acceleration: Option<&super::acceleration::RunAcceleration<'_>>,
1158) -> Result<(MimoDesign, Vec<(String, InputTrace)>), EngineError> {
1159 check(ctx)?;
1160 let mut out = MimoDesign::default();
1161 let positions = &design.spectra.positions;
1162 if positions.len() < 3 {
1163 out.notes
1164 .push(Note::Mimo(crate::notes::MimoNote::InsufficientPositions {
1165 measured: positions.len(),
1166 }));
1167 return Ok((out, Vec::new()));
1168 }
1169 validate_request(req)?;
1170 let spectra = &prepared.spectra;
1171 let started = std::time::Instant::now();
1172 out.ranges = support_ranges(design, spectra, req.fsiso, positions)?;
1173 if let Some(low) = req.f_low.filter(|v| *v != 0.0) {
1174 for (_, range) in &mut out.ranges {
1175 *range =
1176 range.and_then(|(lo, hi)| (lo.max(low) < hi * 0.8).then_some((lo.max(low), hi)));
1177 }
1178 }
1179 let usable: Vec<_> = out
1180 .ranges
1181 .iter()
1182 .filter(|(r, range)| {
1183 range.is_some()
1184 && !req.exclude.contains(r)
1185 && req.groups.as_ref().is_none_or(|groups| {
1186 design
1187 .groups
1188 .iter()
1189 .any(|(g, roles)| groups.contains(g) && roles.contains(r))
1190 })
1191 })
1192 .map(|(r, _)| r.clone())
1193 .collect();
1194 if !req.exclude.is_empty() {
1195 let mut excluded = req.exclude.clone();
1196 excluded.sort();
1197 out.notes
1198 .push(Note::Mimo(crate::notes::MimoNote::ExcludedSupports(
1199 excluded,
1200 )));
1201 }
1202 out.notes
1203 .push(Note::Mimo(crate::notes::MimoNote::SupportRanges(
1204 out.ranges.clone(),
1205 )));
1206 observe("ranges", started.elapsed());
1207 let started = std::time::Instant::now();
1208 let mains: Vec<_> = design
1209 .mains
1210 .iter()
1211 .filter(|m| input.is_none_or(|r| r == m.role))
1212 .collect();
1213 let roles: Vec<_> = usable
1214 .iter()
1215 .filter(|r| mains.iter().any(|m| m.role != **r))
1216 .collect();
1217 let pairs: Vec<_> = positions
1218 .iter()
1219 .flat_map(|&p| roles.iter().map(move |r| (p, *r)))
1220 .collect();
1221 let shared_bytes = (pairs.len()
1222 * (720 * 8 + std::mem::size_of::<Vec<f64>>() + 2 * std::mem::size_of::<(u32, &String)>()))
1223 as u64;
1224 let scratch_bytes = (32 * design_freqs().len() * 8) as u64;
1225 let largest_input = mains
1226 .iter()
1227 .map(|m| {
1228 input_bytes(
1229 positions.len(),
1230 usable.iter().filter(|r| **r != m.role).count(),
1231 req.taps_low,
1232 ) as u64
1233 })
1234 .max()
1235 .unwrap_or(0);
1236 let retained = prepared.reserved.saturating_add(shared_bytes);
1240 let needed = retained.saturating_add(largest_input.max(scratch_bytes));
1241 if needed > ctx.budget().total() {
1242 return Err(EngineError::Input(
1243 MemoryError::TooLarge {
1244 requested: needed,
1245 total: ctx.budget().total(),
1246 }
1247 .to_string(),
1248 ));
1249 }
1250 let _shared_permit = ctx
1251 .budget()
1252 .acquire(shared_bytes, ctx.cancel())
1253 .map_err(memory_error)?;
1254 let low_paths = crate::memory::try_map(
1255 ctx,
1256 pairs.clone(),
1257 retained,
1258 |_| scratch_bytes,
1259 |(p, r)| {
1260 let h = support_path(&out, r, spectra, p, &prepared.align_delay_ms)?;
1261 Ok::<_, EngineError>(secs_dsp::lowrate::to_low(&h, design_freqs(), 720)?)
1262 },
1263 )?;
1264 observe("shared_paths", started.elapsed());
1265 let started = std::time::Instant::now();
1266 let results = crate::memory::try_map(
1267 ctx,
1268 mains,
1269 retained,
1270 |main| {
1271 input_bytes(
1272 positions.len(),
1273 usable.iter().filter(|r| **r != main.role).count(),
1274 req.taps_low,
1275 ) as u64
1276 },
1277 |main| {
1278 let candidates: Vec<_> = usable
1279 .iter()
1280 .filter(|r| **r != main.role)
1281 .cloned()
1282 .collect();
1283 let started = std::time::Instant::now();
1284 let mut xs = Vec::new();
1285 let mut paths = Vec::new();
1286 for &p in positions {
1287 check(ctx)?;
1288 let response =
1289 crate::input_response(design, &main.role, spectra, p).ok_or_else(|| {
1290 EngineError::Input(format!("missing MIMO input {p}/{}", main.role))
1291 })?;
1292 xs.push(secs_dsp::lowrate::to_low(&response, design_freqs(), 720)?);
1293 paths.push(
1294 candidates
1295 .iter()
1296 .map(|r| {
1297 let index = pairs
1298 .iter()
1299 .position(|(pos, role)| *pos == p && *role == r)
1300 .unwrap();
1301 low_paths[index].clone()
1302 })
1303 .collect(),
1304 );
1305 }
1306 let direct = secs_dsp::lowrate::to_low(&main.response, design_freqs(), 720)?;
1307 observe("input_preparation", started.elapsed());
1308 let result = design_input_accelerated(
1309 &xs,
1310 &paths,
1311 &direct,
1312 &candidates,
1313 req,
1314 ctx,
1315 observe,
1316 acceleration,
1317 )?;
1318 Ok((main.role.clone(), result))
1319 },
1320 )?;
1321 observe("inputs_wall", started.elapsed());
1322 let mut traces = Vec::new();
1323 for (role, result) in results {
1324 let scale = result.trace.guard_scale;
1325 if let Some(cause) = guard_cause(&result.trace.guards) {
1326 out.notes.push(Note::Mimo(if scale > 0.0 {
1327 crate::notes::MimoNote::GuardReduced {
1328 role: role.clone(),
1329 gain_db: 20.0 * scale.max(1e-9).log10(),
1330 cause,
1331 }
1332 } else {
1333 crate::notes::MimoNote::GuardDisabled {
1334 role: role.clone(),
1335 cause,
1336 }
1337 }));
1338 }
1339 out.supports.push((role.clone(), result.supports));
1340 out.level_db.push((role.clone(), result.level_db));
1341 traces.push((role, result.trace));
1342 }
1343 Ok((out, traces))
1344}
1345
1346fn validate_request(req: &MimoRequest) -> Result<(), EngineError> {
1347 if !(0.0..300.0).contains(&req.fsiso)
1348 || req.fsiso == 0.0
1349 || !req.level_db.is_finite()
1350 || req.taps_low == 0
1351 || req.taps_low > 720
1352 || req.f_low.is_some_and(|f| !f.is_finite())
1353 {
1354 return Err(EngineError::Input("invalid MIMO request".into()));
1355 }
1356 Ok(())
1357}
1358
1359fn memory_error(error: MemoryError) -> EngineError {
1360 match error {
1361 MemoryError::Cancelled => EngineError::Cancelled,
1362 other => EngineError::Input(other.to_string()),
1363 }
1364}
1365
1366fn input_bytes(positions: usize, candidates: usize, taps: usize) -> usize {
1367 let dim = candidates * taps;
1368 8 * (4 * dim * dim
1369 + taps * taps
1370 + positions * 720 * (4 * candidates + 8)
1371 + 32 * design_freqs().len()
1372 + 16 * dim
1373 + 16384)
1374}
1375
1376#[cfg(test)]
1378pub(crate) fn design_mimo_before(
1379 design: &Design,
1380 measurements: &MeasurementSet,
1381 config: &EngineConfig,
1382 req: &MimoRequest,
1383 ctx: &Context,
1384 input: Option<&str>,
1385) -> Result<(MimoDesign, Vec<(String, InputTrace)>), EngineError> {
1386 check(ctx)?;
1387 let mut out = MimoDesign::default();
1388 let positions = &design.spectra.positions;
1389 if positions.len() < 3 {
1390 out.notes
1391 .push(Note::Mimo(crate::notes::MimoNote::InsufficientPositions {
1392 measured: positions.len(),
1393 }));
1394 return Ok((out, Vec::new()));
1395 }
1396 if !(0.0..300.0).contains(&req.fsiso)
1397 || req.fsiso == 0.0
1398 || !req.level_db.is_finite()
1399 || req.taps_low == 0
1400 || req.taps_low > 720
1401 || req.f_low.is_some_and(|f| !f.is_finite())
1402 {
1403 return Err(EngineError::Input("invalid MIMO request".into()));
1404 }
1405 let prepared =
1406 crate::measurement::prepare(measurements, config, &crate::setup::Setup::new(config))?;
1407 let spectra =
1408 crate::export_view::export_view_prepared(design, measurements, &prepared, &|| false)?
1409 .spectra;
1410 out.ranges = support_ranges(design, &spectra, req.fsiso, positions)?;
1411 if let Some(low) = req.f_low.filter(|v| *v != 0.0) {
1412 for (_, range) in &mut out.ranges {
1413 *range =
1414 range.and_then(|(lo, hi)| (lo.max(low) < hi * 0.8).then_some((lo.max(low), hi)));
1415 }
1416 }
1417 let usable: Vec<_> = out
1418 .ranges
1419 .iter()
1420 .filter(|(r, range)| {
1421 range.is_some()
1422 && !req.exclude.contains(r)
1423 && req.groups.as_ref().is_none_or(|groups| {
1424 design
1425 .groups
1426 .iter()
1427 .any(|(g, roles)| groups.contains(g) && roles.contains(r))
1428 })
1429 })
1430 .map(|(r, _)| r.clone())
1431 .collect();
1432 if !req.exclude.is_empty() {
1433 let mut excluded = req.exclude.clone();
1434 excluded.sort();
1435 out.notes
1436 .push(Note::Mimo(crate::notes::MimoNote::ExcludedSupports(
1437 excluded,
1438 )));
1439 }
1440 out.notes
1441 .push(Note::Mimo(crate::notes::MimoNote::SupportRanges(
1442 out.ranges.clone(),
1443 )));
1444 let results = ctx
1445 .try_map(
1446 design
1447 .mains
1448 .iter()
1449 .filter(|m| input.is_none_or(|r| r == m.role))
1450 .collect(),
1451 |main| {
1452 let candidates: Vec<_> = usable
1453 .iter()
1454 .filter(|r| **r != main.role)
1455 .cloned()
1456 .collect();
1457 let dim = candidates.len() * req.taps_low;
1458 let bytes = 8
1462 * (4 * dim * dim
1463 + req.taps_low * req.taps_low
1464 + positions.len() * 720 * (4 * candidates.len() + 8)
1465 + 32 * design_freqs().len()
1466 + 16 * dim
1467 + 16384);
1468 let _permit =
1469 ctx.budget()
1470 .acquire(bytes as u64, ctx.cancel())
1471 .map_err(|e| match e {
1472 MemoryError::Cancelled => EngineError::Cancelled,
1473 other => EngineError::Input(other.to_string()),
1474 })?;
1475 let mut xs = Vec::new();
1476 let mut paths = Vec::new();
1477 for &p in positions {
1478 check(ctx)?;
1479 let response = crate::input_response(design, &main.role, &spectra, p)
1480 .ok_or_else(|| {
1481 EngineError::Input(format!("missing MIMO input {p}/{}", main.role))
1482 })?;
1483 xs.push(secs_dsp::lowrate::to_low(&response, design_freqs(), 720)?);
1484 let gs: Result<Vec<_>, EngineError> = candidates
1485 .iter()
1486 .map(|r| {
1487 let h = support_path(&out, r, &spectra, p, &prepared.align_delay_ms)?;
1488 Ok(secs_dsp::lowrate::to_low(&h, design_freqs(), 720)?)
1489 })
1490 .collect();
1491 paths.push(gs?);
1492 }
1493 let direct = secs_dsp::lowrate::to_low(&main.response, design_freqs(), 720)?;
1494 let result = design_input(&xs, &paths, &direct, &candidates, req, ctx)?;
1495 Ok((main.role.clone(), result))
1496 },
1497 )
1498 .map_err(|e| match e {
1499 MapError::Cancelled => EngineError::Cancelled,
1500 MapError::Item { error, .. } => error,
1501 })?;
1502 let mut traces = Vec::new();
1503 for (role, result) in results {
1504 let scale = result.trace.guard_scale;
1505 if let Some(cause) = guard_cause(&result.trace.guards) {
1506 out.notes.push(Note::Mimo(if scale > 0.0 {
1507 crate::notes::MimoNote::GuardReduced {
1508 role: role.clone(),
1509 gain_db: 20.0 * scale.max(1e-9).log10(),
1510 cause,
1511 }
1512 } else {
1513 crate::notes::MimoNote::GuardDisabled {
1514 role: role.clone(),
1515 cause,
1516 }
1517 }));
1518 }
1519 out.supports.push((role.clone(), result.supports));
1520 out.level_db.push((role.clone(), result.level_db));
1521 traces.push((role, result.trace));
1522 }
1523 Ok((out, traces))
1524}