sim_lib_interference_solve/
multitone.rs1use std::f64::consts::TAU;
10
11use sim_lib_interference_core::{Hertz, InterferenceProblem, SamplingCertificate, SamplingPlane};
12
13use crate::{
14 HostPhasorField, MultiToneError, ReferencePhasorSolver, SolveEvidence, complex::CompensatedSum,
15};
16
17#[derive(Clone, Copy, Debug, PartialEq)]
19pub enum ToneCombination {
20 IncoherentMagnitudeSquared,
25 Instant {
27 seconds: f64,
29 },
30}
31
32#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct MultiToneSamplingRequirements {
35 highest_frequency: Hertz,
36 spatial_certificate: SamplingCertificate,
37 minimum_temporal_samples_per_second: f64,
38 maximum_temporal_step_seconds: f64,
39}
40
41impl MultiToneSamplingRequirements {
42 pub fn highest_frequency(self) -> Hertz {
44 self.highest_frequency
45 }
46
47 pub fn spatial_certificate(self) -> SamplingCertificate {
52 self.spatial_certificate
53 }
54
55 pub fn minimum_temporal_samples_per_second(self) -> f64 {
57 self.minimum_temporal_samples_per_second
58 }
59
60 pub fn maximum_temporal_step_seconds(self) -> f64 {
62 self.maximum_temporal_step_seconds
63 }
64}
65
66#[derive(Clone, Debug, PartialEq)]
68pub struct ToneCertificate {
69 frequency: Hertz,
70 weight: f64,
71 solve_evidence: SolveEvidence,
72}
73
74impl ToneCertificate {
75 fn from_study(study: &ToneStudy) -> Self {
76 Self {
77 frequency: study.frequency(),
78 weight: study.weight(),
79 solve_evidence: study.evidence().clone(),
80 }
81 }
82
83 pub fn frequency(&self) -> Hertz {
85 self.frequency
86 }
87
88 pub fn weight(&self) -> f64 {
90 self.weight
91 }
92
93 pub fn solve_evidence(&self) -> &SolveEvidence {
95 &self.solve_evidence
96 }
97
98 pub fn sampling_certificate(&self) -> SamplingCertificate {
100 self.solve_evidence.preflight().sampling_certificate
101 }
102}
103
104#[derive(Clone, Debug, PartialEq)]
106pub struct MultiToneCertificate {
107 plane: SamplingPlane,
108 combination: ToneCombination,
109 sampling_requirements: MultiToneSamplingRequirements,
110 components: Vec<ToneCertificate>,
111}
112
113impl MultiToneCertificate {
114 pub fn plane(&self) -> SamplingPlane {
116 self.plane
117 }
118
119 pub fn combination(&self) -> ToneCombination {
121 self.combination
122 }
123
124 pub fn sampling_requirements(&self) -> MultiToneSamplingRequirements {
126 self.sampling_requirements
127 }
128
129 pub fn components(&self) -> &[ToneCertificate] {
131 &self.components
132 }
133}
134
135#[derive(Clone, Debug, PartialEq)]
137pub struct MultiToneProjection {
138 rows: usize,
139 columns: usize,
140 samples: Vec<f64>,
141 certificate: MultiToneCertificate,
142}
143
144impl MultiToneProjection {
145 pub fn rows(&self) -> usize {
147 self.rows
148 }
149
150 pub fn columns(&self) -> usize {
152 self.columns
153 }
154
155 pub fn len(&self) -> usize {
157 self.samples.len()
158 }
159
160 pub fn is_empty(&self) -> bool {
165 self.samples.is_empty()
166 }
167
168 pub fn samples(&self) -> &[f64] {
170 &self.samples
171 }
172
173 pub fn cell(&self, row: usize, column: usize) -> Option<f64> {
175 let index = row.checked_mul(self.columns)?.checked_add(column)?;
176 (row < self.rows && column < self.columns).then(|| self.samples[index])
177 }
178
179 pub fn combination(&self) -> ToneCombination {
181 self.certificate.combination()
182 }
183
184 pub fn certificate(&self) -> &MultiToneCertificate {
186 &self.certificate
187 }
188}
189
190#[derive(Clone, Debug, PartialEq)]
192pub struct ToneStudy {
193 problem: InterferenceProblem,
194 plane: SamplingPlane,
195 weight: f64,
196 field: HostPhasorField,
197 evidence: SolveEvidence,
198}
199
200impl ToneStudy {
201 pub fn solve(
207 problem: InterferenceProblem,
208 plane: SamplingPlane,
209 weight: f64,
210 solver: ReferencePhasorSolver,
211 ) -> Result<Self, MultiToneError> {
212 if !weight.is_finite() || weight <= 0.0 {
213 return Err(MultiToneError::InvalidWeight {
214 frequency_hz: problem.frequency.get(),
215 weight,
216 });
217 }
218 let (field, evidence) =
219 solver
220 .solve(&problem, &plane)
221 .map_err(|cause| MultiToneError::ComponentSolve {
222 frequency_hz: problem.frequency.get(),
223 cause: Box::new(cause),
224 })?;
225 Ok(Self {
226 problem,
227 plane,
228 weight,
229 field,
230 evidence,
231 })
232 }
233
234 pub fn problem(&self) -> &InterferenceProblem {
236 &self.problem
237 }
238
239 pub fn frequency(&self) -> Hertz {
241 self.problem.frequency
242 }
243
244 pub fn weight(&self) -> f64 {
246 self.weight
247 }
248
249 pub fn plane(&self) -> SamplingPlane {
251 self.plane
252 }
253
254 pub fn field(&self) -> &HostPhasorField {
256 &self.field
257 }
258
259 pub fn evidence(&self) -> &SolveEvidence {
261 &self.evidence
262 }
263
264 pub fn sampling_certificate(&self) -> SamplingCertificate {
266 self.evidence.preflight().sampling_certificate
267 }
268}
269
270#[derive(Clone, Debug, PartialEq)]
272pub struct MultiToneStudy {
273 tones: Vec<ToneStudy>,
274 plane: SamplingPlane,
275 sampling_requirements: MultiToneSamplingRequirements,
276}
277
278impl MultiToneStudy {
279 pub fn new(mut tones: Vec<ToneStudy>) -> Result<Self, MultiToneError> {
285 let Some(first) = tones.first() else {
286 return Err(MultiToneError::EmptyStudy);
287 };
288 let plane = first.plane();
289 tones.sort_by(|left, right| left.frequency().get().total_cmp(&right.frequency().get()));
290 for pair in tones.windows(2) {
291 if pair[0].frequency() == pair[1].frequency() {
292 return Err(MultiToneError::DuplicateFrequency {
293 frequency_hz: pair[0].frequency().get(),
294 });
295 }
296 }
297 for tone in &tones {
298 if tone.plane() != plane {
299 return Err(MultiToneError::MismatchedPlane {
300 frequency_hz: tone.frequency().get(),
301 expected: Box::new(plane),
302 actual: Box::new(tone.plane()),
303 });
304 }
305 }
306 let highest = tones.last().expect("non-empty study checked above");
307 let minimum_temporal_samples_per_second = 2.0 * highest.frequency().get();
308 if !minimum_temporal_samples_per_second.is_finite() {
309 return Err(MultiToneError::NonFiniteTemporalSamplingRequirement {
310 highest_frequency_hz: highest.frequency().get(),
311 samples_per_second: minimum_temporal_samples_per_second,
312 });
313 }
314 let sampling_requirements = MultiToneSamplingRequirements {
315 highest_frequency: highest.frequency(),
316 spatial_certificate: highest.sampling_certificate(),
317 minimum_temporal_samples_per_second,
318 maximum_temporal_step_seconds: 1.0 / minimum_temporal_samples_per_second,
319 };
320 Ok(Self {
321 tones,
322 plane,
323 sampling_requirements,
324 })
325 }
326
327 pub fn tones(&self) -> &[ToneStudy] {
329 &self.tones
330 }
331
332 pub fn plane(&self) -> SamplingPlane {
334 self.plane
335 }
336
337 pub fn sampling_requirements(&self) -> MultiToneSamplingRequirements {
339 self.sampling_requirements
340 }
341
342 pub fn combine(
349 &self,
350 combination: ToneCombination,
351 ) -> Result<MultiToneProjection, MultiToneError> {
352 if let ToneCombination::Instant { seconds } = combination
353 && !seconds.is_finite()
354 {
355 return Err(MultiToneError::InvalidSeconds { seconds });
356 }
357
358 let cells = self.plane.cell_count();
359 let mut samples = Vec::new();
360 samples
361 .try_reserve_exact(cells)
362 .map_err(|_| MultiToneError::AllocationFailed { cells })?;
363 for index in 0..cells {
364 let row = index / self.plane.columns();
365 let column = index % self.plane.columns();
366 let mut accumulation = CompensatedSum::default();
367 for tone in &self.tones {
368 let value = component_scalar(tone, index, combination)?;
369 let contribution = tone.weight() * value;
370 if !contribution.is_finite() {
371 return Err(MultiToneError::NonFiniteContribution {
372 frequency_hz: tone.frequency().get(),
373 row,
374 column,
375 value: contribution,
376 });
377 }
378 accumulation.add(contribution);
379 if !accumulation.is_finite() {
380 return Err(MultiToneError::NonFiniteAccumulation {
381 row,
382 column,
383 value: accumulation.total(),
384 });
385 }
386 }
387 samples.push(accumulation.total());
388 }
389 let mut components = Vec::new();
390 components
391 .try_reserve_exact(self.tones.len())
392 .map_err(|_| MultiToneError::CertificateAllocationFailed {
393 tones: self.tones.len(),
394 })?;
395 components.extend(self.tones.iter().map(ToneCertificate::from_study));
396 Ok(MultiToneProjection {
397 rows: self.plane.rows(),
398 columns: self.plane.columns(),
399 samples,
400 certificate: MultiToneCertificate {
401 plane: self.plane,
402 combination,
403 sampling_requirements: self.sampling_requirements,
404 components,
405 },
406 })
407 }
408}
409
410fn component_scalar(
411 tone: &ToneStudy,
412 index: usize,
413 combination: ToneCombination,
414) -> Result<f64, MultiToneError> {
415 let real = tone.field().real()[index];
416 let imaginary = tone.field().imaginary()[index];
417 match combination {
418 ToneCombination::IncoherentMagnitudeSquared => {
419 Ok(real.mul_add(real, imaginary * imaginary))
420 }
421 ToneCombination::Instant { seconds: 0.0 } => Ok(real),
422 ToneCombination::Instant { seconds } => {
423 let angular_time = TAU * tone.frequency().get() * seconds;
424 if !angular_time.is_finite() {
425 return Err(MultiToneError::NonFiniteAngularTime {
426 frequency_hz: tone.frequency().get(),
427 seconds,
428 angular_time,
429 });
430 }
431 Ok(real * angular_time.cos() + imaginary * angular_time.sin())
432 }
433 }
434}
435
436#[cfg(test)]
437#[path = "multitone_tests.rs"]
438mod tests;