sim_lib_interference_solve/
reduce.rs1use std::ops::Range;
4
5use sim_lib_interference_core::SamplingCertificate;
6
7use crate::{
8 DetectorFootprint, GridDimensions, HostPhasorField, LossClass, Observable,
9 ProjectionCertificate, ProjectionError, ScalarProjection, ScalarSample, project,
10 projection::{allocate_samples, project_complex, validate_request},
11};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum ReductionRule {
16 Detail,
18 DetectorComplexMean,
20 DetectorScalarAreaMean,
22 DetectorMagnitudeSquaredAreaMean,
24}
25
26#[allow(clippy::too_many_arguments)]
34pub fn reduce_for_view(
35 field: &HostPhasorField,
36 source_sampling_certificate: SamplingCertificate,
37 observable: Observable,
38 phase_floor: f64,
39 target_rows: usize,
40 target_columns: usize,
41 rule: ReductionRule,
42) -> Result<ScalarProjection, ProjectionError> {
43 validate_request(observable, phase_floor)?;
44 let source = GridDimensions::from_field(field);
45 let target = target_dimensions(target_rows, target_columns)?;
46 if target.rows > source.rows || target.columns > source.columns {
47 return Err(ProjectionError::TargetExceedsSource { source, target });
48 }
49 if rule == ReductionRule::Detail {
50 if target != source {
51 return Err(ProjectionError::DetailReductionRefused { source, target });
52 }
53 return project(field, source_sampling_certificate, observable, phase_floor);
54 }
55 require_compatible(observable, rule)?;
56
57 let cells = target
58 .rows
59 .checked_mul(target.columns)
60 .expect("a target no larger than an existing field must fit");
61 let mut samples = allocate_samples(cells)?;
62 let mut mask_count = 0;
63 for target_row in 0..target.rows {
64 let source_rows = axis_partition(target_row, source.rows, target.rows);
65 for target_column in 0..target.columns {
66 let source_columns = axis_partition(target_column, source.columns, target.columns);
67 let sample = reduce_cell(
68 field,
69 observable,
70 phase_floor,
71 source_rows.clone(),
72 source_columns,
73 target_row,
74 target_column,
75 rule,
76 )?;
77 mask_count += usize::from(sample == ScalarSample::Masked);
78 samples.push(sample);
79 }
80 }
81
82 Ok(ScalarProjection {
83 rows: target.rows,
84 columns: target.columns,
85 samples,
86 certificate: ProjectionCertificate {
87 source_dimensions: source,
88 target_dimensions: target,
89 footprint: DetectorFootprint {
90 min_rows: source.rows / target.rows,
91 max_rows: source.rows.div_ceil(target.rows),
92 min_columns: source.columns / target.columns,
93 max_columns: source.columns.div_ceil(target.columns),
94 },
95 observable,
96 phase_floor,
97 rule,
98 loss_class: if source == target {
99 LossClass::Lossless
100 } else {
101 LossClass::DetectorIntegration
102 },
103 source_sampling_certificate,
104 mask_count,
105 },
106 })
107}
108
109fn target_dimensions(rows: usize, columns: usize) -> Result<GridDimensions, ProjectionError> {
110 if rows == 0 {
111 return Err(ProjectionError::ZeroTargetDimension { axis: "rows" });
112 }
113 if columns == 0 {
114 return Err(ProjectionError::ZeroTargetDimension { axis: "columns" });
115 }
116 Ok(GridDimensions { rows, columns })
117}
118
119fn require_compatible(observable: Observable, rule: ReductionRule) -> Result<(), ProjectionError> {
120 let compatible = matches!(
121 (observable, rule),
122 (
123 Observable::Real
124 | Observable::Imaginary
125 | Observable::Phase
126 | Observable::Instant { .. },
127 ReductionRule::DetectorComplexMean
128 ) | (Observable::Amplitude, ReductionRule::DetectorScalarAreaMean)
129 | (
130 Observable::MagnitudeSquared,
131 ReductionRule::DetectorMagnitudeSquaredAreaMean
132 )
133 );
134 compatible
135 .then_some(())
136 .ok_or(ProjectionError::IncompatibleReduction { observable, rule })
137}
138
139fn axis_partition(index: usize, source: usize, target: usize) -> Range<usize> {
140 let base = source / target;
141 let remainder = source % target;
142 let start = index * base + index.min(remainder);
143 let length = base + usize::from(index < remainder);
144 start..start + length
145}
146
147#[allow(clippy::too_many_arguments)]
148fn reduce_cell(
149 field: &HostPhasorField,
150 observable: Observable,
151 phase_floor: f64,
152 source_rows: Range<usize>,
153 source_columns: Range<usize>,
154 target_row: usize,
155 target_column: usize,
156 rule: ReductionRule,
157) -> Result<ScalarSample, ProjectionError> {
158 let count = source_rows.len() * source_columns.len();
159 let scale = 1.0 / count as f64;
160 match rule {
161 ReductionRule::Detail => unreachable!("detail returns before detector traversal"),
162 ReductionRule::DetectorComplexMean => {
163 let (mut real_sum, mut real_correction) = (0.0, 0.0);
164 let (mut imaginary_sum, mut imaginary_correction) = (0.0, 0.0);
165 let first_index = source_rows.start * field.columns() + source_columns.start;
166 let first_real = field.real()[first_index];
167 let first_imaginary = field.imaginary()[first_index];
168 let mut constant = true;
169 for row in source_rows {
170 for column in source_columns.clone() {
171 let index = row * field.columns() + column;
172 constant &= field.real()[index].to_bits() == first_real.to_bits()
173 && field.imaginary()[index].to_bits() == first_imaginary.to_bits();
174 compensated_add(
175 &mut real_sum,
176 &mut real_correction,
177 field.real()[index] * scale,
178 );
179 compensated_add(
180 &mut imaginary_sum,
181 &mut imaginary_correction,
182 field.imaginary()[index] * scale,
183 );
184 }
185 }
186 let (real, imaginary) = if constant {
187 (first_real, first_imaginary)
188 } else {
189 (
190 real_sum + real_correction,
191 imaginary_sum + imaginary_correction,
192 )
193 };
194 project_complex(
195 real,
196 imaginary,
197 observable,
198 phase_floor,
199 target_row,
200 target_column,
201 )
202 }
203 ReductionRule::DetectorScalarAreaMean | ReductionRule::DetectorMagnitudeSquaredAreaMean => {
204 let (mut sum, mut correction) = (0.0, 0.0);
205 let mut first_value: Option<f64> = None;
206 let mut constant = true;
207 for row in source_rows {
208 for column in source_columns.clone() {
209 let index = row * field.columns() + column;
210 let ScalarSample::Value(value) = project_complex(
211 field.real()[index],
212 field.imaginary()[index],
213 observable,
214 phase_floor,
215 row,
216 column,
217 )?
218 else {
219 unreachable!("amplitude and squared magnitude are never masked");
220 };
221 if let Some(first) = first_value {
222 constant &= value.to_bits() == first.to_bits();
223 } else {
224 first_value = Some(value);
225 }
226 compensated_add(&mut sum, &mut correction, value * scale);
227 }
228 }
229 let value = if constant {
230 first_value.expect("detector partitions are non-empty")
231 } else {
232 sum + correction
233 };
234 if value.is_finite() {
235 Ok(ScalarSample::Value(value))
236 } else {
237 Err(ProjectionError::NonFiniteSample {
238 row: target_row,
239 column: target_column,
240 observable,
241 value,
242 })
243 }
244 }
245 }
246}
247
248fn compensated_add(sum: &mut f64, correction: &mut f64, value: f64) {
249 let next = *sum + value;
250 *correction += if sum.abs() >= value.abs() {
251 (*sum - next) + value
252 } else {
253 (value - next) + *sum
254 };
255 *sum = next;
256}
257
258#[cfg(test)]
259#[path = "reduce_tests.rs"]
260mod tests;