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