1use std::{f64::consts::PI, fmt};
4
5use sim_lib_interference_core::SamplingCertificate;
6
7use crate::{HostPhasorField, Observable, ReductionRule};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct GridDimensions {
12 pub(crate) rows: usize,
13 pub(crate) columns: usize,
14}
15
16impl GridDimensions {
17 pub(crate) fn from_field(field: &HostPhasorField) -> Self {
18 Self {
19 rows: field.rows(),
20 columns: field.columns(),
21 }
22 }
23
24 pub fn rows(self) -> usize {
26 self.rows
27 }
28
29 pub fn columns(self) -> usize {
31 self.columns
32 }
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub struct DetectorFootprint {
38 pub(crate) min_rows: usize,
39 pub(crate) max_rows: usize,
40 pub(crate) min_columns: usize,
41 pub(crate) max_columns: usize,
42}
43
44impl DetectorFootprint {
45 pub(crate) fn detail() -> Self {
46 Self {
47 min_rows: 1,
48 max_rows: 1,
49 min_columns: 1,
50 max_columns: 1,
51 }
52 }
53
54 pub fn min_rows(self) -> usize {
56 self.min_rows
57 }
58
59 pub fn max_rows(self) -> usize {
61 self.max_rows
62 }
63
64 pub fn min_columns(self) -> usize {
66 self.min_columns
67 }
68
69 pub fn max_columns(self) -> usize {
71 self.max_columns
72 }
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum LossClass {
78 Lossless,
80 DetectorIntegration,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq)]
86pub struct ProjectionCertificate {
87 pub(crate) source_dimensions: GridDimensions,
88 pub(crate) target_dimensions: GridDimensions,
89 pub(crate) footprint: DetectorFootprint,
90 pub(crate) observable: Observable,
91 pub(crate) phase_floor: f64,
92 pub(crate) rule: ReductionRule,
93 pub(crate) loss_class: LossClass,
94 pub(crate) source_sampling_certificate: SamplingCertificate,
95 pub(crate) mask_count: usize,
96}
97
98impl ProjectionCertificate {
99 pub fn identity(self) -> ProjectionIdentity {
106 ProjectionIdentity {
107 source_dimensions: self.source_dimensions,
108 target_dimensions: self.target_dimensions,
109 footprint: self.footprint,
110 observable: self.observable,
111 phase_floor: self.phase_floor,
112 rule: self.rule,
113 loss_class: self.loss_class,
114 }
115 }
116
117 pub fn source_dimensions(self) -> GridDimensions {
119 self.source_dimensions
120 }
121
122 pub fn target_dimensions(self) -> GridDimensions {
124 self.target_dimensions
125 }
126
127 pub fn footprint(self) -> DetectorFootprint {
129 self.footprint
130 }
131
132 pub fn observable(self) -> Observable {
134 self.observable
135 }
136
137 pub fn phase_floor(self) -> f64 {
139 self.phase_floor
140 }
141
142 pub fn rule(self) -> ReductionRule {
144 self.rule
145 }
146
147 pub fn loss_class(self) -> LossClass {
149 self.loss_class
150 }
151
152 pub fn source_sampling_certificate(self) -> SamplingCertificate {
154 self.source_sampling_certificate
155 }
156
157 pub fn mask_count(self) -> usize {
159 self.mask_count
160 }
161}
162
163#[derive(Clone, Copy, Debug, PartialEq)]
169pub struct ProjectionIdentity {
170 source_dimensions: GridDimensions,
171 target_dimensions: GridDimensions,
172 footprint: DetectorFootprint,
173 observable: Observable,
174 phase_floor: f64,
175 rule: ReductionRule,
176 loss_class: LossClass,
177}
178
179impl ProjectionIdentity {
180 pub fn source_dimensions(self) -> GridDimensions {
182 self.source_dimensions
183 }
184
185 pub fn target_dimensions(self) -> GridDimensions {
187 self.target_dimensions
188 }
189
190 pub fn footprint(self) -> DetectorFootprint {
192 self.footprint
193 }
194
195 pub fn observable(self) -> Observable {
197 self.observable
198 }
199
200 pub fn phase_floor(self) -> f64 {
202 self.phase_floor
203 }
204
205 pub fn rule(self) -> ReductionRule {
207 self.rule
208 }
209
210 pub fn loss_class(self) -> LossClass {
212 self.loss_class
213 }
214}
215
216#[derive(Clone, Copy, Debug, PartialEq)]
218pub enum ScalarSample {
219 Value(f64),
221 Masked,
223}
224
225#[derive(Clone, Debug, PartialEq)]
227pub struct ScalarProjection {
228 pub(crate) rows: usize,
229 pub(crate) columns: usize,
230 pub(crate) samples: Vec<ScalarSample>,
231 pub(crate) certificate: ProjectionCertificate,
232}
233
234impl ScalarProjection {
235 pub fn rows(&self) -> usize {
237 self.rows
238 }
239
240 pub fn columns(&self) -> usize {
242 self.columns
243 }
244
245 pub fn samples(&self) -> &[ScalarSample] {
247 &self.samples
248 }
249
250 pub fn cell(&self, row: usize, column: usize) -> Option<ScalarSample> {
252 let index = row.checked_mul(self.columns)?.checked_add(column)?;
253 (row < self.rows && column < self.columns).then(|| self.samples[index])
254 }
255
256 pub fn certificate(&self) -> ProjectionCertificate {
258 self.certificate
259 }
260}
261
262#[derive(Clone, Debug, PartialEq)]
264pub enum ProjectionError {
265 InvalidPhaseFloor {
267 value: f64,
269 },
270 InvalidAngularTime {
272 wt: f64,
274 },
275 ZeroTargetDimension {
277 axis: &'static str,
279 },
280 TargetExceedsSource {
282 source: GridDimensions,
284 target: GridDimensions,
286 },
287 DetailReductionRefused {
289 source: GridDimensions,
291 target: GridDimensions,
293 },
294 IncompatibleReduction {
296 observable: Observable,
298 rule: ReductionRule,
300 },
301 NonFiniteSample {
303 row: usize,
305 column: usize,
307 observable: Observable,
309 value: f64,
311 },
312 AllocationFailed {
314 cells: usize,
316 },
317}
318
319impl fmt::Display for ProjectionError {
320 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
321 match self {
322 Self::InvalidPhaseFloor { value } => {
323 write!(
324 formatter,
325 "phase amplitude floor must be finite and non-negative: {value:?}"
326 )
327 }
328 Self::InvalidAngularTime { wt } => {
329 write!(
330 formatter,
331 "instantaneous angular time must be finite: {wt:?}"
332 )
333 }
334 Self::ZeroTargetDimension { axis } => {
335 write!(formatter, "target {axis} must be greater than zero")
336 }
337 Self::TargetExceedsSource { source, target } => write!(
338 formatter,
339 "target {}x{} exceeds source {}x{}; projection does not upsample",
340 target.rows, target.columns, source.rows, source.columns
341 ),
342 Self::DetailReductionRefused { source, target } => write!(
343 formatter,
344 "detail projection refuses reduction from {}x{} to {}x{}",
345 source.rows, source.columns, target.rows, target.columns
346 ),
347 Self::IncompatibleReduction { observable, rule } => write!(
348 formatter,
349 "observable {observable:?} is incompatible with reduction rule {rule:?}"
350 ),
351 Self::NonFiniteSample {
352 row,
353 column,
354 observable,
355 value,
356 } => write!(
357 formatter,
358 "{observable:?} projection produced non-finite sample {value:?} \
359 at ({row}, {column})"
360 ),
361 Self::AllocationFailed { cells } => {
362 write!(
363 formatter,
364 "could not reserve {cells} scalar projection cells"
365 )
366 }
367 }
368 }
369}
370
371impl std::error::Error for ProjectionError {}
372
373pub fn project(
379 field: &HostPhasorField,
380 source_sampling_certificate: SamplingCertificate,
381 observable: Observable,
382 phase_floor: f64,
383) -> Result<ScalarProjection, ProjectionError> {
384 validate_request(observable, phase_floor)?;
385 let mut samples = allocate_samples(field.len())?;
386 let mut mask_count = 0;
387 for index in 0..field.len() {
388 let row = index / field.columns();
389 let column = index % field.columns();
390 let sample = project_complex(
391 field.real()[index],
392 field.imaginary()[index],
393 observable,
394 phase_floor,
395 row,
396 column,
397 )?;
398 mask_count += usize::from(sample == ScalarSample::Masked);
399 samples.push(sample);
400 }
401
402 let dimensions = GridDimensions::from_field(field);
403 Ok(ScalarProjection {
404 rows: field.rows(),
405 columns: field.columns(),
406 samples,
407 certificate: ProjectionCertificate {
408 source_dimensions: dimensions,
409 target_dimensions: dimensions,
410 footprint: DetectorFootprint::detail(),
411 observable,
412 phase_floor,
413 rule: ReductionRule::Detail,
414 loss_class: LossClass::Lossless,
415 source_sampling_certificate,
416 mask_count,
417 },
418 })
419}
420
421pub(crate) fn validate_request(
422 observable: Observable,
423 phase_floor: f64,
424) -> Result<(), ProjectionError> {
425 if !phase_floor.is_finite() || phase_floor < 0.0 {
426 return Err(ProjectionError::InvalidPhaseFloor { value: phase_floor });
427 }
428 if let Observable::Instant { wt } = observable
429 && !wt.is_finite()
430 {
431 return Err(ProjectionError::InvalidAngularTime { wt });
432 }
433 Ok(())
434}
435
436pub(crate) fn project_complex(
437 real: f64,
438 imaginary: f64,
439 observable: Observable,
440 phase_floor: f64,
441 row: usize,
442 column: usize,
443) -> Result<ScalarSample, ProjectionError> {
444 let amplitude = real.hypot(imaginary);
445 let sample = match observable {
446 Observable::Real => ScalarSample::Value(real),
447 Observable::Imaginary => ScalarSample::Value(imaginary),
448 Observable::Amplitude => ScalarSample::Value(amplitude),
449 Observable::Phase if amplitude <= phase_floor => ScalarSample::Masked,
450 Observable::Phase => {
451 let phase = imaginary.atan2(real);
452 ScalarSample::Value(if phase == PI { -PI } else { phase })
453 }
454 Observable::MagnitudeSquared => {
455 ScalarSample::Value(real.mul_add(real, imaginary * imaginary))
456 }
457 Observable::Instant { wt } => ScalarSample::Value(if wt == 0.0 {
458 real
459 } else {
460 real * wt.cos() + imaginary * wt.sin()
461 }),
462 };
463 if let ScalarSample::Value(value) = sample
464 && !value.is_finite()
465 {
466 return Err(ProjectionError::NonFiniteSample {
467 row,
468 column,
469 observable,
470 value,
471 });
472 }
473 Ok(sample)
474}
475
476pub(crate) fn allocate_samples(cells: usize) -> Result<Vec<ScalarSample>, ProjectionError> {
477 let mut samples = Vec::new();
478 samples
479 .try_reserve_exact(cells)
480 .map_err(|_| ProjectionError::AllocationFailed { cells })?;
481 Ok(samples)
482}
483
484#[cfg(test)]
485#[path = "projection_tests.rs"]
486mod tests;