Skip to main content

hekate_math/fft/
reed_solomon.rs

1// SPDX-License-Identifier: Apache-2.0
2// This file is part of the hekate-math project.
3// Copyright (C) 2026 Andrei Kochergin <andrei@oumuamua.dev>
4// Copyright (C) 2026 Oumuamua Labs <info@oumuamua.dev>.
5//
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10//     http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Systematic Reed–Solomon encoding on the additive FFT.
19
20use super::{AdditiveFft, FftError};
21use crate::{BinaryFieldExtras, Flat, HardwareField, PackedFlat};
22
23/// Error returned by the Reed–Solomon encoder.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum RsError {
27    /// `log_k` is outside `1..log_n`.
28    BadRate {
29        /// The `log_k` passed to `new`.
30        log_k: u32,
31
32        /// The `log_n` passed to `new`.
33        log_n: u32,
34    },
35
36    /// `log_n` exceeds `max_log_n`, the largest size `F` supports.
37    FieldTooSmall {
38        /// The `log_n` passed to `new`.
39        log_n: u32,
40
41        /// The largest `log_n` that `F` supports on this target.
42        max_log_n: u32,
43    },
44
45    /// `msg` or `out` has length `got`; it needs `expected`, k or n.
46    BadLength {
47        /// k for `msg`, n for `out`.
48        expected: usize,
49
50        /// The length of the rejected buffer.
51        got: usize,
52    },
53
54    /// Allocating a 2^log_size-point twiddle table failed.
55    TwiddleAlloc {
56        /// The `log_k` or `log_n` of the failing transform.
57        log_size: u32,
58    },
59}
60
61impl core::fmt::Display for RsError {
62    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
63        match self {
64            RsError::BadRate { log_k, log_n } => {
65                write!(
66                    f,
67                    "ReedSolomon rate: log_k {log_k} must satisfy 1 <= log_k < log_n {log_n}"
68                )
69            }
70            RsError::FieldTooSmall { log_n, max_log_n } => {
71                write!(
72                    f,
73                    "ReedSolomon log_n {log_n} exceeds the maximum {max_log_n} \
74                     = min(field degree, usize::BITS - 1)"
75                )
76            }
77            RsError::BadLength { expected, got } => {
78                write!(f, "ReedSolomon buffer length {got}, expected {expected}")
79            }
80            RsError::TwiddleAlloc { log_size } => {
81                write!(
82                    f,
83                    "ReedSolomon 2^{log_size}-point twiddle table allocation failed"
84                )
85            }
86        }
87    }
88}
89
90impl core::error::Error for RsError {}
91
92impl From<FftError> for RsError {
93    fn from(e: FftError) -> Self {
94        match e {
95            FftError::BadLength { expected, got } => RsError::BadLength { expected, got },
96            FftError::BadLogN { log_n, max } => RsError::FieldTooSmall {
97                log_n,
98                max_log_n: max,
99            },
100            FftError::TwiddleAlloc { log_n } => RsError::TwiddleAlloc { log_size: log_n },
101        }
102    }
103}
104
105/// Systematic Reed–Solomon `[n, k, n-k+1]` encoder
106/// over the additive-FFT subspaces `W_k ⊂ W_n`:
107/// `encode(msg)[..k] == msg` (Lin–Chung–Han 2014).
108pub struct ReedSolomon<F> {
109    fft_k: AdditiveFft<F>,
110    fft_n: AdditiveFft<F>,
111    k: usize,
112    n: usize,
113}
114
115impl<F: BinaryFieldExtras + HardwareField> ReedSolomon<F> {
116    /// Builds the encoder for `k = 2^log_k` message and `n = 2^log_n`
117    /// codeword symbols. The only allocation is the two twiddle schedules.
118    ///
119    /// # Errors
120    /// `BadRate` unless `1 <= log_k < log_n`; `FieldTooSmall`
121    /// if `log_n > min(F::BITS, usize::BITS - 1)`;
122    /// `TwiddleAlloc` if a twiddle table cannot be allocated.
123    pub fn new(log_k: u32, log_n: u32) -> Result<Self, RsError> {
124        if log_k < 1 || log_k >= log_n {
125            return Err(RsError::BadRate { log_k, log_n });
126        }
127
128        let max_log_n = F::BITS.min(usize::BITS as usize - 1) as u32;
129
130        if log_n > max_log_n {
131            return Err(RsError::FieldTooSmall { log_n, max_log_n });
132        }
133
134        Ok(Self {
135            fft_k: AdditiveFft::new(log_k)?,
136            fft_n: AdditiveFft::new(log_n)?,
137            k: 1usize << log_k,
138            n: 1usize << log_n,
139        })
140    }
141
142    /// Returns k, the message length per row.
143    pub fn message_len(&self) -> usize {
144        self.k
145    }
146
147    /// Returns n, the codeword length per row.
148    pub fn codeword_len(&self) -> usize {
149        self.n
150    }
151
152    /// Encodes one row: `out[..k] == msg` on return,
153    /// `out[k..]` the n - k parity symbols.
154    ///
155    /// # Errors
156    /// `BadLength` unless `msg.len() == k` and `out.len() == n`.
157    pub fn encode_scalar(&self, msg: &[Flat<F>], out: &mut [Flat<F>]) -> Result<(), RsError> {
158        self.check(msg.len(), out.len())?;
159
160        out[..self.k].copy_from_slice(msg);
161        out[self.k..].fill(Flat::from_raw(F::ZERO));
162
163        self.fft_k.inverse_scalar(&mut out[..self.k])?;
164        self.fft_n.forward_scalar(out)?;
165
166        Ok(())
167    }
168
169    /// Encodes `F::WIDTH` rows in lockstep, one per lane;
170    /// lengths count packed elements.
171    ///
172    /// # Errors
173    /// `BadLength` unless `msg.len() == k` and `out.len() == n`.
174    pub fn encode(&self, msg: &[PackedFlat<F>], out: &mut [PackedFlat<F>]) -> Result<(), RsError> {
175        self.check(msg.len(), out.len())?;
176
177        out[..self.k].copy_from_slice(msg);
178        out[self.k..].fill(PackedFlat::default());
179
180        self.fft_k.inverse(&mut out[..self.k])?;
181        self.fft_n.forward(out)?;
182
183        Ok(())
184    }
185
186    fn check(&self, msg_len: usize, out_len: usize) -> Result<(), RsError> {
187        if msg_len != self.k {
188            return Err(RsError::BadLength {
189                expected: self.k,
190                got: msg_len,
191            });
192        }
193
194        if out_len != self.n {
195            return Err(RsError::BadLength {
196                expected: self.n,
197                got: out_len,
198            });
199        }
200
201        Ok(())
202    }
203}