Skip to main content

sim_lib_interference_solve/
field.rs

1//! Immutable host storage for phasor component planes.
2
3use crate::{ReferenceSolveError, complex::Complex64};
4
5/// A host phasor component buffer was not a valid finite two-dimensional field.
6#[derive(Clone, Debug, PartialEq)]
7pub enum HostPhasorFieldError {
8    /// A component plane dimension was zero.
9    ZeroDimension {
10        /// Rejected dimension.
11        axis: &'static str,
12    },
13    /// The row/column product overflowed or did not match both component lengths.
14    ShapeMismatch {
15        /// Declared row count.
16        rows: usize,
17        /// Declared column count.
18        columns: usize,
19        /// Real component length.
20        real_len: usize,
21        /// Imaginary component length.
22        imaginary_len: usize,
23    },
24    /// One component contained a NaN or infinity.
25    NonFinite {
26        /// Component plane.
27        component: &'static str,
28        /// Row-major cell index.
29        index: usize,
30        /// Rejected value.
31        value: f64,
32    },
33}
34
35/// A complete two-dimensional host phasor field.
36///
37/// Real and imaginary components are stored in separate flat `f64` buffers.
38/// Both buffers use the same row-major index, `row * columns + column`.
39#[derive(Clone, Debug, PartialEq)]
40pub struct HostPhasorField {
41    rows: usize,
42    columns: usize,
43    real: Vec<f64>,
44    imaginary: Vec<f64>,
45}
46
47impl HostPhasorField {
48    /// Admits complete finite row-major component planes.
49    ///
50    /// This is the explicit materialization boundary used by runtime tensor
51    /// adapters. It performs no propagation and takes ownership of both
52    /// buffers without copying them.
53    pub fn from_component_planes(
54        rows: usize,
55        columns: usize,
56        real: Vec<f64>,
57        imaginary: Vec<f64>,
58    ) -> Result<Self, HostPhasorFieldError> {
59        if rows == 0 {
60            return Err(HostPhasorFieldError::ZeroDimension { axis: "rows" });
61        }
62        if columns == 0 {
63            return Err(HostPhasorFieldError::ZeroDimension { axis: "columns" });
64        }
65        let Some(cells) = rows.checked_mul(columns) else {
66            return Err(HostPhasorFieldError::ShapeMismatch {
67                rows,
68                columns,
69                real_len: real.len(),
70                imaginary_len: imaginary.len(),
71            });
72        };
73        if real.len() != cells || imaginary.len() != cells {
74            return Err(HostPhasorFieldError::ShapeMismatch {
75                rows,
76                columns,
77                real_len: real.len(),
78                imaginary_len: imaginary.len(),
79            });
80        }
81        for (component, values) in [("real", &real), ("imaginary", &imaginary)] {
82            if let Some((index, value)) = values
83                .iter()
84                .copied()
85                .enumerate()
86                .find(|(_, value)| !value.is_finite())
87            {
88                return Err(HostPhasorFieldError::NonFinite {
89                    component,
90                    index,
91                    value,
92                });
93            }
94        }
95        Ok(Self {
96            rows,
97            columns,
98            real,
99            imaginary,
100        })
101    }
102
103    pub(crate) fn try_zeroed(rows: usize, columns: usize) -> Result<Self, ReferenceSolveError> {
104        let cells = rows
105            .checked_mul(columns)
106            .expect("SamplingPlane has already checked rows * columns");
107        let real = zeroed_component("real", cells)?;
108        let imaginary = zeroed_component("imaginary", cells)?;
109        Ok(Self {
110            rows,
111            columns,
112            real,
113            imaginary,
114        })
115    }
116
117    pub(crate) fn set_index(&mut self, index: usize, value: Complex64) {
118        let (real, imaginary) = value.components();
119        self.real[index] = real;
120        self.imaginary[index] = imaginary;
121    }
122
123    #[cfg(test)]
124    pub(crate) fn from_test_components(
125        rows: usize,
126        columns: usize,
127        real: Vec<f64>,
128        imaginary: Vec<f64>,
129    ) -> Self {
130        let cells = rows.checked_mul(columns).expect("test shape must fit");
131        assert_eq!(real.len(), cells);
132        assert_eq!(imaginary.len(), cells);
133        assert!(real.iter().chain(&imaginary).all(|value| value.is_finite()));
134        Self {
135            rows,
136            columns,
137            real,
138            imaginary,
139        }
140    }
141
142    /// Returns the number of rows.
143    pub fn rows(&self) -> usize {
144        self.rows
145    }
146
147    /// Returns the number of columns.
148    pub fn columns(&self) -> usize {
149        self.columns
150    }
151
152    /// Returns the number of cells in each component plane.
153    pub fn len(&self) -> usize {
154        self.real.len()
155    }
156
157    /// Returns whether the component planes contain no cells.
158    ///
159    /// Successfully solved fields are never empty because sampling-plane
160    /// construction requires non-zero dimensions.
161    pub fn is_empty(&self) -> bool {
162        self.real.is_empty()
163    }
164
165    /// Borrows the flat row-major real component plane.
166    pub fn real(&self) -> &[f64] {
167        &self.real
168    }
169
170    /// Borrows the flat row-major imaginary component plane.
171    pub fn imaginary(&self) -> &[f64] {
172        &self.imaginary
173    }
174
175    /// Returns one cell's Cartesian components.
176    pub fn cell(&self, row: usize, column: usize) -> Option<(f64, f64)> {
177        let index = row.checked_mul(self.columns)?.checked_add(column)?;
178        (row < self.rows && column < self.columns)
179            .then(|| Complex64::new(self.real[index], self.imaginary[index]).components())
180    }
181
182    /// Consumes the field into its shape and component planes.
183    pub fn into_component_planes(self) -> (usize, usize, Vec<f64>, Vec<f64>) {
184        (self.rows, self.columns, self.real, self.imaginary)
185    }
186}
187
188fn zeroed_component(
189    component: &'static str,
190    cells: usize,
191) -> Result<Vec<f64>, ReferenceSolveError> {
192    let mut values = Vec::new();
193    values
194        .try_reserve_exact(cells)
195        .map_err(|_| ReferenceSolveError::AllocationFailed { component, cells })?;
196    values.resize(cells, 0.0);
197    Ok(values)
198}
199
200#[cfg(test)]
201mod tests {
202    use super::HostPhasorField;
203
204    #[test]
205    fn component_planes_share_one_row_major_shape() {
206        let field = HostPhasorField {
207            rows: 2,
208            columns: 3,
209            real: vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
210            imaginary: vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0],
211        };
212
213        assert_eq!(field.rows(), 2);
214        assert_eq!(field.columns(), 3);
215        assert_eq!(field.len(), 6);
216        assert!(!field.is_empty());
217        assert_eq!(field.cell(1, 2), Some((5.0, 15.0)));
218        assert_eq!(field.cell(2, 0), None);
219        assert_eq!(field.cell(0, 3), None);
220        assert_eq!(field.real(), &[0.0, 1.0, 2.0, 3.0, 4.0, 5.0]);
221        assert_eq!(field.imaginary(), &[10.0, 11.0, 12.0, 13.0, 14.0, 15.0]);
222    }
223}