1use sim_lib_interference_core::{
4 Emitter, InterferenceError, InterferenceProblem, Point3M, RequestPreflight, SamplingPlane,
5 SamplingPolicy, SamplingThresholds, WorkBudget, contribution_at,
6};
7
8use crate::{
9 HostPhasorField, ReferenceSolveError,
10 complex::{CompensatedSum, Complex64},
11};
12
13#[derive(Clone, Debug, PartialEq)]
15pub struct SolveEvidence {
16 preflight: RequestPreflight,
17 completed_cells: u64,
18 completed_emitter_evaluations: u64,
19}
20
21impl SolveEvidence {
22 pub fn preflight(&self) -> RequestPreflight {
24 self.preflight
25 }
26
27 pub fn completed_cells(&self) -> u64 {
29 self.completed_cells
30 }
31
32 pub fn completed_emitter_evaluations(&self) -> u64 {
34 self.completed_emitter_evaluations
35 }
36}
37
38#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct ReferencePhasorSolver {
41 sampling_policy: SamplingPolicy,
42 sampling_thresholds: SamplingThresholds,
43 work_budget: WorkBudget,
44}
45
46impl ReferencePhasorSolver {
47 pub fn new(
49 sampling_policy: SamplingPolicy,
50 sampling_thresholds: SamplingThresholds,
51 work_budget: WorkBudget,
52 ) -> Self {
53 Self {
54 sampling_policy,
55 sampling_thresholds,
56 work_budget,
57 }
58 }
59
60 pub fn sampling_policy(self) -> SamplingPolicy {
62 self.sampling_policy
63 }
64
65 pub fn sampling_thresholds(self) -> SamplingThresholds {
67 self.sampling_thresholds
68 }
69
70 pub fn work_budget(self) -> WorkBudget {
72 self.work_budget
73 }
74
75 pub fn solve(
81 self,
82 problem: &InterferenceProblem,
83 plane: &SamplingPlane,
84 ) -> Result<(HostPhasorField, SolveEvidence), ReferenceSolveError> {
85 let preflight = RequestPreflight::admit(
86 problem,
87 plane,
88 self.sampling_policy,
89 self.sampling_thresholds,
90 self.work_budget,
91 )
92 .map_err(ReferenceSolveError::from)?;
93 preflight_geometry(problem, plane)?;
94 let field = evaluate_field(problem, plane)?;
95 let evidence = SolveEvidence {
96 preflight,
97 completed_cells: preflight.work_estimate.cells,
98 completed_emitter_evaluations: preflight.work_estimate.emitter_evaluations,
99 };
100 Ok((field, evidence))
101 }
102}
103
104impl Default for ReferencePhasorSolver {
105 fn default() -> Self {
106 Self::new(
107 SamplingPolicy::Strict,
108 SamplingThresholds::default(),
109 WorkBudget::default(),
110 )
111 }
112}
113
114fn preflight_geometry(
115 problem: &InterferenceProblem,
116 plane: &SamplingPlane,
117) -> Result<(), ReferenceSolveError> {
118 for row in 0..plane.rows() {
119 for column in 0..plane.columns() {
120 let at =
121 plane
122 .point_at(row, column)
123 .map_err(|cause| ReferenceSolveError::CellGeometry {
124 row,
125 column,
126 cause: Box::new(cause),
127 })?;
128 for source in &problem.sources {
129 validate_source_geometry(problem, source, at).map_err(|cause| {
130 ReferenceSolveError::SourceAtCell {
131 source_id: source.id().to_owned(),
132 row,
133 column,
134 cause: Box::new(cause),
135 }
136 })?;
137 }
138 }
139 }
140 Ok(())
141}
142
143fn validate_source_geometry(
144 problem: &InterferenceProblem,
145 source: &Emitter,
146 at: Point3M,
147) -> Result<(), InterferenceError> {
148 match source {
149 Emitter::Point { id, position, .. } => {
150 let distance = at.distance_to(*position);
151 if !distance.is_finite() {
152 return Err(InterferenceError::NonFinitePropagation {
153 source_id: id.clone(),
154 name: "point-distance-metres",
155 value: distance,
156 });
157 }
158 if distance <= problem.singularity_radius.get() {
159 return Err(InterferenceError::SingularPointSample {
160 source_id: id.clone(),
161 distance_metres: distance,
162 singularity_radius_metres: problem.singularity_radius.get(),
163 });
164 }
165 }
166 Emitter::ForwardPlane {
167 id,
168 through,
169 direction,
170 ..
171 } => {
172 let signed_distance = direction.signed_distance_metres(*through, at);
173 if !signed_distance.is_finite() {
174 return Err(InterferenceError::NonFinitePropagation {
175 source_id: id.clone(),
176 name: "plane-signed-distance-metres",
177 value: signed_distance,
178 });
179 }
180 if signed_distance < 0.0 {
181 return Err(InterferenceError::BehindForwardPlane {
182 source_id: id.clone(),
183 signed_distance_metres: signed_distance,
184 });
185 }
186 }
187 }
188 Ok(())
189}
190
191fn evaluate_field(
192 problem: &InterferenceProblem,
193 plane: &SamplingPlane,
194) -> Result<HostPhasorField, ReferenceSolveError> {
195 let mut field = HostPhasorField::try_zeroed(plane.rows(), plane.columns())?;
196 for row in 0..plane.rows() {
197 for column in 0..plane.columns() {
198 let at =
199 plane
200 .point_at(row, column)
201 .map_err(|cause| ReferenceSolveError::CellGeometry {
202 row,
203 column,
204 cause: Box::new(cause),
205 })?;
206 let value = accumulate_cell(problem, at, row, column)?;
207 let index = row * plane.columns() + column;
208 field.set_index(index, value);
209 }
210 }
211 Ok(field)
212}
213
214fn accumulate_cell(
215 problem: &InterferenceProblem,
216 at: Point3M,
217 row: usize,
218 column: usize,
219) -> Result<Complex64, ReferenceSolveError> {
220 let mut real_sum = CompensatedSum::default();
221 let mut imaginary_sum = CompensatedSum::default();
222
223 for source in &problem.sources {
224 let source_id = source.id();
225 let (real, imaginary) = contribution_at(problem, source, at).map_err(|cause| {
226 ReferenceSolveError::SourceAtCell {
227 source_id: source_id.to_owned(),
228 row,
229 column,
230 cause: Box::new(cause),
231 }
232 })?;
233 real_sum.add(real);
234 require_finite_accumulation(source_id, row, column, "real", real_sum)?;
235 imaginary_sum.add(imaginary);
236 require_finite_accumulation(source_id, row, column, "imaginary", imaginary_sum)?;
237 }
238
239 Ok(Complex64::new(real_sum.total(), imaginary_sum.total()))
240}
241
242fn require_finite_accumulation(
243 source_id: &str,
244 row: usize,
245 column: usize,
246 component: &'static str,
247 accumulation: CompensatedSum,
248) -> Result<(), ReferenceSolveError> {
249 let value = accumulation.total();
250 if accumulation.is_finite() {
251 Ok(())
252 } else {
253 Err(ReferenceSolveError::NonFiniteAccumulation {
254 source_id: source_id.to_owned(),
255 row,
256 column,
257 component,
258 value,
259 })
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use sim_lib_interference_core::{
266 Emitter, FieldAmplitude, Hertz, InterferenceProblem, MetresPerSecond, NepersPerMetre,
267 Point3M, PositiveMetres, Radians, SamplingPlane, SamplingPolicy, SamplingThresholds,
268 ScalarMedium, SourceSet, UnitVector3, WorkBudget,
269 };
270
271 use super::{ReferencePhasorSolver, evaluate_field};
272
273 fn point(x: f64, y: f64, z: f64) -> Point3M {
274 Point3M::from_metres(x, y, z).unwrap()
275 }
276
277 fn plane(rows: usize, columns: usize) -> SamplingPlane {
278 SamplingPlane::new(
279 point(0.0, 0.0, 0.0),
280 UnitVector3::new(1.0, 0.0, 0.0).unwrap(),
281 UnitVector3::new(0.0, 1.0, 0.0).unwrap(),
282 PositiveMetres::new(1.0).unwrap(),
283 PositiveMetres::new(1.0).unwrap(),
284 rows,
285 columns,
286 )
287 .unwrap()
288 }
289
290 fn problem(sources: Vec<Emitter>) -> InterferenceProblem {
291 InterferenceProblem::new(
292 Hertz::new(1.0).unwrap(),
293 ScalarMedium::new(
294 MetresPerSecond::new(100.0).unwrap(),
295 NepersPerMetre::new(0.0).unwrap(),
296 ),
297 SourceSet::new(sources).unwrap(),
298 PositiveMetres::new(0.001).unwrap(),
299 )
300 }
301
302 fn plane_source(id: &str, amplitude: f64) -> Emitter {
303 Emitter::ForwardPlane {
304 id: id.to_owned(),
305 through: point(0.0, 0.0, 0.0),
306 direction: UnitVector3::new(0.0, 0.0, 1.0).unwrap(),
307 amplitude: FieldAmplitude::new(amplitude).unwrap(),
308 phase: Radians::new(0.0).unwrap(),
309 }
310 }
311
312 #[test]
313 fn solver_configuration_is_explicit_and_immutable() {
314 let thresholds = SamplingThresholds::new(12.0, 6.0, 0.025, 0.075).unwrap();
315 let budget = WorkBudget {
316 max_cells: 100,
317 max_emitter_evaluations: 200,
318 max_host_bytes: 3_200,
319 max_result_bytes: 1_600,
320 max_certificate_stencil_work: 700,
321 };
322 let solver = ReferencePhasorSolver::new(SamplingPolicy::Annotate, thresholds, budget);
323
324 assert_eq!(solver.sampling_policy(), SamplingPolicy::Annotate);
325 assert_eq!(solver.sampling_thresholds(), thresholds);
326 assert_eq!(solver.work_budget(), budget);
327 }
328
329 #[test]
330 fn row_major_cells_use_canonical_sources_and_neumaier_components() {
331 let problem = problem(vec![
332 plane_source("c-small", 1.0),
333 plane_source("a-large", 1.0e16),
334 plane_source("b-small", 1.0),
335 ]);
336 assert_eq!(
337 problem.sources.iter().map(Emitter::id).collect::<Vec<_>>(),
338 ["a-large", "b-small", "c-small"]
339 );
340
341 let field = evaluate_field(&problem, &plane(2, 3)).unwrap();
342 assert_eq!(field.real(), &[1.0e16 + 2.0; 6]);
343 assert_eq!(field.imaginary(), &[0.0; 6]);
344 assert_eq!(field.cell(0, 2), Some((field.real()[2], 0.0)));
345 assert_eq!(field.cell(1, 0), Some((field.real()[3], 0.0)));
346 }
347
348 #[test]
349 fn solve_retains_complete_sampling_and_work_preflight() {
350 let problem = problem(vec![plane_source("plane", 2.0)]);
351 let (field, evidence) = ReferencePhasorSolver::default()
352 .solve(&problem, &plane(2, 3))
353 .unwrap();
354
355 assert_eq!(field.len(), 6);
356 assert_eq!(evidence.completed_cells(), 6);
357 assert_eq!(evidence.completed_emitter_evaluations(), 6);
358 assert_eq!(evidence.preflight().work_estimate.cells, 6);
359 assert_eq!(evidence.preflight().work_estimate.host_bytes, 96);
360 assert_eq!(evidence.preflight().sampling_policy, SamplingPolicy::Strict);
361 }
362
363 #[test]
364 fn request_and_cell_geometry_fail_before_field_construction() {
365 let problem = problem(vec![plane_source("plane", 1.0)]);
366 let no_cells = WorkBudget {
367 max_cells: 0,
368 ..WorkBudget::default()
369 };
370 let budget_error = ReferencePhasorSolver::new(
371 SamplingPolicy::Annotate,
372 SamplingThresholds::default(),
373 no_cells,
374 )
375 .solve(&problem, &plane(1, 1))
376 .unwrap_err();
377 assert!(matches!(
378 budget_error,
379 crate::ReferenceSolveError::Request { cause }
380 if matches!(*cause, sim_lib_interference_core::InterferenceError::WorkBudgetExceeded { .. })
381 ));
382
383 let extreme_plane = SamplingPlane::new(
384 point(f64::MAX, 0.0, 0.0),
385 UnitVector3::new(1.0, 0.0, 0.0).unwrap(),
386 UnitVector3::new(0.0, 1.0, 0.0).unwrap(),
387 PositiveMetres::new(f64::MAX).unwrap(),
388 PositiveMetres::new(1.0).unwrap(),
389 1,
390 1,
391 )
392 .unwrap();
393 let geometry_error = ReferencePhasorSolver::new(
394 SamplingPolicy::Annotate,
395 SamplingThresholds::default(),
396 WorkBudget::default(),
397 )
398 .solve(&problem, &extreme_plane)
399 .unwrap_err();
400 assert!(matches!(
401 geometry_error,
402 crate::ReferenceSolveError::CellGeometry {
403 row: 0,
404 column: 0,
405 ..
406 }
407 ));
408 }
409
410 #[test]
411 fn singular_and_behind_samples_name_source_and_cell() {
412 let singular_problem = InterferenceProblem::new(
413 Hertz::new(1.0).unwrap(),
414 ScalarMedium::new(
415 MetresPerSecond::new(100.0).unwrap(),
416 NepersPerMetre::new(0.0).unwrap(),
417 ),
418 SourceSet::new(vec![Emitter::Point {
419 id: "near-point".to_owned(),
420 position: point(0.5, 0.5, 0.01),
421 amplitude_at_reference: FieldAmplitude::new(1.0).unwrap(),
422 phase: Radians::new(0.0).unwrap(),
423 }])
424 .unwrap(),
425 PositiveMetres::new(0.02).unwrap(),
426 );
427 let solver = ReferencePhasorSolver::new(
428 SamplingPolicy::Annotate,
429 SamplingThresholds::default(),
430 WorkBudget::default(),
431 );
432 let singular_error = solver.solve(&singular_problem, &plane(1, 1)).unwrap_err();
433 assert!(matches!(
434 singular_error,
435 crate::ReferenceSolveError::SourceAtCell {
436 source_id,
437 row: 0,
438 column: 0,
439 cause,
440 } if source_id == "near-point"
441 && matches!(*cause, sim_lib_interference_core::InterferenceError::SingularPointSample { .. })
442 ));
443
444 let behind_problem = problem(vec![Emitter::ForwardPlane {
445 id: "forward-only".to_owned(),
446 through: point(0.0, 0.0, 1.0),
447 direction: UnitVector3::new(0.0, 0.0, 1.0).unwrap(),
448 amplitude: FieldAmplitude::new(1.0).unwrap(),
449 phase: Radians::new(0.0).unwrap(),
450 }]);
451 let behind_error = solver.solve(&behind_problem, &plane(2, 2)).unwrap_err();
452 assert!(matches!(
453 behind_error,
454 crate::ReferenceSolveError::SourceAtCell {
455 source_id,
456 row: 0,
457 column: 0,
458 cause,
459 } if source_id == "forward-only"
460 && matches!(*cause, sim_lib_interference_core::InterferenceError::BehindForwardPlane { .. })
461 ));
462 }
463}