Skip to main content

sim_lib_numbers_signal/
plan.rs

1//! Typed transform conventions and canonical signal buffers.
2
3use std::num::NonZeroUsize;
4
5use sim_lib_numbers_tensor_cmplxf::ComplexFTensor;
6use sim_lib_numbers_tensor_f64::F64Tensor;
7
8use crate::SignalError;
9
10/// Direction in which a transform plan is applied.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Direction {
13    /// Map samples to transform coefficients.
14    Forward,
15    /// Map transform coefficients back to samples.
16    Inverse,
17}
18
19/// Scaling convention applied by a forward/inverse transform pair.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Normalization {
22    /// Apply no normalization in either direction.
23    None,
24    /// Normalize only the forward transform by its definition-level factor.
25    Forward,
26    /// Normalize only the inverse transform by its definition-level factor.
27    Inverse,
28    /// Use the orthonormal basis in both directions.
29    Orthonormal,
30}
31
32/// Complex-exponential sign convention.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum SignConvention {
35    /// Forward transforms use `exp(-i theta)` and inverse transforms use
36    /// `exp(+i theta)`.
37    NegativeForward,
38    /// Forward transforms use `exp(+i theta)` and inverse transforms use
39    /// `exp(-i theta)`.
40    PositiveForward,
41}
42
43impl SignConvention {
44    /// Returns the signed angle multiplier for `direction`.
45    pub fn angle_sign(self, direction: Direction) -> f64 {
46        match (self, direction) {
47            (Self::NegativeForward, Direction::Forward)
48            | (Self::PositiveForward, Direction::Inverse) => -1.0,
49            (Self::PositiveForward, Direction::Forward)
50            | (Self::NegativeForward, Direction::Inverse) => 1.0,
51        }
52    }
53}
54
55/// Packing of a real FFT spectrum.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum SpectrumPacking {
58    /// Store all `N` complex frequency bins.
59    Full,
60    /// Store bins `0..=N/2`; omitted bins are their Hermitian mirrors.
61    HermitianHalf,
62}
63
64/// Relationship between the logical plan length and available input values.
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum LengthPolicy {
67    /// Require exactly the plan length.
68    Exact,
69    /// Admit a shorter input and extend it according to [`PaddingPolicy`].
70    Pad,
71    /// Require at least the plan length and ignore later values.
72    Truncate,
73}
74
75/// Values used when [`LengthPolicy::Pad`] extends an input.
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum PaddingPolicy {
78    /// Do not synthesize values.
79    Reject,
80    /// Extend with real or complex zero values.
81    Zero,
82}
83
84/// Logical selection of values from a physical one-dimensional buffer.
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub struct Stride {
87    offset: usize,
88    step: NonZeroUsize,
89}
90
91impl Stride {
92    /// Contiguous selection starting at physical index zero.
93    pub const fn contiguous() -> Self {
94        Self {
95            offset: 0,
96            step: NonZeroUsize::MIN,
97        }
98    }
99
100    /// Builds a selection from `offset` with a nonzero `step`.
101    pub fn new(offset: usize, step: usize) -> Result<Self, SignalError> {
102        let step = NonZeroUsize::new(step).ok_or(SignalError::ZeroStride)?;
103        Ok(Self { offset, step })
104    }
105
106    /// First selected physical index.
107    pub const fn offset(self) -> usize {
108        self.offset
109    }
110
111    /// Distance between consecutive selected physical indices.
112    pub const fn step(self) -> usize {
113        self.step.get()
114    }
115
116    /// Returns the number of values reachable in a physical buffer of `len`.
117    pub fn available(self, len: usize) -> usize {
118        if self.offset >= len {
119            0
120        } else {
121            1 + (len - 1 - self.offset) / self.step()
122        }
123    }
124
125    /// Maps a logical index to a physical index with overflow checking.
126    pub fn physical_index(self, logical: usize) -> Result<usize, SignalError> {
127        logical
128            .checked_mul(self.step())
129            .and_then(|delta| self.offset.checked_add(delta))
130            .ok_or(SignalError::StrideOverflow)
131    }
132}
133
134impl Default for Stride {
135    fn default() -> Self {
136        Self::contiguous()
137    }
138}
139
140/// Whether execution writes a distinct result or overwrites caller storage.
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub enum PlacementPolicy {
143    /// Return a new canonical tensor buffer.
144    OutOfPlace,
145    /// Overwrite a mutable caller slice of the same representation and length.
146    InPlace,
147}
148
149/// One of the four standard discrete cosine transform definitions.
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub enum DctType {
152    /// DCT-I.
153    I,
154    /// DCT-II.
155    II,
156    /// DCT-III.
157    III,
158    /// DCT-IV.
159    IV,
160}
161
162/// One of the four standard discrete sine transform definitions.
163#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164pub enum DstType {
165    /// DST-I.
166    I,
167    /// DST-II.
168    II,
169    /// DST-III.
170    III,
171    /// DST-IV.
172    IV,
173}
174
175/// Mathematical transform selected by a plan.
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub enum TransformKind {
178    /// Direct O(N^2) complex discrete Fourier transform reference.
179    Dft,
180    /// Mixed-radix or Bluestein complex fast Fourier transform.
181    Fft,
182    /// FFT of real samples with explicit full or Hermitian-half packing.
183    RealFft,
184    /// Discrete cosine transform of the selected type.
185    Dct(DctType),
186    /// Discrete sine transform of the selected type.
187    Dst(DstType),
188}
189
190/// Reusable, fully explicit transform plan.
191#[derive(Clone, Debug, PartialEq, Eq)]
192pub struct TransformPlan {
193    /// Transform definition.
194    pub kind: TransformKind,
195    /// Logical transform length.
196    pub len: usize,
197    /// Forward or inverse application.
198    pub direction: Direction,
199    /// Scaling convention.
200    pub normalization: Normalization,
201    /// Complex-exponential sign convention.
202    pub sign: SignConvention,
203    /// Real-spectrum packing.
204    pub packing: SpectrumPacking,
205    /// Length admission policy.
206    pub length: LengthPolicy,
207    /// Padding value policy.
208    pub padding: PaddingPolicy,
209    /// Input selection stride.
210    pub stride: Stride,
211    /// Output placement policy.
212    pub placement: PlacementPolicy,
213}
214
215impl TransformPlan {
216    /// Builds the conventional forward, inverse-normalized, out-of-place plan.
217    pub fn new(kind: TransformKind, len: usize) -> Self {
218        Self {
219            kind,
220            len,
221            direction: Direction::Forward,
222            normalization: Normalization::Inverse,
223            sign: SignConvention::NegativeForward,
224            packing: SpectrumPacking::Full,
225            length: LengthPolicy::Exact,
226            padding: PaddingPolicy::Reject,
227            stride: Stride::contiguous(),
228            placement: PlacementPolicy::OutOfPlace,
229        }
230    }
231
232    /// Validates definition-level length and policy invariants.
233    pub fn validate(&self) -> Result<(), SignalError> {
234        if self.len == 0 {
235            return Err(SignalError::InvalidLength {
236                len: self.len,
237                reason: "transforms require at least one value",
238            });
239        }
240        if self.kind == TransformKind::Dct(DctType::I) && self.len < 2 {
241            return Err(SignalError::InvalidLength {
242                len: self.len,
243                reason: "DCT-I requires at least two values",
244            });
245        }
246        if self.packing != SpectrumPacking::Full && self.kind != TransformKind::RealFft {
247            return Err(SignalError::InvalidPolicy {
248                policy: "packing",
249                reason: "Hermitian-half packing is defined only for real FFT",
250            });
251        }
252        match (self.length, self.padding) {
253            (LengthPolicy::Pad, PaddingPolicy::Zero)
254            | (LengthPolicy::Exact | LengthPolicy::Truncate, PaddingPolicy::Reject) => Ok(()),
255            (LengthPolicy::Pad, PaddingPolicy::Reject) => Err(SignalError::InvalidPolicy {
256                policy: "padding",
257                reason: "Pad length policy requires zero padding",
258            }),
259            (LengthPolicy::Exact | LengthPolicy::Truncate, PaddingPolicy::Zero) => {
260                Err(SignalError::InvalidPolicy {
261                    policy: "padding",
262                    reason: "zero padding requires the Pad length policy",
263                })
264            }
265        }
266    }
267}
268
269/// Borrowed transform input.
270#[derive(Clone, Copy, Debug)]
271pub enum SignalView<'a> {
272    /// Canonical complex cells as `(real, imag)` pairs.
273    Complex(&'a [(f64, f64)]),
274    /// Canonical real cells.
275    Real(&'a [f64]),
276}
277
278impl<'a> SignalView<'a> {
279    /// Borrows the native storage of a canonical complex tensor.
280    pub fn from_complex_tensor(tensor: &'a ComplexFTensor) -> Self {
281        Self::Complex(tensor.as_slice())
282    }
283
284    /// Borrows the native storage of a canonical f64 tensor.
285    pub fn from_real_tensor(tensor: &'a F64Tensor) -> Self {
286        Self::Real(tensor.as_slice())
287    }
288
289    /// Physical number of cells in the borrowed storage.
290    pub fn physical_len(self) -> usize {
291        match self {
292            Self::Complex(values) => values.len(),
293            Self::Real(values) => values.len(),
294        }
295    }
296}
297
298/// Mutable signal storage accepted by in-place transforms.
299#[derive(Debug)]
300pub enum SignalViewMut<'a> {
301    /// Mutable complex `(real, imag)` cells.
302    Complex(&'a mut [(f64, f64)]),
303    /// Mutable real cells.
304    Real(&'a mut [f64]),
305}
306
307/// Owned transform result in canonical tensor storage.
308#[derive(Clone, Debug, PartialEq)]
309pub enum SignalBuffer {
310    /// Complex coefficient or sample tensor.
311    Complex(ComplexFTensor),
312    /// Real coefficient or sample tensor.
313    Real(F64Tensor),
314}
315
316impl SignalBuffer {
317    /// Logical one-dimensional result length.
318    pub fn len(&self) -> usize {
319        match self {
320            Self::Complex(values) => values.as_slice().len(),
321            Self::Real(values) => values.as_slice().len(),
322        }
323    }
324
325    /// Whether the result has no cells.
326    pub fn is_empty(&self) -> bool {
327        self.len() == 0
328    }
329}