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