Skip to main content

optirs_learned/transformer_based_optimizer/
sequence_processor.rs

1// Sequence processing for optimization trajectories
2
3use super::config::TransformerBasedOptimizerConfig;
4use crate::error::Result;
5use scirs2_core::ndarray::{Array1, Array2};
6use scirs2_core::numeric::Float;
7use std::collections::VecDeque;
8use std::fmt::Debug;
9
10/// Type alias for sequence data tuple (gradients, parameters, losses)
11type SequenceDataTuple<T> = (Vec<Array2<T>>, Vec<Array2<T>>, Vec<Array1<T>>);
12
13/// Sequence processing strategy types
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum SequenceProcessingStrategy {
16    /// Sliding window approach
17    SlidingWindow,
18    /// Hierarchical chunking
19    Hierarchical,
20    /// Attention-based selection
21    AttentionBased,
22    /// Adaptive segmentation
23    Adaptive,
24    /// Truncated backpropagation
25    TruncatedBPTT,
26}
27
28/// Optimization sequence processor
29pub struct OptimizationSequenceProcessor<
30    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
31> {
32    /// Processing strategy
33    strategy: SequenceProcessingStrategy,
34
35    /// Maximum sequence length
36    max_sequence_length: usize,
37
38    /// Window size for sliding window
39    window_size: usize,
40
41    /// Overlap between windows
42    window_overlap: usize,
43
44    /// Sequence history buffer
45    sequence_buffer: SequenceBuffer<T>,
46
47    /// Sequence statistics
48    statistics: SequenceStatistics<T>,
49
50    /// Preprocessing pipeline
51    preprocessor: SequencePreprocessor<T>,
52}
53
54impl<
55        T: Float
56            + Debug
57            + scirs2_core::numeric::FromPrimitive
58            + scirs2_core::ndarray::ScalarOperand
59            + Send
60            + Sync,
61    > OptimizationSequenceProcessor<T>
62{
63    /// Create new sequence processor
64    ///
65    /// # Errors
66    /// Returns `Err` when `config.sequence_length` is zero. The derived window
67    /// size (`sequence_length / 2`) and stride (`window_size - overlap`) must
68    /// both be at least one: a zero stride makes `Iterator::step_by` panic, and
69    /// `window_size - window_overlap` would underflow in release builds. The
70    /// window is therefore floored at 1 and the overlap is kept strictly below
71    /// it.
72    pub fn new(config: &TransformerBasedOptimizerConfig<T>) -> Result<Self> {
73        let strategy = SequenceProcessingStrategy::SlidingWindow;
74        let max_sequence_length = config.sequence_length;
75        if max_sequence_length == 0 {
76            return Err(crate::error::OptimError::InvalidConfig(
77                "sequence_length must be greater than 0".to_string(),
78            ));
79        }
80        let window_size = (max_sequence_length / 2).max(1);
81        // Overlap must stay strictly below the window so the stride is >= 1.
82        let window_overlap = (window_size / 4).min(window_size - 1);
83        let model_dimension = config.model_dimension;
84
85        let sequence_buffer = SequenceBuffer::new(1000)?;
86        let statistics = SequenceStatistics::new();
87        let preprocessor = SequencePreprocessor::new(model_dimension)?;
88
89        Ok(Self {
90            strategy,
91            max_sequence_length,
92            window_size,
93            window_overlap,
94            sequence_buffer,
95            statistics,
96            preprocessor,
97        })
98    }
99
100    /// Process optimization sequence
101    pub fn process_optimization_sequence(
102        &mut self,
103        gradient_history: &Array2<T>,
104        parameter_history: &Array2<T>,
105        loss_history: &Array1<T>,
106    ) -> Result<Array2<T>> {
107        // Update statistics
108        self.statistics
109            .update(gradient_history, parameter_history, loss_history)?;
110
111        // Store in buffer
112        self.sequence_buffer
113            .add_sequence(gradient_history, parameter_history, loss_history)?;
114
115        match self.strategy {
116            SequenceProcessingStrategy::SlidingWindow => {
117                self.process_sliding_window(gradient_history, parameter_history, loss_history)
118            }
119            SequenceProcessingStrategy::Hierarchical => {
120                self.process_hierarchical(gradient_history, parameter_history, loss_history)
121            }
122            SequenceProcessingStrategy::AttentionBased => {
123                self.process_attention_based(gradient_history, parameter_history, loss_history)
124            }
125            SequenceProcessingStrategy::Adaptive => {
126                self.process_adaptive(gradient_history, parameter_history, loss_history)
127            }
128            SequenceProcessingStrategy::TruncatedBPTT => {
129                self.process_truncated_bptt(gradient_history, parameter_history, loss_history)
130            }
131        }
132    }
133
134    /// Process using sliding window strategy
135    fn process_sliding_window(
136        &mut self,
137        gradient_history: &Array2<T>,
138        parameter_history: &Array2<T>,
139        loss_history: &Array1<T>,
140    ) -> Result<Array2<T>> {
141        let sequence_length = gradient_history.shape()[0];
142
143        if sequence_length <= self.max_sequence_length {
144            // Sequence fits in one window
145            return self.combine_sequences(gradient_history, parameter_history, loss_history);
146        }
147
148        // Process in overlapping windows. `saturating_sub` plus `.max(1)` keeps
149        // the stride positive even if the window/overlap pair is ever mutated
150        // into an inconsistent state: `step_by(0)` panics.
151        let mut processed_chunks = Vec::new();
152        let step_size = self.window_size.saturating_sub(self.window_overlap).max(1);
153
154        for start in (0..sequence_length).step_by(step_size) {
155            let end = (start + self.window_size).min(sequence_length);
156
157            let grad_chunk = gradient_history.slice(s![start..end, ..]);
158            let param_chunk = parameter_history.slice(s![start..end, ..]);
159            let loss_chunk = loss_history.slice(s![start..end]);
160
161            let processed_chunk = self.combine_sequences(
162                &grad_chunk.to_owned(),
163                &param_chunk.to_owned(),
164                &loss_chunk.to_owned(),
165            )?;
166
167            processed_chunks.push(processed_chunk);
168        }
169
170        // Combine processed chunks
171        self.combine_chunks(&processed_chunks)
172    }
173
174    /// Process using hierarchical strategy
175    fn process_hierarchical(
176        &mut self,
177        gradient_history: &Array2<T>,
178        parameter_history: &Array2<T>,
179        loss_history: &Array1<T>,
180    ) -> Result<Array2<T>> {
181        let sequence_length = gradient_history.shape()[0];
182
183        // Create hierarchical representation
184        let mut levels = Vec::new();
185        let mut current_level =
186            self.combine_sequences(gradient_history, parameter_history, loss_history)?;
187        levels.push(current_level.clone());
188
189        // Build hierarchy by downsampling
190        let mut current_length = sequence_length;
191        while current_length > self.max_sequence_length {
192            current_length /= 2;
193            current_level = self.downsample_sequence(&current_level, current_length)?;
194            levels.push(current_level.clone());
195        }
196
197        // Return the highest level that fits. `levels` is seeded with the
198        // combined sequence before the loop, so it is never empty; taking the
199        // fallible read keeps that local rather than asserting it from afar.
200        levels.pop().ok_or_else(|| {
201            crate::error::OptimError::ComputationError(
202                "hierarchical processing produced no levels".to_string(),
203            )
204        })
205    }
206
207    /// Process using attention-based selection
208    fn process_attention_based(
209        &mut self,
210        gradient_history: &Array2<T>,
211        parameter_history: &Array2<T>,
212        loss_history: &Array1<T>,
213    ) -> Result<Array2<T>> {
214        let sequence_length = gradient_history.shape()[0];
215
216        if sequence_length <= self.max_sequence_length {
217            return self.combine_sequences(gradient_history, parameter_history, loss_history);
218        }
219
220        // Compute importance scores for each time step
221        let importance_scores = self.compute_importance_scores(gradient_history, loss_history)?;
222
223        // Select most important steps
224        let selected_indices =
225            self.select_top_k_indices(&importance_scores, self.max_sequence_length)?;
226
227        // Extract selected sequences
228        let mut selected_gradients =
229            Array2::zeros((self.max_sequence_length, gradient_history.shape()[1]));
230        let mut selected_parameters =
231            Array2::zeros((self.max_sequence_length, parameter_history.shape()[1]));
232        let mut selected_losses = Array1::zeros(self.max_sequence_length);
233
234        for (i, &idx) in selected_indices.iter().enumerate() {
235            selected_gradients
236                .row_mut(i)
237                .assign(&gradient_history.row(idx));
238            selected_parameters
239                .row_mut(i)
240                .assign(&parameter_history.row(idx));
241            selected_losses[i] = loss_history[idx];
242        }
243
244        self.combine_sequences(&selected_gradients, &selected_parameters, &selected_losses)
245    }
246
247    /// Process using adaptive segmentation
248    fn process_adaptive(
249        &mut self,
250        gradient_history: &Array2<T>,
251        parameter_history: &Array2<T>,
252        loss_history: &Array1<T>,
253    ) -> Result<Array2<T>> {
254        // Detect change points in the optimization trajectory
255        let change_points = self.detect_change_points(loss_history)?;
256
257        // Segment sequence based on change points
258        let segments = self.segment_by_change_points(
259            gradient_history,
260            parameter_history,
261            loss_history,
262            &change_points,
263        )?;
264
265        // Process each segment and combine
266        let mut processed_segments = Vec::new();
267        for segment in segments {
268            let processed =
269                self.combine_sequences(&segment.gradients, &segment.parameters, &segment.losses)?;
270            processed_segments.push(processed);
271        }
272
273        self.combine_chunks(&processed_segments)
274    }
275
276    /// Process using truncated BPTT
277    fn process_truncated_bptt(
278        &mut self,
279        gradient_history: &Array2<T>,
280        parameter_history: &Array2<T>,
281        loss_history: &Array1<T>,
282    ) -> Result<Array2<T>> {
283        let sequence_length = gradient_history.shape()[0];
284
285        if sequence_length <= self.max_sequence_length {
286            return self.combine_sequences(gradient_history, parameter_history, loss_history);
287        }
288
289        // Take the most recent subsequence
290        let start_idx = sequence_length - self.max_sequence_length;
291        let grad_chunk = gradient_history.slice(s![start_idx.., ..]);
292        let param_chunk = parameter_history.slice(s![start_idx.., ..]);
293        let loss_chunk = loss_history.slice(s![start_idx..]);
294
295        self.combine_sequences(
296            &grad_chunk.to_owned(),
297            &param_chunk.to_owned(),
298            &loss_chunk.to_owned(),
299        )
300    }
301
302    /// Convert optimization trajectory to training sequences
303    pub fn trajectory_to_sequences(
304        &mut self,
305        trajectory: &super::OptimizationTrajectory<T>,
306    ) -> Result<Vec<super::TrainingSequence<T>>> {
307        let sequence_length = trajectory.gradient_sequence.shape()[0];
308        let mut sequences = Vec::new();
309
310        // Create overlapping sequences for training. The stride is floored at 1
311        // because `step_by(0)` panics, which it did for any window size < 2.
312        let stride = (self.window_size / 2).max(1);
313        for start in (0..sequence_length).step_by(stride) {
314            let end = (start + self.window_size).min(sequence_length);
315
316            if end - start < stride {
317                break; // Skip sequences that are too short
318            }
319
320            let input_end = end - 1;
321            let target_start = start + 1;
322
323            let input_gradients = trajectory.gradient_sequence.slice(s![start..input_end, ..]);
324            let input_parameters = trajectory
325                .parameter_sequence
326                .slice(s![start..input_end, ..]);
327            let input_losses = trajectory.loss_sequence.slice(s![start..input_end]);
328
329            let target_gradients = trajectory
330                .gradient_sequence
331                .slice(s![target_start..end, ..]);
332            let target_parameters = trajectory
333                .parameter_sequence
334                .slice(s![target_start..end, ..]);
335            let target_losses = trajectory.loss_sequence.slice(s![target_start..end]);
336
337            let input = self.combine_sequences(
338                &input_gradients.to_owned(),
339                &input_parameters.to_owned(),
340                &input_losses.to_owned(),
341            )?;
342
343            let target = self.combine_sequences(
344                &target_gradients.to_owned(),
345                &target_parameters.to_owned(),
346                &target_losses.to_owned(),
347            )?;
348
349            sequences.push(super::TrainingSequence {
350                input,
351                target,
352                sequence_length: input_end - start,
353            });
354        }
355
356        Ok(sequences)
357    }
358
359    /// Combine gradients, parameters, and losses into unified sequence
360    fn combine_sequences(
361        &self,
362        gradients: &Array2<T>,
363        parameters: &Array2<T>,
364        losses: &Array1<T>,
365    ) -> Result<Array2<T>> {
366        self.preprocessor
367            .combine_sequences(gradients, parameters, losses)
368    }
369
370    /// Combine multiple processed chunks
371    fn combine_chunks(&self, chunks: &[Array2<T>]) -> Result<Array2<T>> {
372        if chunks.is_empty() {
373            return Err(crate::error::OptimError::Other(
374                "No chunks to combine".to_string(),
375            ));
376        }
377
378        if chunks.len() == 1 {
379            return Ok(chunks[0].clone());
380        }
381
382        // Simple concatenation strategy
383        let total_length: usize = chunks.iter().map(|chunk| chunk.shape()[0]).sum();
384        let feature_dim = chunks[0].shape()[1];
385
386        let mut combined = Array2::zeros((total_length.min(self.max_sequence_length), feature_dim));
387        let mut current_pos = 0;
388
389        for chunk in chunks {
390            let chunk_len = chunk.shape()[0];
391            let copy_len = (chunk_len).min(self.max_sequence_length - current_pos);
392
393            if copy_len == 0 {
394                break;
395            }
396
397            combined
398                .slice_mut(s![current_pos..current_pos + copy_len, ..])
399                .assign(&chunk.slice(s![..copy_len, ..]));
400
401            current_pos += copy_len;
402
403            if current_pos >= self.max_sequence_length {
404                break;
405            }
406        }
407
408        Ok(combined)
409    }
410
411    /// Downsample sequence to target length
412    fn downsample_sequence(&self, sequence: &Array2<T>, target_length: usize) -> Result<Array2<T>> {
413        let current_length = sequence.shape()[0];
414        let feature_dim = sequence.shape()[1];
415
416        if current_length <= target_length {
417            return Ok(sequence.clone());
418        }
419
420        let mut downsampled = Array2::zeros((target_length, feature_dim));
421        let step = current_length as f64 / target_length as f64;
422
423        for i in 0..target_length {
424            let source_idx = (i as f64 * step) as usize;
425            downsampled
426                .row_mut(i)
427                .assign(&sequence.row(source_idx.min(current_length - 1)));
428        }
429
430        Ok(downsampled)
431    }
432
433    /// Compute importance scores for sequence steps
434    fn compute_importance_scores(
435        &self,
436        gradients: &Array2<T>,
437        losses: &Array1<T>,
438    ) -> Result<Array1<T>> {
439        let sequence_length = gradients.shape()[0];
440        let mut scores = Array1::zeros(sequence_length);
441
442        for i in 0..sequence_length {
443            // Combine gradient magnitude and loss change
444            let grad_norm = gradients
445                .row(i)
446                .iter()
447                .map(|&x| x * x)
448                .fold(T::zero(), |acc, x| acc + x)
449                .sqrt();
450
451            let loss_change = if i > 0 {
452                (losses[i] - losses[i - 1]).abs()
453            } else {
454                T::zero()
455            };
456
457            scores[i] = grad_norm + loss_change;
458        }
459
460        Ok(scores)
461    }
462
463    /// Select top-k indices based on importance scores
464    fn select_top_k_indices(&self, scores: &Array1<T>, k: usize) -> Result<Vec<usize>> {
465        let mut indexed_scores: Vec<(usize, T)> = scores
466            .iter()
467            .enumerate()
468            .map(|(i, &score)| (i, score))
469            .collect();
470
471        // `total_cmp` is a total order, so a NaN importance score sorts
472        // deterministically rather than panicking inside `sort_by`.
473        indexed_scores.sort_by(|a, b| {
474            b.1.to_f64()
475                .unwrap_or(f64::NAN)
476                .total_cmp(&a.1.to_f64().unwrap_or(f64::NAN))
477        });
478
479        let mut selected_indices: Vec<usize> = indexed_scores
480            .into_iter()
481            .take(k)
482            .map(|(idx, _)| idx)
483            .collect();
484
485        selected_indices.sort();
486        Ok(selected_indices)
487    }
488
489    /// Detect change points in loss trajectory
490    fn detect_change_points(&self, losses: &Array1<T>) -> Result<Vec<usize>> {
491        let mut change_points = vec![0]; // Always include start
492        let window_size = 5;
493        let threshold = scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero());
494
495        // `losses.len() - window_size` underflowed (usize) for any trajectory
496        // shorter than 2*window_size, producing a huge upper bound and an
497        // out-of-bounds slice panic. Bail out instead when there is not enough
498        // history on both sides of a candidate change point.
499        let last_candidate = losses.len().saturating_sub(window_size);
500        for i in window_size..last_candidate {
501            let before_mean = match losses.slice(s![i - window_size..i]).mean() {
502                Some(m) => m,
503                None => continue,
504            };
505            let after_mean = match losses.slice(s![i..i + window_size]).mean() {
506                Some(m) => m,
507                None => continue,
508            };
509
510            if (before_mean - after_mean).abs() > threshold {
511                change_points.push(i);
512            }
513        }
514
515        change_points.push(losses.len() - 1); // Always include end
516        Ok(change_points)
517    }
518
519    /// Segment sequences by change points
520    fn segment_by_change_points(
521        &self,
522        gradients: &Array2<T>,
523        parameters: &Array2<T>,
524        losses: &Array1<T>,
525        change_points: &[usize],
526    ) -> Result<Vec<SequenceSegment<T>>> {
527        let mut segments = Vec::new();
528
529        for i in 0..change_points.len() - 1 {
530            let start = change_points[i];
531            let end = change_points[i + 1];
532
533            let segment = SequenceSegment {
534                gradients: gradients.slice(s![start..end, ..]).to_owned(),
535                parameters: parameters.slice(s![start..end, ..]).to_owned(),
536                losses: losses.slice(s![start..end]).to_owned(),
537                start_index: start,
538                end_index: end,
539            };
540
541            segments.push(segment);
542        }
543
544        Ok(segments)
545    }
546
547    /// Set processing strategy
548    pub fn set_strategy(&mut self, strategy: SequenceProcessingStrategy) {
549        self.strategy = strategy;
550    }
551
552    /// Get current strategy
553    pub fn get_strategy(&self) -> SequenceProcessingStrategy {
554        self.strategy
555    }
556
557    /// Get sequence statistics
558    pub fn get_statistics(&self) -> &SequenceStatistics<T> {
559        &self.statistics
560    }
561
562    /// Reset processor state
563    pub fn reset(&mut self) -> Result<()> {
564        self.sequence_buffer.clear();
565        self.statistics.reset();
566        Ok(())
567    }
568}
569
570/// Sequence buffer for storing optimization history
571pub struct SequenceBuffer<
572    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
573> {
574    /// Gradient history
575    gradient_buffer: VecDeque<Array2<T>>,
576
577    /// Parameter history
578    parameter_buffer: VecDeque<Array2<T>>,
579
580    /// Loss history
581    loss_buffer: VecDeque<Array1<T>>,
582
583    /// Maximum buffer size
584    max_size: usize,
585}
586
587impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
588    SequenceBuffer<T>
589{
590    /// A buffer holding at most `max_size` recorded sequences.
591    ///
592    /// The feature width is not a buffer concern: it belongs to
593    /// [`SequencePreprocessor`], which is the component that actually truncates
594    /// to it. The duplicate `model_dimension` this used to store was never read
595    /// and could silently disagree with the preprocessor's.
596    pub fn new(max_size: usize) -> Result<Self> {
597        Ok(Self {
598            gradient_buffer: VecDeque::new(),
599            parameter_buffer: VecDeque::new(),
600            loss_buffer: VecDeque::new(),
601            max_size,
602        })
603    }
604
605    pub fn add_sequence(
606        &mut self,
607        gradients: &Array2<T>,
608        parameters: &Array2<T>,
609        losses: &Array1<T>,
610    ) -> Result<()> {
611        self.gradient_buffer.push_back(gradients.clone());
612        self.parameter_buffer.push_back(parameters.clone());
613        self.loss_buffer.push_back(losses.clone());
614
615        while self.gradient_buffer.len() > self.max_size {
616            self.gradient_buffer.pop_front();
617            self.parameter_buffer.pop_front();
618            self.loss_buffer.pop_front();
619        }
620
621        Ok(())
622    }
623
624    pub fn clear(&mut self) {
625        self.gradient_buffer.clear();
626        self.parameter_buffer.clear();
627        self.loss_buffer.clear();
628    }
629
630    pub fn get_recent_sequences(&self, count: usize) -> SequenceDataTuple<T> {
631        let actual_count = count.min(self.gradient_buffer.len());
632
633        let gradients = self
634            .gradient_buffer
635            .iter()
636            .rev()
637            .take(actual_count)
638            .cloned()
639            .collect();
640        let parameters = self
641            .parameter_buffer
642            .iter()
643            .rev()
644            .take(actual_count)
645            .cloned()
646            .collect();
647        let losses = self
648            .loss_buffer
649            .iter()
650            .rev()
651            .take(actual_count)
652            .cloned()
653            .collect();
654
655        (gradients, parameters, losses)
656    }
657}
658
659/// Sequence statistics tracker
660pub struct SequenceStatistics<
661    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
662> {
663    /// Gradient statistics
664    gradient_stats: StatisticsAccumulator<T>,
665
666    /// Parameter statistics
667    parameter_stats: StatisticsAccumulator<T>,
668
669    /// Loss statistics
670    loss_stats: StatisticsAccumulator<T>,
671
672    /// Sequence length statistics
673    length_stats: StatisticsAccumulator<T>,
674}
675
676impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static> Default
677    for SequenceStatistics<T>
678{
679    fn default() -> Self {
680        Self::new()
681    }
682}
683
684impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
685    SequenceStatistics<T>
686{
687    pub fn new() -> Self {
688        Self {
689            gradient_stats: StatisticsAccumulator::new(),
690            parameter_stats: StatisticsAccumulator::new(),
691            loss_stats: StatisticsAccumulator::new(),
692            length_stats: StatisticsAccumulator::new(),
693        }
694    }
695
696    pub fn update(
697        &mut self,
698        gradients: &Array2<T>,
699        parameters: &Array2<T>,
700        losses: &Array1<T>,
701    ) -> Result<()> {
702        self.gradient_stats.update_from_array2(gradients);
703        self.parameter_stats.update_from_array2(parameters);
704        self.loss_stats.update_from_array1(losses);
705        self.length_stats
706            .update(crate::common::cast_scalar(gradients.shape()[0])?);
707
708        Ok(())
709    }
710
711    pub fn reset(&mut self) {
712        self.gradient_stats.reset();
713        self.parameter_stats.reset();
714        self.loss_stats.reset();
715        self.length_stats.reset();
716    }
717
718    pub fn get_gradient_stats(&self) -> &StatisticsAccumulator<T> {
719        &self.gradient_stats
720    }
721
722    pub fn get_parameter_stats(&self) -> &StatisticsAccumulator<T> {
723        &self.parameter_stats
724    }
725
726    pub fn get_loss_stats(&self) -> &StatisticsAccumulator<T> {
727        &self.loss_stats
728    }
729}
730
731/// Sequence preprocessor.
732///
733/// The per-feature normalization statistics this used to declare were never
734/// populated or consulted — `combine_sequences` normalizes a loss against the
735/// first observed loss instead — so the empty map has been removed rather than
736/// left as a permanently-empty cache.
737pub struct SequencePreprocessor<
738    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
739> {
740    /// Model dimension
741    model_dimension: usize,
742
743    _element: std::marker::PhantomData<T>,
744}
745
746impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
747    SequencePreprocessor<T>
748{
749    pub fn new(model_dimension: usize) -> Result<Self> {
750        Ok(Self {
751            model_dimension,
752            _element: std::marker::PhantomData,
753        })
754    }
755
756    pub fn combine_sequences(
757        &self,
758        gradients: &Array2<T>,
759        parameters: &Array2<T>,
760        losses: &Array1<T>,
761    ) -> Result<Array2<T>> {
762        let sequence_length = gradients.shape()[0];
763        let grad_dim = gradients.shape()[1];
764        let param_dim = parameters.shape()[1];
765
766        // Combined features: [gradients, parameters, loss, normalized_loss]
767        let feature_dim = grad_dim + param_dim + 2;
768        let mut combined = Array2::zeros((sequence_length, feature_dim.min(self.model_dimension)));
769
770        for i in 0..sequence_length {
771            let mut feature_idx = 0;
772
773            // Add gradient features
774            for j in 0..grad_dim.min(self.model_dimension / 3) {
775                if feature_idx < combined.shape()[1] {
776                    combined[[i, feature_idx]] = gradients[[i, j]];
777                    feature_idx += 1;
778                }
779            }
780
781            // Add parameter features
782            for j in 0..param_dim.min(self.model_dimension / 3) {
783                if feature_idx < combined.shape()[1] {
784                    combined[[i, feature_idx]] = parameters[[i, j]];
785                    feature_idx += 1;
786                }
787            }
788
789            // Add loss features
790            if feature_idx < combined.shape()[1] {
791                combined[[i, feature_idx]] = losses[i];
792                feature_idx += 1;
793            }
794
795            // Add normalized loss (loss relative to first loss)
796            if feature_idx < combined.shape()[1] && i > 0 {
797                let normalized_loss = if losses[0] != T::zero() {
798                    losses[i] / losses[0]
799                } else {
800                    T::one()
801                };
802                combined[[i, feature_idx]] = normalized_loss;
803            }
804        }
805
806        Ok(combined)
807    }
808}
809
810/// Chunking strategy
811pub struct ChunkingStrategy<
812    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
813> {
814    /// Maximum chunk size
815    max_chunk_size: usize,
816
817    /// Overlap between chunks
818    overlap_size: usize,
819
820    /// Chunk statistics
821    chunk_stats: StatisticsAccumulator<T>,
822}
823
824impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
825    ChunkingStrategy<T>
826{
827    /// # Errors
828    /// Returns `Err` when `max_chunk_size` is zero or `overlap_size` is not
829    /// strictly smaller than it — either would make the chunking stride
830    /// (`max_chunk_size - overlap_size`) zero or underflow, and a zero stride
831    /// makes `Iterator::step_by` panic.
832    pub fn new(max_chunk_size: usize, overlap_size: usize) -> Result<Self> {
833        if max_chunk_size == 0 {
834            return Err(crate::error::OptimError::InvalidConfig(
835                "max_chunk_size must be greater than 0".to_string(),
836            ));
837        }
838        if overlap_size >= max_chunk_size {
839            return Err(crate::error::OptimError::InvalidConfig(format!(
840                "overlap_size ({overlap_size}) must be smaller than max_chunk_size ({max_chunk_size})"
841            )));
842        }
843        Ok(Self {
844            max_chunk_size,
845            overlap_size,
846            chunk_stats: StatisticsAccumulator::new(),
847        })
848    }
849
850    /// Accumulated statistics over every element of every chunk this strategy
851    /// has produced.
852    ///
853    /// `create_chunks` used to build the chunks without ever feeding the
854    /// accumulator, so these statistics were permanently empty.
855    pub fn chunk_stats(&self) -> &StatisticsAccumulator<T> {
856        &self.chunk_stats
857    }
858
859    pub fn create_chunks(&mut self, sequence: &Array2<T>) -> Result<Vec<Array2<T>>> {
860        let sequence_length = sequence.shape()[0];
861        let mut chunks = Vec::new();
862
863        if sequence_length <= self.max_chunk_size {
864            self.chunk_stats.update_from_array2(sequence);
865            chunks.push(sequence.clone());
866            return Ok(chunks);
867        }
868
869        let step_size = self.max_chunk_size.saturating_sub(self.overlap_size).max(1);
870
871        for start in (0..sequence_length).step_by(step_size) {
872            let end = (start + self.max_chunk_size).min(sequence_length);
873            let chunk = sequence.slice(s![start..end, ..]).to_owned();
874            self.chunk_stats.update_from_array2(&chunk);
875            chunks.push(chunk);
876
877            if end >= sequence_length {
878                break;
879            }
880        }
881
882        Ok(chunks)
883    }
884}
885
886/// Supporting data structures
887#[derive(Debug, Clone)]
888pub struct SequenceSegment<
889    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
890> {
891    pub gradients: Array2<T>,
892    pub parameters: Array2<T>,
893    pub losses: Array1<T>,
894    pub start_index: usize,
895    pub end_index: usize,
896}
897
898pub struct StatisticsAccumulator<
899    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
900> {
901    count: usize,
902    sum: T,
903    sum_sq: T,
904    min: T,
905    max: T,
906}
907
908impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static> Default
909    for StatisticsAccumulator<T>
910{
911    fn default() -> Self {
912        Self::new()
913    }
914}
915
916impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
917    StatisticsAccumulator<T>
918{
919    pub fn new() -> Self {
920        Self {
921            count: 0,
922            sum: T::zero(),
923            sum_sq: T::zero(),
924            min: T::infinity(),
925            max: T::neg_infinity(),
926        }
927    }
928
929    pub fn update(&mut self, value: T) {
930        self.count += 1;
931        self.sum = self.sum + value;
932        self.sum_sq = self.sum_sq + value * value;
933        self.min = self.min.min(value);
934        self.max = self.max.max(value);
935    }
936
937    pub fn update_from_array1(&mut self, array: &Array1<T>) {
938        for &value in array.iter() {
939            self.update(value);
940        }
941    }
942
943    pub fn update_from_array2(&mut self, array: &Array2<T>) {
944        for &value in array.iter() {
945            self.update(value);
946        }
947    }
948
949    pub fn mean(&self) -> T {
950        if self.count > 0 {
951            self.sum / scirs2_core::numeric::NumCast::from(self.count).unwrap_or_else(|| T::zero())
952        } else {
953            T::zero()
954        }
955    }
956
957    pub fn variance(&self) -> T {
958        if self.count > 1 {
959            let mean = self.mean();
960            (self.sum_sq
961                / scirs2_core::numeric::NumCast::from(self.count).unwrap_or_else(|| T::zero()))
962                - (mean * mean)
963        } else {
964            T::zero()
965        }
966    }
967
968    pub fn std_dev(&self) -> T {
969        self.variance().sqrt()
970    }
971
972    pub fn min(&self) -> T {
973        self.min
974    }
975
976    pub fn max(&self) -> T {
977        self.max
978    }
979
980    pub fn reset(&mut self) {
981        self.count = 0;
982        self.sum = T::zero();
983        self.sum_sq = T::zero();
984        self.min = T::infinity();
985        self.max = T::neg_infinity();
986    }
987}
988
989// Import for slice macro
990use scirs2_core::ndarray::s;
991
992#[cfg(test)]
993mod tests {
994    use super::*;
995
996    #[test]
997    fn test_sequence_processor_creation() {
998        let config = super::super::config::TransformerBasedOptimizerConfig::<f32>::default();
999        let processor = OptimizationSequenceProcessor::new(&config);
1000        assert!(processor.is_ok());
1001    }
1002
1003    #[test]
1004    fn test_sequence_buffer() {
1005        let buffer = SequenceBuffer::<f32>::new(10);
1006        assert!(buffer.is_ok());
1007
1008        let mut buf = buffer.expect("SequenceBuffer::new should succeed");
1009        let gradients = Array2::<f32>::ones((5, 64));
1010        let parameters = Array2::<f32>::ones((5, 64));
1011        let losses = Array1::<f32>::ones(5);
1012
1013        assert!(buf.add_sequence(&gradients, &parameters, &losses).is_ok());
1014    }
1015
1016    #[test]
1017    fn test_sequence_statistics() {
1018        let mut stats = SequenceStatistics::<f32>::new();
1019
1020        let gradients = Array2::<f32>::ones((10, 5));
1021        let parameters = Array2::<f32>::ones((10, 5));
1022        let losses = Array1::<f32>::ones(10);
1023
1024        assert!(stats.update(&gradients, &parameters, &losses).is_ok());
1025        assert!(stats.get_gradient_stats().mean() > 0.0);
1026    }
1027
1028    #[test]
1029    fn test_statistics_accumulator() {
1030        let mut acc = StatisticsAccumulator::<f32>::new();
1031
1032        acc.update(1.0);
1033        acc.update(2.0);
1034        acc.update(3.0);
1035
1036        assert_eq!(acc.mean(), 2.0);
1037        assert!(acc.std_dev() > 0.0);
1038        assert_eq!(acc.min(), 1.0);
1039        assert_eq!(acc.max(), 3.0);
1040    }
1041
1042    #[test]
1043    fn test_chunking_strategy() {
1044        let chunking = ChunkingStrategy::<f32>::new(10, 2);
1045        assert!(chunking.is_ok());
1046
1047        let mut strategy = chunking.expect("ChunkingStrategy::new should succeed");
1048        let sequence = Array2::<f32>::ones((25, 5));
1049
1050        let chunks = strategy.create_chunks(&sequence);
1051        assert!(chunks.is_ok());
1052
1053        let chunk_vec = chunks.expect("create_chunks should succeed");
1054        assert!(chunk_vec.len() > 1);
1055    }
1056}