Skip to main content

optirs_learned/transformer_based_optimizer/
positional_encoding.rs

1// Positional encoding implementations for transformer sequences
2
3use crate::error::{OptimError, Result};
4use scirs2_core::ndarray::{Array1, Array2};
5use scirs2_core::numeric::Float;
6use serde::{Deserialize, Serialize};
7use std::fmt::Debug;
8
9/// Angular frequency of dimension `i` for a `model_dimension`-wide sinusoidal
10/// encoding with the given base: `1 / base^(2 * floor(i / 2) / d)`.
11///
12/// The `floor(i / 2)` makes `(2k, 2k+1)` a real `(sin, cos)` pair at one shared
13/// frequency, as in Vaswani et al. (2017).
14fn inverse_frequency(i: usize, model_dimension: usize, base: f64) -> f64 {
15    let exponent = 2.0 * ((i / 2) as f64) / model_dimension.max(1) as f64;
16    1.0 / base.powf(exponent)
17}
18
19/// Positional encoding types
20#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
21pub enum PositionalEncodingType {
22    /// Sinusoidal positional encoding (original Transformer)
23    Sinusoidal,
24    /// Learned positional encoding
25    Learned,
26    /// Rotary Position Embedding (RoPE)
27    Rotary,
28    /// No positional encoding
29    None,
30}
31
32/// Positional encoding implementation
33pub struct PositionalEncoding<
34    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
35> {
36    /// Type of positional encoding
37    encoding_type: PositionalEncodingType,
38
39    /// Maximum sequence length
40    max_sequence_length: usize,
41
42    /// Model dimension
43    model_dimension: usize,
44
45    /// Precomputed encoding matrix
46    encoding_matrix: Array2<T>,
47
48    /// Learned parameters (for learned encoding)
49    learned_embeddings: Option<Array2<T>>,
50
51    /// RoPE frequency base
52    rope_base: T,
53}
54
55impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
56    PositionalEncoding<T>
57{
58    /// Create new positional encoding
59    pub fn new(
60        max_sequence_length: usize,
61        model_dimension: usize,
62        encoding_type: PositionalEncodingType,
63    ) -> Result<Self> {
64        let rope_base = scirs2_core::numeric::NumCast::from(10000.0).unwrap_or_else(|| T::zero());
65        let mut encoding = Self {
66            encoding_type,
67            max_sequence_length,
68            model_dimension,
69            encoding_matrix: Array2::zeros((max_sequence_length, model_dimension)),
70            learned_embeddings: None,
71            rope_base,
72        };
73
74        encoding.initialize_encoding()?;
75        Ok(encoding)
76    }
77
78    /// Initialize the encoding based on type
79    fn initialize_encoding(&mut self) -> Result<()> {
80        match self.encoding_type {
81            PositionalEncodingType::Sinusoidal => self.initialize_sinusoidal(),
82            PositionalEncodingType::Learned => self.initialize_learned(),
83            PositionalEncodingType::Rotary => self.initialize_rotary(),
84            PositionalEncodingType::None => Ok(()),
85        }
86    }
87
88    /// Initialize sinusoidal positional encoding
89    fn initialize_sinusoidal(&mut self) -> Result<()> {
90        let base = self.rope_base.to_f64().unwrap_or(10000.0);
91        self.fill_sinusoidal(base);
92        Ok(())
93    }
94
95    /// Fill the encoding matrix with a sinusoidal table for the given base.
96    fn fill_sinusoidal(&mut self, base: f64) {
97        for pos in 0..self.max_sequence_length {
98            for i in 0..self.model_dimension {
99                let angle = pos as f64 * inverse_frequency(i, self.model_dimension, base);
100                let value = if i % 2 == 0 { angle.sin() } else { angle.cos() };
101                self.encoding_matrix[[pos, i]] =
102                    scirs2_core::numeric::NumCast::from(value).unwrap_or_else(|| T::zero());
103            }
104        }
105    }
106
107    /// Initialize learned positional encoding.
108    ///
109    /// Embeddings are seeded from the sinusoidal table rather than from zeros,
110    /// so an untrained learned encoding still carries position information
111    /// instead of collapsing every position onto the same (zero) vector.
112    fn initialize_learned(&mut self) -> Result<()> {
113        let base = self.rope_base.to_f64().unwrap_or(10000.0);
114        self.fill_sinusoidal(base);
115        self.learned_embeddings = Some(self.encoding_matrix.clone());
116        Ok(())
117    }
118
119    /// Initialize rotary positional encoding.
120    ///
121    /// RoPE has no additive table; row 0 of the encoding matrix caches the
122    /// inverse frequency shared by each `(2k, 2k+1)` coordinate pair.
123    fn initialize_rotary(&mut self) -> Result<()> {
124        let base = self.rope_base.to_f64().unwrap_or(10000.0);
125        self.encoding_matrix.fill(T::zero());
126        for i in (0..self.model_dimension).step_by(2) {
127            let freq: T = scirs2_core::numeric::NumCast::from(inverse_frequency(
128                i,
129                self.model_dimension,
130                base,
131            ))
132            .unwrap_or_else(|| T::zero());
133            self.encoding_matrix[[0, i]] = freq;
134            if i + 1 < self.model_dimension {
135                self.encoding_matrix[[0, i + 1]] = freq;
136            }
137        }
138        Ok(())
139    }
140
141    /// Validate that `input` is a `(sequence_length, model_dimension)` matrix
142    /// that fits within the configured maximum sequence length.
143    fn validate_input(&self, input: &Array2<T>) -> Result<(usize, usize)> {
144        let (sequence_length, model_dimension) = input.dim();
145        if model_dimension != self.model_dimension {
146            return Err(OptimError::InvalidConfig(format!(
147                "Input width {model_dimension} does not match the model dimension {}",
148                self.model_dimension
149            )));
150        }
151        if sequence_length > self.max_sequence_length {
152            return Err(OptimError::InvalidConfig(format!(
153                "Sequence length {sequence_length} exceeds the maximum {}",
154                self.max_sequence_length
155            )));
156        }
157        Ok((sequence_length, model_dimension))
158    }
159
160    /// Apply positional encoding to input
161    pub fn encode(&self, input: &Array2<T>) -> Result<Array2<T>> {
162        match self.encoding_type {
163            PositionalEncodingType::None => Ok(input.clone()),
164            PositionalEncodingType::Sinusoidal => self.apply_sinusoidal(input),
165            PositionalEncodingType::Learned => self.apply_learned(input),
166            PositionalEncodingType::Rotary => self.apply_rotary(input),
167        }
168    }
169
170    /// Apply sinusoidal encoding elementwise: `output[pos, dim] += table[pos, dim]`.
171    fn apply_sinusoidal(&self, input: &Array2<T>) -> Result<Array2<T>> {
172        let (sequence_length, model_dimension) = self.validate_input(input)?;
173        let mut output = input.clone();
174
175        for pos in 0..sequence_length {
176            for dim in 0..model_dimension {
177                output[[pos, dim]] = output[[pos, dim]] + self.encoding_matrix[[pos, dim]];
178            }
179        }
180
181        Ok(output)
182    }
183
184    /// Apply learned encoding elementwise.
185    fn apply_learned(&self, input: &Array2<T>) -> Result<Array2<T>> {
186        let Some(ref learned) = self.learned_embeddings else {
187            return Err(OptimError::InvalidState(
188                "Learned embeddings not initialized".to_string(),
189            ));
190        };
191
192        let (sequence_length, model_dimension) = self.validate_input(input)?;
193        let mut output = input.clone();
194
195        for pos in 0..sequence_length {
196            for dim in 0..model_dimension {
197                output[[pos, dim]] = output[[pos, dim]] + learned[[pos, dim]];
198            }
199        }
200
201        Ok(output)
202    }
203
204    /// Apply rotary position embedding.
205    ///
206    /// Each `(2k, 2k+1)` coordinate pair of every row is rotated by
207    /// `theta = position * inv_freq_k`:
208    /// `x' = x cos(theta) - y sin(theta)`, `y' = x sin(theta) + y cos(theta)`.
209    /// This is a genuine rotation, so it preserves the norm of every pair.
210    fn apply_rotary(&self, input: &Array2<T>) -> Result<Array2<T>> {
211        let (sequence_length, model_dimension) = self.validate_input(input)?;
212        let mut output = input.clone();
213
214        for pos in 0..sequence_length {
215            let position: T =
216                scirs2_core::numeric::NumCast::from(pos as f64).unwrap_or_else(|| T::zero());
217
218            for i in (0..model_dimension.saturating_sub(1)).step_by(2) {
219                let freq = self.encoding_matrix[[0, i]];
220                let angle = position * freq;
221                let cos_val = angle.cos();
222                let sin_val = angle.sin();
223
224                let x = input[[pos, i]];
225                let y = input[[pos, i + 1]];
226
227                output[[pos, i]] = x * cos_val - y * sin_val;
228                output[[pos, i + 1]] = x * sin_val + y * cos_val;
229            }
230        }
231
232        Ok(output)
233    }
234
235    /// Get encoding for specific position
236    pub fn get_position_encoding(&self, position: usize) -> Result<Array1<T>> {
237        if position >= self.max_sequence_length {
238            return Err(crate::error::OptimError::Other(
239                "Position exceeds maximum sequence length".to_string(),
240            ));
241        }
242
243        match self.encoding_type {
244            PositionalEncodingType::Sinusoidal => Ok(self.encoding_matrix.row(position).to_owned()),
245            PositionalEncodingType::Learned => {
246                if let Some(ref learned) = self.learned_embeddings {
247                    Ok(learned.row(position).to_owned())
248                } else {
249                    Err(crate::error::OptimError::Other(
250                        "Learned embeddings not available".to_string(),
251                    ))
252                }
253            }
254            PositionalEncodingType::Rotary => {
255                // Return frequency information for RoPE
256                Ok(self.encoding_matrix.row(0).to_owned())
257            }
258            PositionalEncodingType::None => Ok(Array1::zeros(self.model_dimension)),
259        }
260    }
261
262    /// Update learned embeddings (for training)
263    pub fn update_learned_embeddings(&mut self, gradients: &Array2<T>) -> Result<()> {
264        let Some(ref mut learned) = self.learned_embeddings else {
265            return Err(OptimError::InvalidState(
266                "No learned embeddings to update".to_string(),
267            ));
268        };
269        if learned.dim() != gradients.dim() {
270            return Err(OptimError::InvalidConfig(format!(
271                "Gradient shape {:?} does not match the embedding shape {:?}",
272                gradients.dim(),
273                learned.dim()
274            )));
275        }
276        *learned = &*learned - gradients;
277        Ok(())
278    }
279
280    /// Get parameter count
281    pub fn parameter_count(&self) -> usize {
282        match self.encoding_type {
283            PositionalEncodingType::Learned => self.max_sequence_length * self.model_dimension,
284            _ => 0, // Sinusoidal, RoPE, and None have no learnable parameters
285        }
286    }
287
288    /// Reset parameters
289    pub fn reset(&mut self) -> Result<()> {
290        self.initialize_encoding()
291    }
292
293    /// Get encoding type
294    pub fn get_encoding_type(&self) -> PositionalEncodingType {
295        self.encoding_type
296    }
297
298    /// Get maximum sequence length
299    pub fn get_max_sequence_length(&self) -> usize {
300        self.max_sequence_length
301    }
302
303    /// Get model dimension
304    pub fn get_model_dimension(&self) -> usize {
305        self.model_dimension
306    }
307
308    /// Create sinusoidal encoding with custom base
309    pub fn sinusoidal_with_base(
310        max_sequence_length: usize,
311        model_dimension: usize,
312        base: T,
313    ) -> Result<Self> {
314        let mut encoding = Self::new(
315            max_sequence_length,
316            model_dimension,
317            PositionalEncodingType::Sinusoidal,
318        )?;
319
320        // Reinitialize with custom base
321        encoding.rope_base = base;
322        encoding.fill_sinusoidal(base.to_f64().unwrap_or(10000.0));
323
324        Ok(encoding)
325    }
326
327    /// Create rotary encoding with custom base
328    pub fn rotary_with_base(
329        max_sequence_length: usize,
330        model_dimension: usize,
331        base: T,
332    ) -> Result<Self> {
333        let mut encoding = Self::new(
334            max_sequence_length,
335            model_dimension,
336            PositionalEncodingType::Rotary,
337        )?;
338
339        encoding.rope_base = base;
340        encoding.initialize_rotary()?;
341
342        Ok(encoding)
343    }
344}
345
346/// Relative positional encoding for local attention patterns
347pub struct RelativePositionalEncoding<
348    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
349> {
350    /// Maximum relative distance
351    max_relative_distance: usize,
352
353    /// Model dimension
354    model_dimension: usize,
355
356    /// Relative encoding table
357    relative_encoding: Array2<T>,
358}
359
360impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
361    RelativePositionalEncoding<T>
362{
363    /// Create new relative positional encoding
364    pub fn new(max_relative_distance: usize, model_dimension: usize) -> Result<Self> {
365        let table_size = 2 * max_relative_distance + 1;
366        let relative_encoding = Array2::zeros((table_size, model_dimension));
367
368        Ok(Self {
369            max_relative_distance,
370            model_dimension,
371            relative_encoding,
372        })
373    }
374
375    /// Get relative encoding between two positions
376    pub fn get_relative_encoding(&self, from_pos: usize, to_pos: usize) -> Array1<T> {
377        let relative_distance = (to_pos as i32 - from_pos as i32)
378            .max(-(self.max_relative_distance as i32))
379            .min(self.max_relative_distance as i32);
380
381        let index = (relative_distance + self.max_relative_distance as i32) as usize;
382        self.relative_encoding.row(index).to_owned()
383    }
384
385    /// Initialize with sinusoidal patterns
386    pub fn initialize_sinusoidal(&mut self) -> Result<()> {
387        let table_size = 2 * self.max_relative_distance + 1;
388
389        for i in 0..table_size {
390            let relative_pos = i as i32 - self.max_relative_distance as i32;
391            let position =
392                scirs2_core::numeric::NumCast::from(relative_pos).unwrap_or_else(|| T::zero());
393
394            let _ = position;
395            for j in 0..self.model_dimension {
396                let angle =
397                    relative_pos as f64 * inverse_frequency(j, self.model_dimension, 10000.0);
398                let value = if j % 2 == 0 { angle.sin() } else { angle.cos() };
399                self.relative_encoding[[i, j]] =
400                    scirs2_core::numeric::NumCast::from(value).unwrap_or_else(|| T::zero());
401            }
402        }
403
404        Ok(())
405    }
406
407    /// Get parameter count
408    pub fn parameter_count(&self) -> usize {
409        (2 * self.max_relative_distance + 1) * self.model_dimension
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    #[test]
418    fn sinusoidal_pairs_share_a_frequency() {
419        let pe = PositionalEncoding::<f64>::new(64, 8, PositionalEncodingType::Sinusoidal)
420            .expect("encoding creation");
421        let row = pe.get_position_encoding(3).expect("position encoding");
422
423        // d = 8; the pair (2, 3) shares the frequency 1 / 10000^(2/8).
424        let omega = 1.0 / 10000.0_f64.powf(2.0 * 1.0 / 8.0);
425        assert!((row[2] - (3.0 * omega).sin()).abs() < 1e-12);
426        assert!((row[3] - (3.0 * omega).cos()).abs() < 1e-12);
427        assert!((row[2] * row[2] + row[3] * row[3] - 1.0).abs() < 1e-12);
428    }
429
430    #[test]
431    fn position_zero_alternates_zero_and_one() {
432        let pe = PositionalEncoding::<f64>::new(16, 8, PositionalEncodingType::Sinusoidal)
433            .expect("encoding creation");
434        let row = pe.get_position_encoding(0).expect("position encoding");
435        for i in 0..8 {
436            let expected = if i % 2 == 0 { 0.0 } else { 1.0 };
437            assert!((row[i] - expected).abs() < 1e-12, "dim {i} was {}", row[i]);
438        }
439    }
440
441    #[test]
442    fn sinusoidal_encoding_adds_per_cell_not_per_row() {
443        let pe = PositionalEncoding::<f64>::new(16, 8, PositionalEncodingType::Sinusoidal)
444            .expect("encoding creation");
445        let input = Array2::<f64>::zeros((4, 8));
446        let encoded = pe.encode(&input).expect("encode");
447        assert_eq!(encoded.dim(), (4, 8));
448
449        for pos in 0..4 {
450            let reference = pe.get_position_encoding(pos).expect("position encoding");
451            for dim in 0..8 {
452                assert!(
453                    (encoded[[pos, dim]] - reference[dim]).abs() < 1e-12,
454                    "cell ({pos}, {dim}) was {}",
455                    encoded[[pos, dim]]
456                );
457            }
458        }
459        // Distinct positions must produce distinct rows.
460        assert!(encoded.row(0) != encoded.row(1));
461    }
462
463    #[test]
464    fn wrong_width_is_rejected() {
465        let pe = PositionalEncoding::<f64>::new(16, 8, PositionalEncodingType::Sinusoidal)
466            .expect("encoding creation");
467        assert!(pe.encode(&Array2::<f64>::zeros((4, 5))).is_err());
468        assert!(pe.encode(&Array2::<f64>::zeros((32, 8))).is_err());
469    }
470
471    #[test]
472    fn rotary_encoding_is_a_rotation() {
473        let pe = PositionalEncoding::<f64>::new(16, 4, PositionalEncodingType::Rotary)
474            .expect("encoding creation");
475        let input = Array2::<f64>::from_shape_vec(
476            (3, 4),
477            vec![
478                1.0, 0.0, 0.0, 1.0, //
479                2.0, 1.0, -1.0, 3.0, //
480                0.5, 0.5, 0.5, 0.5,
481            ],
482        )
483        .expect("valid shape");
484
485        let output = pe.encode(&input).expect("encode");
486        assert_eq!(output.dim(), (3, 4));
487
488        for pos in 0..3 {
489            for pair in 0..2 {
490                let before = input[[pos, 2 * pair]].powi(2) + input[[pos, 2 * pair + 1]].powi(2);
491                let after = output[[pos, 2 * pair]].powi(2) + output[[pos, 2 * pair + 1]].powi(2);
492                assert!(
493                    (before - after).abs() < 1e-10,
494                    "pair {pair} at position {pos} changed norm: {before} -> {after}"
495                );
496            }
497        }
498
499        // Position 0 is the identity rotation.
500        for dim in 0..4 {
501            assert!((output[[0, dim]] - input[[0, dim]]).abs() < 1e-12);
502        }
503        // Later positions really rotate.
504        assert!((output[[2, 0]] - input[[2, 0]]).abs() > 1e-9);
505    }
506
507    #[test]
508    fn rotary_matches_hand_computed_values() {
509        let pe = PositionalEncoding::<f64>::new(16, 2, PositionalEncodingType::Rotary)
510            .expect("encoding creation");
511        // d = 2 => the single pair has inverse frequency 1.
512        let input =
513            Array2::<f64>::from_shape_vec((2, 2), vec![1.0, 0.0, 1.0, 0.0]).expect("valid shape");
514        let output = pe.encode(&input).expect("encode");
515        assert!((output[[1, 0]] - 1.0_f64.cos()).abs() < 1e-12);
516        assert!((output[[1, 1]] - 1.0_f64.sin()).abs() < 1e-12);
517    }
518
519    #[test]
520    fn learned_encoding_starts_position_aware() {
521        let pe = PositionalEncoding::<f64>::new(32, 8, PositionalEncodingType::Learned)
522            .expect("encoding creation");
523        assert_eq!(pe.parameter_count(), 32 * 8);
524        let a = pe.get_position_encoding(1).expect("position encoding");
525        let b = pe.get_position_encoding(2).expect("position encoding");
526        assert!(a != b, "learned embeddings collapsed onto one vector");
527    }
528
529    #[test]
530    fn no_encoding_is_the_identity() {
531        let pe = PositionalEncoding::<f64>::new(16, 4, PositionalEncodingType::None)
532            .expect("encoding creation");
533        let input = Array2::<f64>::from_elem((3, 4), 0.25);
534        assert_eq!(pe.encode(&input).expect("encode"), input);
535    }
536
537    #[test]
538    fn relative_encoding_pairs_share_a_frequency() {
539        let mut rel = RelativePositionalEncoding::<f64>::new(8, 8).expect("creation");
540        rel.initialize_sinusoidal().expect("initialization");
541        let row = rel.get_relative_encoding(0, 3);
542        let omega = 1.0 / 10000.0_f64.powf(2.0 * 1.0 / 8.0);
543        assert!((row[2] - (3.0 * omega).sin()).abs() < 1e-12);
544        assert!((row[3] - (3.0 * omega).cos()).abs() < 1e-12);
545        assert_eq!(rel.parameter_count(), 17 * 8);
546    }
547
548    #[test]
549    fn all_encoding_types_construct() {
550        for encoding_type in [
551            PositionalEncodingType::Sinusoidal,
552            PositionalEncodingType::Learned,
553            PositionalEncodingType::Rotary,
554            PositionalEncodingType::None,
555        ] {
556            let pe = PositionalEncoding::<f64>::new(50, 32, encoding_type);
557            assert!(pe.is_ok(), "{encoding_type:?} failed to construct");
558        }
559    }
560}