Skip to main content

sim_lib_interference_compute/
constants.rs

1//! Host f64 phase anchors and source-specific normalized constants.
2
3use sim_lib_interference_core::{
4    Emitter, InterferenceProblem, POINT_SOURCE_REFERENCE_DISTANCE_METRES, Point3M,
5};
6
7use crate::{LoweringError, PhaseBudget, PreflightCheck};
8
9/// Tile-center constants for a spherical point source.
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct PointTileConstants {
12    /// Unit direction from the source to the tile center.
13    pub n0: [f32; 3],
14    /// Reciprocal source-to-center distance.
15    pub rho: f32,
16    /// Host f64 cosine of the absolute phase, rounded once to f32.
17    pub phase_cos: f32,
18    /// Host f64 sine of the absolute phase, rounded once to f32.
19    pub phase_sin: f32,
20    /// Attenuated point-source gain at the tile center.
21    pub gain0: f32,
22}
23
24/// Tile-center constants for a forward plane source.
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct PlaneTileConstants {
27    /// Forward unit direction, rounded once from host f64.
28    pub direction: [f32; 3],
29    /// Host f64 signed distance from the phase plane to the tile center.
30    pub signed_distance0_m: f64,
31    /// Host f64 cosine of the absolute phase, rounded once to f32.
32    pub phase_cos: f32,
33    /// Host f64 sine of the absolute phase, rounded once to f32.
34    pub phase_sin: f32,
35    /// Host f64 attenuated gain at the tile center, rounded once to f32.
36    pub gain0: f32,
37}
38
39/// Predicted phase-domain accuracy for one source on one tile.
40#[derive(Clone, Copy, Debug, Default, PartialEq)]
41pub struct SourcePhaseEstimate {
42    /// Largest absolute f32 residual phase submitted to `sin` or `cos`.
43    pub max_abs_residual_phase_rad: f64,
44    /// Largest phase discrepancy caused by lowering local geometry to f32.
45    pub max_predicted_geometry_error_rad: f64,
46    /// Largest predicted f32 wavenumber, multiply, and operation roundoff.
47    pub max_predicted_roundoff_error_rad: f64,
48}
49
50/// Prepared constants and phase estimate for one source on one tile.
51#[derive(Clone, Copy, Debug, PartialEq)]
52pub enum SourceTileConstants {
53    /// Spherical point-source constants.
54    Point {
55        /// Constants consumed by the Tensor lowering.
56        constants: PointTileConstants,
57        /// Preflight estimate for this source and tile.
58        estimate: SourcePhaseEstimate,
59    },
60    /// Forward plane-wave constants.
61    ForwardPlane {
62        /// Constants consumed by the Tensor lowering.
63        constants: PlaneTileConstants,
64        /// Preflight estimate for this source and tile.
65        estimate: SourcePhaseEstimate,
66    },
67}
68
69impl SourceTileConstants {
70    /// Returns the preflight phase estimate.
71    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}