secs_project/
signature.rs1use crate::{Project, PyJson};
2use secs_types::role::{RoleKind, role_kind};
3use std::cmp::Ordering;
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
6pub struct MeasurementSignatureEntry {
7 pub pos: String,
8 pub roles: Vec<String>,
9 pub t0: Vec<(String, String)>,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
13pub struct FloatKey(pub f64);
14
15pub fn measured_positions(project: &Project) -> Vec<i64> {
16 let mut out = project
17 .measurements
18 .array()
19 .unwrap_or(&[])
20 .iter()
21 .filter_map(|record| {
22 let ir = record.get("ir")?.object()?;
23 if ir.is_empty() {
24 return None;
25 }
26 crate::project::integer(record.get("pos"), 0, "measurement.pos").ok()
27 })
28 .collect::<Vec<_>>();
29 out.sort_unstable();
30 out.dedup();
31 out
32}
33
34pub fn measurement_signature(project: &Project) -> Vec<MeasurementSignatureEntry> {
35 let mut out = project
36 .measurements
37 .array()
38 .unwrap_or(&[])
39 .iter()
40 .filter_map(|record| {
41 let object = record.object()?;
42 let pos = object
43 .iter()
44 .find(|(key, _)| key == "pos")
45 .map(|(_, value)| python_str(value))
46 .unwrap_or_else(|| "None".into());
47 let mut roles = record
48 .get("ir")
49 .and_then(PyJson::object)
50 .map_or_else(Vec::new, |value| {
51 value.iter().map(|(role, _)| role.clone()).collect()
52 });
53 roles.sort();
54 let mut t0 = record
55 .get("t0")
56 .and_then(PyJson::object)
57 .map_or_else(Vec::new, |value| {
58 value
59 .iter()
60 .map(|(role, value)| (role.clone(), python_repr(value)))
61 .collect()
62 });
63 t0.sort();
64 Some(MeasurementSignatureEntry { pos, roles, t0 })
65 })
66 .collect::<Vec<_>>();
67 out.sort();
68 out
69}
70
71pub fn project_spl_offset(project: &Project) -> Option<f64> {
73 let mut values: Vec<_> = project
74 .measurements
75 .array()
76 .unwrap_or(&[])
77 .iter()
78 .filter_map(PyJson::object)
79 .filter_map(|record| {
80 record
81 .iter()
82 .find(|(key, _)| key == "spl_offset")
83 .and_then(|(_, value)| value.object())
84 })
85 .flat_map(|offsets| {
86 offsets.iter().filter_map(|(_, value)| match value {
87 PyJson::Float(value) => Some(*value),
88 PyJson::Int(value) => value.parse().ok(),
89 _ => None,
90 })
91 })
92 .collect();
93 if values.is_empty() {
94 return None;
95 }
96 values.sort_by(f64::total_cmp);
97 let middle = values.len() / 2;
98 Some(if values.len().is_multiple_of(2) {
99 (values[middle - 1] + values[middle]) / 2.0
100 } else {
101 values[middle]
102 })
103}
104
105pub fn target_key(project: &Project) -> Vec<(FloatKey, FloatKey)> {
106 let mut points = project.target.points.clone();
107 points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
108 points
109 .into_iter()
110 .map(|(frequency, level)| (FloatKey(frequency), FloatKey(level)))
111 .collect()
112}
113
114pub fn estimate_seconds(project: &Project, mode: &str) -> f64 {
115 let mains = project
116 .config
117 .speakers
118 .iter()
119 .filter(|speaker| role_kind(&speaker.role) != RoleKind::Sub)
120 .count();
121 let subs = project.config.speakers.len() - mains;
122 let positions = measured_positions(project).len().max(1);
123 let per = if mode == "zero" { 4.1 } else { 6.5 } * (0.4 + 0.6 * positions.min(9) as f64 / 9.0);
124 (per * mains.max(1) as f64 + if subs > 1 { 10.0 } else { 0.0 }).max(5.0)
125}
126
127pub fn estimate_subs_seconds(project: &Project) -> f64 {
128 let subs = project
129 .config
130 .speakers
131 .iter()
132 .filter(|speaker| role_kind(&speaker.role) == RoleKind::Sub)
133 .count();
134 let positions = measured_positions(project).len().max(1);
135 (18.0 * (0.4 + 0.6 * positions.min(9) as f64 / 9.0) * if subs > 1 { 1.0 } else { 0.3 }).max(3.0)
136}
137
138pub fn estimate_xo_seconds(project: &Project, mode: &str, candidates: usize) -> f64 {
139 let cpu_count = std::thread::available_parallelism().map_or(2, std::num::NonZero::get);
140 estimate_xo_seconds_on(project, mode, candidates, cpu_count)
141}
142
143pub fn estimate_xo_seconds_on(
146 project: &Project,
147 mode: &str,
148 candidates: usize,
149 cpu_count: usize,
150) -> f64 {
151 let workers = candidates.min(cpu_count / 2).max(1);
152 let rounds = candidates.div_ceil(workers);
153 estimate_seconds(project, mode) * rounds as f64 * if rounds < candidates { 1.1 } else { 1.0 }
154}
155
156fn python_str(value: &PyJson) -> String {
157 match value {
158 PyJson::Null => "None".into(),
159 PyJson::Bool(true) => "True".into(),
160 PyJson::Bool(false) => "False".into(),
161 PyJson::Int(value) | PyJson::Str(value) => value.clone(),
162 PyJson::Float(value) => python_float(*value),
163 PyJson::Array(_) | PyJson::Object(_) => crate::dumps(value),
164 }
165}
166
167fn python_repr(value: &PyJson) -> String {
168 match value {
169 PyJson::Null => "None".into(),
170 PyJson::Bool(true) => "True".into(),
171 PyJson::Bool(false) => "False".into(),
172 PyJson::Int(value) => value.clone(),
173 PyJson::Float(value) => python_float(*value),
174 PyJson::Str(value) => format!("'{value}'"),
175 PyJson::Array(_) | PyJson::Object(_) => crate::dumps(value),
176 }
177}
178
179fn python_float(value: f64) -> String {
180 if value.is_nan() {
181 "nan".into()
182 } else if value == f64::INFINITY {
183 "inf".into()
184 } else if value == f64::NEG_INFINITY {
185 "-inf".into()
186 } else {
187 let mut text = format!("{value:?}");
188 if let Some((mantissa, exponent)) = text.split_once('e') {
189 let sign = exponent.starts_with(['+', '-']);
190 let digits = exponent.trim_start_matches(['+', '-']);
191 if digits.len() == 1 {
192 text = format!(
193 "{mantissa}e{}0{digits}",
194 if sign { &exponent[..1] } else { "+" }
195 );
196 }
197 }
198 text
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn project(values: &[f64]) -> Project {
207 Project {
208 measurements: PyJson::Array(
209 values
210 .iter()
211 .enumerate()
212 .map(|(index, value)| {
213 PyJson::Object(vec![(
214 "spl_offset".into(),
215 PyJson::Object(vec![(format!("R{index}"), PyJson::Float(*value))]),
216 )])
217 })
218 .collect(),
219 ),
220 ..Project::default()
221 }
222 }
223
224 #[test]
225 fn project_spl_offset_matches_numpy_median() {
226 assert_eq!(project_spl_offset(&project(&[4.0])), Some(4.0));
227 assert_eq!(project_spl_offset(&project(&[8.0, 2.0])), Some(5.0));
228 assert_eq!(project_spl_offset(&project(&[8.0, 2.0, 4.0])), Some(4.0));
229 assert_eq!(project_spl_offset(&project(&[])), None);
230 }
231}