sim-lib-interference-solve 0.1.0

Deterministic CPU f64 reference solving for coherent scalar wave fields.
Documentation
use sim_lib_interference_core::{
    Emitter, FieldAmplitude, Hertz, MetresPerSecond, NepersPerMetre, Point3M, PositiveMetres,
    Radians, ScalarMedium, UnitVector3,
};

use super::{
    AperturePolicy, STRICT_MAX_SPACING_WAVELENGTHS, ScenarioBuilder, ScenarioError, ScenarioKind,
    ScenarioLimits,
};

fn point(x: f64, y: f64, z: f64) -> Point3M {
    Point3M::from_metres(x, y, z).unwrap()
}

fn builder(limits: ScenarioLimits) -> ScenarioBuilder {
    ScenarioBuilder::new(
        Hertz::new(2.0).unwrap(),
        ScalarMedium::new(
            MetresPerSecond::new(4.0).unwrap(),
            NepersPerMetre::new(0.0).unwrap(),
        ),
        PositiveMetres::new(0.01).unwrap(),
        limits,
    )
}

fn x_axis() -> UnitVector3 {
    UnitVector3::new(1.0, 0.0, 0.0).unwrap()
}

fn y_axis() -> UnitVector3 {
    UnitVector3::new(0.0, 1.0, 0.0).unwrap()
}

#[test]
fn two_point_and_counter_plane_builders_are_canonical_and_named() {
    let two_point = builder(ScenarioLimits::default())
        .two_point(
            point(-1.0, 0.0, 0.0),
            point(1.0, 0.0, 0.0),
            FieldAmplitude::new(2.0).unwrap(),
            Radians::new(std::f64::consts::PI).unwrap(),
        )
        .unwrap();
    assert_eq!(two_point.certificate().kind, ScenarioKind::TwoPoint);
    assert_eq!(two_point.certificate().source_count, 2);
    assert_eq!(
        two_point
            .problem()
            .sources
            .iter()
            .map(Emitter::id)
            .collect::<Vec<_>>(),
        ["scenario/two-point/0", "scenario/two-point/1"]
    );

    let planes = builder(ScenarioLimits::default())
        .counter_propagating_planes(
            point(0.0, 0.0, 0.0),
            x_axis(),
            PositiveMetres::new(4.0).unwrap(),
            FieldAmplitude::new(1.0).unwrap(),
            Radians::new(0.5).unwrap(),
        )
        .unwrap();
    assert_eq!(
        planes.certificate().kind,
        ScenarioKind::CounterPropagatingPlanes
    );
    let sources = planes.problem().sources.as_slice();
    let Emitter::ForwardPlane {
        through: first,
        direction: first_direction,
        ..
    } = &sources[0]
    else {
        panic!("first source must be a plane");
    };
    let Emitter::ForwardPlane {
        through: second,
        direction: second_direction,
        ..
    } = &sources[1]
    else {
        panic!("second source must be a plane");
    };
    assert_eq!(first.coordinates_metres(), [-2.0, 0.0, 0.0]);
    assert_eq!(second.coordinates_metres(), [2.0, 0.0, 0.0]);
    assert_eq!(first_direction.components(), [1.0, 0.0, 0.0]);
    assert_eq!(second_direction.components(), [-1.0, 0.0, 0.0]);
}

#[test]
fn array_and_aperture_normalize_total_amplitude_and_report_spacing() {
    let array = builder(ScenarioLimits::default())
        .phased_array(
            point(0.0, 0.0, 0.0),
            x_axis(),
            4,
            PositiveMetres::new(1.0).unwrap(),
            FieldAmplitude::new(8.0).unwrap(),
            Radians::new(0.0).unwrap(),
            Radians::new(0.25).unwrap(),
            AperturePolicy::Strict,
        )
        .unwrap();
    let certificate = array.certificate();
    assert_eq!(certificate.kind, ScenarioKind::PhasedArray);
    assert_eq!(certificate.total_source_amplitude, 8.0);
    assert_eq!(certificate.amplitude_per_source, 2.0);
    assert_eq!(certificate.element_spacing_wavelengths.u, Some(0.5));
    assert_eq!(certificate.element_spacing_wavelengths.v, None);
    assert!(array.problem().sources.iter().all(|source| {
        matches!(
            source,
            Emitter::Point {
                amplitude_at_reference,
                ..
            } if amplitude_at_reference.get() == 2.0
        )
    }));

    let aperture = builder(ScenarioLimits::default())
        .discrete_aperture(
            point(0.0, 0.0, 0.0),
            x_axis(),
            y_axis(),
            2,
            3,
            PositiveMetres::new(0.5).unwrap(),
            PositiveMetres::new(1.0).unwrap(),
            FieldAmplitude::new(12.0).unwrap(),
            Radians::new(0.0).unwrap(),
            AperturePolicy::Strict,
        )
        .unwrap();
    let certificate = aperture.certificate();
    assert_eq!(certificate.kind, ScenarioKind::DiscreteAperture);
    assert_eq!(certificate.source_count, 6);
    assert_eq!(certificate.amplitude_per_source, 2.0);
    assert_eq!(certificate.element_spacing_wavelengths.u, Some(0.25));
    assert_eq!(certificate.element_spacing_wavelengths.v, Some(0.5));
    assert_eq!(
        aperture
            .problem()
            .sources
            .iter()
            .map(Emitter::id)
            .collect::<Vec<_>>(),
        [
            "scenario/aperture/0",
            "scenario/aperture/1",
            "scenario/aperture/2",
            "scenario/aperture/3",
            "scenario/aperture/4",
            "scenario/aperture/5",
        ]
    );
}

#[test]
fn strict_spacing_refuses_sparse_elements_while_annotate_preserves_truth() {
    let strict = builder(ScenarioLimits::default()).phased_array(
        point(0.0, 0.0, 0.0),
        x_axis(),
        2,
        PositiveMetres::new(1.1).unwrap(),
        FieldAmplitude::new(1.0).unwrap(),
        Radians::new(0.0).unwrap(),
        Radians::new(0.0).unwrap(),
        AperturePolicy::Strict,
    );
    assert_eq!(
        strict,
        Err(ScenarioError::SparseAperture {
            axis: "u",
            spacing_wavelengths: 0.55,
            maximum_wavelengths: STRICT_MAX_SPACING_WAVELENGTHS,
        })
    );

    let annotated = builder(ScenarioLimits::default())
        .phased_array(
            point(0.0, 0.0, 0.0),
            x_axis(),
            2,
            PositiveMetres::new(1.1).unwrap(),
            FieldAmplitude::new(1.0).unwrap(),
            Radians::new(0.0).unwrap(),
            Radians::new(0.0).unwrap(),
            AperturePolicy::Annotate,
        )
        .unwrap();
    assert_eq!(
        annotated.certificate().element_spacing_wavelengths.u,
        Some(0.55)
    );
    assert_eq!(
        annotated.certificate().aperture_policy,
        Some(AperturePolicy::Annotate)
    );

    let singleton = builder(ScenarioLimits::default())
        .phased_array(
            point(0.0, 0.0, 0.0),
            x_axis(),
            1,
            PositiveMetres::new(f64::MAX).unwrap(),
            FieldAmplitude::new(1.0).unwrap(),
            Radians::new(0.0).unwrap(),
            Radians::new(0.0).unwrap(),
            AperturePolicy::Strict,
        )
        .unwrap();
    assert_eq!(
        singleton
            .certificate()
            .element_spacing_wavelengths
            .maximum(),
        None
    );
}

#[test]
fn count_and_generated_identity_limits_fail_before_construction() {
    let count_limits = ScenarioLimits::new(4, 96, 1_024).unwrap();
    assert_eq!(
        builder(count_limits).discrete_aperture(
            point(0.0, 0.0, 0.0),
            x_axis(),
            y_axis(),
            2,
            3,
            PositiveMetres::new(0.1).unwrap(),
            PositiveMetres::new(0.1).unwrap(),
            FieldAmplitude::new(1.0).unwrap(),
            Radians::new(0.0).unwrap(),
            AperturePolicy::Strict,
        ),
        Err(ScenarioError::SourceLimitExceeded {
            requested: 6,
            limit: 4,
        })
    );

    let id_limits = ScenarioLimits::new(8, 19, 1_024).unwrap();
    assert_eq!(
        builder(id_limits).two_point(
            point(0.0, 0.0, 0.0),
            point(1.0, 0.0, 0.0),
            FieldAmplitude::new(1.0).unwrap(),
            Radians::new(0.0).unwrap(),
        ),
        Err(ScenarioError::GeneratedIdLimitExceeded {
            requested: 20,
            limit: 19,
        })
    );

    let total_limits = ScenarioLimits::new(8, 96, 39).unwrap();
    assert_eq!(
        builder(total_limits).two_point(
            point(0.0, 0.0, 0.0),
            point(1.0, 0.0, 0.0),
            FieldAmplitude::new(1.0).unwrap(),
            Radians::new(0.0).unwrap(),
        ),
        Err(ScenarioError::TotalIdLimitExceeded {
            requested: 40,
            limit: 39,
        })
    );

    assert_eq!(
        builder(ScenarioLimits::default()).discrete_aperture(
            point(0.0, 0.0, 0.0),
            x_axis(),
            y_axis(),
            usize::MAX,
            2,
            PositiveMetres::new(0.1).unwrap(),
            PositiveMetres::new(0.1).unwrap(),
            FieldAmplitude::new(1.0).unwrap(),
            Radians::new(0.0).unwrap(),
            AperturePolicy::Strict,
        ),
        Err(ScenarioError::SourceCountOverflow {
            rows: usize::MAX,
            columns: 2,
        })
    );
}