1use sim_lib_interference_core::{
4 Emitter, InterferenceProblem, POINT_SOURCE_REFERENCE_DISTANCE_METRES, Point3M,
5};
6
7use crate::{LoweringError, PhaseBudget, PreflightCheck};
8
9#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct PointTileConstants {
12 pub n0: [f32; 3],
14 pub rho: f32,
16 pub phase_cos: f32,
18 pub phase_sin: f32,
20 pub gain0: f32,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct PlaneTileConstants {
27 pub direction: [f32; 3],
29 pub signed_distance0_m: f64,
31 pub phase_cos: f32,
33 pub phase_sin: f32,
35 pub gain0: f32,
37}
38
39#[derive(Clone, Copy, Debug, Default, PartialEq)]
41pub struct SourcePhaseEstimate {
42 pub max_abs_residual_phase_rad: f64,
44 pub max_predicted_geometry_error_rad: f64,
46 pub max_predicted_roundoff_error_rad: f64,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq)]
52pub enum SourceTileConstants {
53 Point {
55 constants: PointTileConstants,
57 estimate: SourcePhaseEstimate,
59 },
60 ForwardPlane {
62 constants: PlaneTileConstants,
64 estimate: SourcePhaseEstimate,
66 },
67}
68
69impl SourceTileConstants {
70 pub fn estimate(self) -> SourcePhaseEstimate {
72 match self {
73 Self::Point { estimate, .. } | Self::ForwardPlane { estimate, .. } => estimate,
74 }
75 }
76}
77
78pub(crate) fn prepare_source_constants(
79 problem: &InterferenceProblem,
80 source: &Emitter,
81 center: Point3M,
82 local_f64: &[[f64; 3]],
83 local_f32: &[[f32; 3]],
84 budget: PhaseBudget,
85) -> Result<SourceTileConstants, LoweringError> {
86 let estimates = EstimateInputs {
87 wavenumber: problem.wavenumber().real_radians_per_metre(),
88 local_f64,
89 local_f32,
90 budget,
91 };
92 match source {
93 Emitter::Point {
94 id,
95 position,
96 amplitude_at_reference,
97 phase,
98 } => {
99 let center_xyz = center.coordinates_metres();
100 let source_xyz = position.coordinates_metres();
101 let w0 = [
102 center_xyz[0] - source_xyz[0],
103 center_xyz[1] - source_xyz[1],
104 center_xyz[2] - source_xyz[2],
105 ];
106 let r0 = norm(w0);
107 if !r0.is_finite() || r0 <= problem.singularity_radius.get() {
108 return Err(LoweringError::new(
109 PreflightCheck::Singularity,
110 format!(
111 "point source {id} tile center distance {r0} is at or inside {}",
112 problem.singularity_radius.get()
113 ),
114 ));
115 }
116 let rho = 1.0 / r0;
117 let n0 = [w0[0] / r0, w0[1] / r0, w0[2] / r0];
118 let wave = problem.wavenumber();
119 let phase0 = wave.real_radians_per_metre().mul_add(r0, phase.get());
120 let gain0 = point_gain(
121 amplitude_at_reference.get(),
122 wave.imaginary_nepers_per_metre(),
123 r0,
124 )?;
125 let constants = PointTileConstants {
126 n0: [
127 finite_f32("point n0.x", n0[0], false)?,
128 finite_f32("point n0.y", n0[1], false)?,
129 finite_f32("point n0.z", n0[2], false)?,
130 ],
131 rho: finite_f32("point rho", rho, true)?,
132 phase_cos: finite_f32("point phase cosine", phase0.cos(), false)?,
133 phase_sin: finite_f32("point phase sine", phase0.sin(), false)?,
134 gain0: finite_f32("point center gain", gain0, gain0 != 0.0)?,
135 };
136 let estimate = point_estimate(
137 id,
138 w0,
139 problem.singularity_radius.get(),
140 constants,
141 estimates,
142 )?;
143 Ok(SourceTileConstants::Point {
144 constants,
145 estimate,
146 })
147 }
148 Emitter::ForwardPlane {
149 id,
150 through,
151 direction,
152 amplitude,
153 phase,
154 } => {
155 let signed_distance0_m = direction.signed_distance_metres(*through, center);
156 if !signed_distance0_m.is_finite() {
157 return Err(LoweringError::new(
158 PreflightCheck::ForwardPlane,
159 format!("forward plane {id} has a non-finite center distance"),
160 ));
161 }
162 let wave = problem.wavenumber();
163 let phase0 = wave
164 .real_radians_per_metre()
165 .mul_add(signed_distance0_m, phase.get());
166 let gain0 = plane_gain(
167 amplitude.get(),
168 wave.imaginary_nepers_per_metre(),
169 signed_distance0_m,
170 )?;
171 let direction64 = direction.components();
172 let constants = PlaneTileConstants {
173 direction: [
174 finite_f32("plane direction.x", direction64[0], false)?,
175 finite_f32("plane direction.y", direction64[1], false)?,
176 finite_f32("plane direction.z", direction64[2], false)?,
177 ],
178 signed_distance0_m,
179 phase_cos: finite_f32("plane phase cosine", phase0.cos(), false)?,
180 phase_sin: finite_f32("plane phase sine", phase0.sin(), false)?,
181 gain0: finite_f32("plane center gain", gain0, gain0 != 0.0)?,
182 };
183 let estimate = plane_estimate(
184 id,
185 signed_distance0_m,
186 direction64,
187 constants.direction,
188 estimates,
189 )?;
190 Ok(SourceTileConstants::ForwardPlane {
191 constants,
192 estimate,
193 })
194 }
195 }
196}
197
198#[derive(Clone, Copy)]
199struct EstimateInputs<'a> {
200 wavenumber: f64,
201 local_f64: &'a [[f64; 3]],
202 local_f32: &'a [[f32; 3]],
203 budget: PhaseBudget,
204}
205
206fn point_estimate(
207 source_id: &str,
208 w0: [f64; 3],
209 singularity_radius: f64,
210 constants: PointTileConstants,
211 inputs: EstimateInputs<'_>,
212) -> Result<SourcePhaseEstimate, LoweringError> {
213 let mut estimate = SourcePhaseEstimate::default();
214 let k32 = finite_f32("real wavenumber", inputs.wavenumber, true)?;
215 for (offset64, offset32) in inputs.local_f64.iter().zip(inputs.local_f32) {
216 let sample_vector = [
217 w0[0] + offset64[0],
218 w0[1] + offset64[1],
219 w0[2] + offset64[2],
220 ];
221 let distance = norm(sample_vector);
222 if !distance.is_finite() || distance <= singularity_radius {
223 return Err(LoweringError::new(
224 PreflightCheck::Singularity,
225 format!(
226 "point source {source_id} sample distance {distance} is at or inside {singularity_radius}"
227 ),
228 ));
229 }
230 let exact_delta = normalized_point_delta_f64(w0, *offset64)?;
231 let lowered_delta = normalized_point_delta_f32(constants, *offset32)?;
232 update_estimate(
233 &mut estimate,
234 inputs.wavenumber,
235 k32,
236 exact_delta,
237 lowered_delta,
238 );
239 let gain_denominator = 1.0_f32 + constants.rho * lowered_delta;
240 if !gain_denominator.is_finite() || gain_denominator <= 0.0 {
241 return Err(LoweringError::new(
242 PreflightCheck::Denominator,
243 format!("point source {source_id} gain denominator is {gain_denominator}"),
244 ));
245 }
246 }
247 admit_estimate(source_id, estimate, inputs.budget)?;
248 Ok(estimate)
249}
250
251fn plane_estimate(
252 source_id: &str,
253 signed_distance0_m: f64,
254 direction64: [f64; 3],
255 direction32: [f32; 3],
256 inputs: EstimateInputs<'_>,
257) -> Result<SourcePhaseEstimate, LoweringError> {
258 let mut estimate = SourcePhaseEstimate::default();
259 let k32 = finite_f32("real wavenumber", inputs.wavenumber, true)?;
260 for (offset64, offset32) in inputs.local_f64.iter().zip(inputs.local_f32) {
261 let exact_delta = dot64(direction64, *offset64);
262 let signed_distance = signed_distance0_m + exact_delta;
263 if !signed_distance.is_finite() || signed_distance < 0.0 {
264 return Err(LoweringError::new(
265 PreflightCheck::ForwardPlane,
266 format!("forward plane {source_id} sample signed distance is {signed_distance}"),
267 ));
268 }
269 let lowered_delta = dot32(direction32, *offset32);
270 update_estimate(
271 &mut estimate,
272 inputs.wavenumber,
273 k32,
274 exact_delta,
275 lowered_delta,
276 );
277 }
278 admit_estimate(source_id, estimate, inputs.budget)?;
279 Ok(estimate)
280}
281
282fn update_estimate(
283 estimate: &mut SourcePhaseEstimate,
284 wavenumber: f64,
285 wavenumber32: f32,
286 exact_delta: f64,
287 lowered_delta: f32,
288) {
289 let psi32 = wavenumber32 * lowered_delta;
290 let geometry = wavenumber * (lowered_delta as f64 - exact_delta).abs();
291 let ideal_with_lowered_geometry = wavenumber * lowered_delta as f64;
292 let arithmetic = (psi32 as f64 - ideal_with_lowered_geometry).abs()
293 + 16.0 * f32::EPSILON as f64 * (1.0 + (psi32 as f64).abs());
294 estimate.max_abs_residual_phase_rad = estimate
295 .max_abs_residual_phase_rad
296 .max((psi32 as f64).abs());
297 estimate.max_predicted_geometry_error_rad =
298 estimate.max_predicted_geometry_error_rad.max(geometry);
299 estimate.max_predicted_roundoff_error_rad =
300 estimate.max_predicted_roundoff_error_rad.max(arithmetic);
301}
302
303fn admit_estimate(
304 source_id: &str,
305 estimate: SourcePhaseEstimate,
306 budget: PhaseBudget,
307) -> Result<(), LoweringError> {
308 for (check, name, predicted, limit) in [
309 (
310 PreflightCheck::PhaseBudget,
311 "absolute residual phase",
312 estimate.max_abs_residual_phase_rad,
313 budget.max_abs_residual_phase_rad,
314 ),
315 (
316 PreflightCheck::GeometryBudget,
317 "predicted geometry error",
318 estimate.max_predicted_geometry_error_rad,
319 budget.max_predicted_geometry_error_rad,
320 ),
321 (
322 PreflightCheck::RoundoffBudget,
323 "predicted roundoff error",
324 estimate.max_predicted_roundoff_error_rad,
325 budget.max_predicted_roundoff_error_rad,
326 ),
327 ] {
328 if !predicted.is_finite() || predicted > limit * (1.0 + 8.0 * f64::EPSILON) {
329 return Err(LoweringError::new(
330 check,
331 format!("source {source_id} {name} {predicted} exceeds {limit}"),
332 ));
333 }
334 }
335 Ok(())
336}
337
338fn normalized_point_delta_f64(w0: [f64; 3], offset: [f64; 3]) -> Result<f64, LoweringError> {
339 let r0 = norm(w0);
340 let rho = 1.0 / r0;
341 let n0 = [w0[0] * rho, w0[1] * rho, w0[2] * rho];
342 let a = dot64(n0, offset);
343 let b = dot64(offset, offset);
344 let z = 2.0 * a * rho + b * rho * rho;
345 let denominator = (1.0 + z).sqrt() + 1.0;
346 if !denominator.is_finite() || denominator <= 0.0 {
347 return Err(LoweringError::new(
348 PreflightCheck::Denominator,
349 format!("host normalized-distance denominator is {denominator}"),
350 ));
351 }
352 Ok((2.0 * a + b * rho) / denominator)
353}
354
355fn normalized_point_delta_f32(
356 constants: PointTileConstants,
357 offset: [f32; 3],
358) -> Result<f32, LoweringError> {
359 let a = dot32(constants.n0, offset);
360 let b = dot32(offset, offset);
361 let z = (2.0 * a) * constants.rho + (b * constants.rho) * constants.rho;
362 let sqrt_argument = 1.0 + z;
363 if !sqrt_argument.is_finite() || sqrt_argument < 0.0 {
364 return Err(LoweringError::new(
365 PreflightCheck::Denominator,
366 format!("normalized-distance sqrt argument is {sqrt_argument}"),
367 ));
368 }
369 let denominator = sqrt_argument.sqrt() + 1.0;
370 if !denominator.is_finite() || denominator <= 0.0 {
371 return Err(LoweringError::new(
372 PreflightCheck::Denominator,
373 format!("normalized-distance denominator is {denominator}"),
374 ));
375 }
376 let delta = ((2.0 * a) + b * constants.rho) / denominator;
377 if !delta.is_finite() {
378 return Err(LoweringError::new(
379 PreflightCheck::Denominator,
380 "normalized-distance residual is not finite",
381 ));
382 }
383 Ok(delta)
384}
385
386fn point_gain(amplitude: f64, alpha: f64, distance: f64) -> Result<f64, LoweringError> {
387 let attenuation = attenuation(alpha, distance)?;
388 let gain = amplitude * POINT_SOURCE_REFERENCE_DISTANCE_METRES * attenuation / distance;
389 if gain.is_finite() {
390 Ok(gain)
391 } else {
392 Err(LoweringError::new(
393 PreflightCheck::Constant,
394 "point center gain is not finite",
395 ))
396 }
397}
398
399fn plane_gain(amplitude: f64, alpha: f64, distance: f64) -> Result<f64, LoweringError> {
400 if distance < 0.0 {
401 return Err(LoweringError::new(
402 PreflightCheck::ForwardPlane,
403 format!("forward-plane center signed distance is {distance}"),
404 ));
405 }
406 let gain = amplitude * attenuation(alpha, distance)?;
407 if gain.is_finite() {
408 Ok(gain)
409 } else {
410 Err(LoweringError::new(
411 PreflightCheck::Constant,
412 "plane center gain is not finite",
413 ))
414 }
415}
416
417fn attenuation(alpha: f64, distance: f64) -> Result<f64, LoweringError> {
418 let exponent = alpha * distance;
419 if exponent.is_infinite() && exponent.is_sign_positive() {
420 Ok(0.0)
421 } else if exponent.is_finite() {
422 Ok((-exponent).exp())
423 } else {
424 Err(LoweringError::new(
425 PreflightCheck::Constant,
426 "attenuation exponent is invalid",
427 ))
428 }
429}
430
431fn finite_f32(name: &str, value: f64, preserve_nonzero: bool) -> Result<f32, LoweringError> {
432 let lowered = value as f32;
433 if !value.is_finite()
434 || !lowered.is_finite()
435 || (preserve_nonzero && value != 0.0 && lowered == 0.0)
436 {
437 Err(LoweringError::new(
438 PreflightCheck::Constant,
439 format!("{name} value {value} is not safely representable as f32"),
440 ))
441 } else {
442 Ok(lowered)
443 }
444}
445
446fn norm(vector: [f64; 3]) -> f64 {
447 vector[0].hypot(vector[1]).hypot(vector[2])
448}
449
450fn dot64(left: [f64; 3], right: [f64; 3]) -> f64 {
451 left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
452}
453
454fn dot32(left: [f32; 3], right: [f32; 3]) -> f32 {
455 left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
456}