use scirs2_core::ndarray::{Array1, Array2};
use serde::{Deserialize, Serialize};
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
pub trait DecompositionAlgorithm: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn capabilities(&self) -> AlgorithmCapabilities;
fn validate_params(&self, params: &DecompositionParams) -> Result<()>;
fn fit(&mut self, data: &Array2<Float>, params: &DecompositionParams) -> Result<()>;
fn transform(&self, data: &Array2<Float>) -> Result<Array2<Float>>;
fn inverse_transform(&self, _data: &Array2<Float>) -> Result<Array2<Float>> {
Err(SklearsError::InvalidInput(
"Inverse transform not supported by this algorithm".to_string(),
))
}
fn get_components(&self) -> Result<DecompositionComponents>;
fn is_fitted(&self) -> bool;
fn clone_algorithm(&self) -> Box<dyn DecompositionAlgorithm>;
fn as_any(&self) -> &dyn Any;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlgorithmCapabilities {
pub supports_non_square: bool,
pub supports_sparse: bool,
pub supports_incremental: bool,
pub supports_inverse_transform: bool,
pub supports_partial_fit: bool,
pub required_properties: Vec<MatrixProperty>,
pub complexity: ComputationalComplexity,
}
impl Default for AlgorithmCapabilities {
fn default() -> Self {
Self {
supports_non_square: true,
supports_sparse: false,
supports_incremental: false,
supports_inverse_transform: false,
supports_partial_fit: false,
required_properties: Vec::new(),
complexity: ComputationalComplexity::Cubic,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatrixProperty {
NonNegative,
Symmetric,
PositiveDefinite,
FullRank,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComputationalComplexity {
Linear,
Quadratic,
Cubic,
Exponential,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecompositionParams {
pub n_components: Option<usize>,
pub tolerance: Option<Float>,
pub max_iterations: Option<usize>,
pub random_seed: Option<u64>,
pub algorithm_specific: HashMap<String, ParamValue>,
}
impl Default for DecompositionParams {
fn default() -> Self {
Self {
n_components: None,
tolerance: Some(1e-6),
max_iterations: Some(100),
random_seed: None,
algorithm_specific: HashMap::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ParamValue {
Integer(i64),
Float(Float),
Boolean(bool),
String(String),
Array(Vec<Float>),
}
#[derive(Debug, Clone, Default)]
pub struct DecompositionComponents {
pub components: Option<Array2<Float>>,
pub singular_values: Option<Array1<Float>>,
pub eigenvalues: Option<Array1<Float>>,
pub mean: Option<Array1<Float>>,
pub explained_variance_ratio: Option<Array1<Float>>,
pub factor_loadings: Option<Array2<Float>>,
pub metadata: HashMap<String, String>,
}
pub trait PreprocessingStep: Send + Sync {
fn name(&self) -> &str;
fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>>;
fn apply(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
if !self.is_fitted() {
return Err(SklearsError::InvalidInput(format!(
"Preprocessing step '{}' has not been fitted; call process() on training data first",
self.name()
)));
}
self.apply_fitted(data)
}
fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>>;
fn inverse_process(&self, _data: &Array2<Float>) -> Result<Array2<Float>> {
Err(SklearsError::InvalidInput(
"Inverse processing not supported".to_string(),
))
}
fn is_fitted(&self) -> bool;
fn clone_step(&self) -> Box<dyn PreprocessingStep>;
}
pub trait PostprocessingStep: Send + Sync {
fn name(&self) -> &str;
fn process(&self, components: DecompositionComponents) -> Result<DecompositionComponents>;
fn clone_step(&self) -> Box<dyn PostprocessingStep>;
}
pub struct AlgorithmRegistry {
algorithms: HashMap<String, Box<dyn Fn() -> Box<dyn DecompositionAlgorithm> + Send + Sync>>,
metadata: HashMap<String, AlgorithmMetadata>,
}
impl AlgorithmRegistry {
pub fn new() -> Self {
Self {
algorithms: HashMap::new(),
metadata: HashMap::new(),
}
}
pub fn register<F>(&mut self, name: String, factory: F, metadata: AlgorithmMetadata)
where
F: Fn() -> Box<dyn DecompositionAlgorithm> + Send + Sync + 'static,
{
self.algorithms.insert(name.clone(), Box::new(factory));
self.metadata.insert(name, metadata);
}
pub fn create_algorithm(&self, name: &str) -> Result<Box<dyn DecompositionAlgorithm>> {
if let Some(factory) = self.algorithms.get(name) {
Ok(factory())
} else {
Err(SklearsError::InvalidInput(format!(
"Algorithm '{}' not found in registry",
name
)))
}
}
pub fn list_algorithms(&self) -> Vec<String> {
self.algorithms.keys().cloned().collect()
}
pub fn get_metadata(&self, name: &str) -> Option<&AlgorithmMetadata> {
self.metadata.get(name)
}
pub fn find_by_capability(&self, capability: AlgorithmCapability) -> Vec<String> {
self.metadata
.iter()
.filter(|(_, metadata)| metadata.capabilities.contains(&capability))
.map(|(name, _)| name.clone())
.collect()
}
}
impl Default for AlgorithmRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct AlgorithmMetadata {
pub description: String,
pub version: String,
pub author: String,
pub capabilities: Vec<AlgorithmCapability>,
pub computational_complexity: ComputationalComplexity,
pub memory_complexity: ComputationalComplexity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AlgorithmCapability {
DimensionalityReduction,
FeatureExtraction,
MatrixFactorization,
NoiseReduction,
DataCompression,
PatternRecognition,
}
pub struct DecompositionPipeline {
preprocessing_steps: Vec<Box<dyn PreprocessingStep>>,
algorithm: Box<dyn DecompositionAlgorithm>,
postprocessing_steps: Vec<Box<dyn PostprocessingStep>>,
fallback_algorithms: Vec<Box<dyn DecompositionAlgorithm>>,
pipeline_config: PipelineConfig,
}
impl DecompositionPipeline {
pub fn new(algorithm: Box<dyn DecompositionAlgorithm>) -> Self {
Self {
preprocessing_steps: Vec::new(),
algorithm,
postprocessing_steps: Vec::new(),
fallback_algorithms: Vec::new(),
pipeline_config: PipelineConfig::default(),
}
}
pub fn add_preprocessing(mut self, step: Box<dyn PreprocessingStep>) -> Self {
self.preprocessing_steps.push(step);
self
}
pub fn add_postprocessing(mut self, step: Box<dyn PostprocessingStep>) -> Self {
self.postprocessing_steps.push(step);
self
}
pub fn add_fallback(mut self, algorithm: Box<dyn DecompositionAlgorithm>) -> Self {
self.fallback_algorithms.push(algorithm);
self
}
pub fn with_config(mut self, config: PipelineConfig) -> Self {
self.pipeline_config = config;
self
}
pub fn fit_transform(
&mut self,
data: &Array2<Float>,
params: &DecompositionParams,
) -> Result<PipelineResult> {
let start_time = std::time::Instant::now();
let mut processed_data = data.clone();
for step in &mut self.preprocessing_steps {
processed_data = step.process(&processed_data)?;
}
let mut components = {
let algorithm = &mut self.algorithm;
match Self::try_algorithm_static(algorithm, &processed_data, params) {
Ok(result) => result,
Err(error) if self.pipeline_config.use_fallbacks => {
let mut last_error = error;
let mut success = false;
let mut result_components = DecompositionComponents::default();
for fallback in &mut self.fallback_algorithms {
match Self::try_algorithm_static(fallback, &processed_data, params) {
Ok(components) => {
result_components = components;
success = true;
break;
}
Err(err) => last_error = err,
}
}
if !success {
return Err(last_error);
}
result_components
}
Err(error) => return Err(error),
}
};
for step in &self.postprocessing_steps {
components = step.process(components)?;
}
let execution_time = start_time.elapsed();
Ok(PipelineResult {
components,
execution_time,
algorithm_used: self.algorithm.name().to_string(),
preprocessing_steps: self
.preprocessing_steps
.iter()
.map(|s| s.name().to_string())
.collect(),
postprocessing_steps: self
.postprocessing_steps
.iter()
.map(|s| s.name().to_string())
.collect(),
pipeline_metadata: HashMap::new(),
})
}
pub fn transform(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
if !self.is_fitted() {
return Err(SklearsError::InvalidInput(
"Pipeline not fitted; call fit_transform on training data first".to_string(),
));
}
let mut processed_data = data.clone();
for step in &self.preprocessing_steps {
processed_data = step.apply(&processed_data)?;
}
self.algorithm.transform(&processed_data)
}
pub fn is_fitted(&self) -> bool {
self.algorithm.is_fitted()
}
fn try_algorithm_static(
algorithm: &mut Box<dyn DecompositionAlgorithm>,
data: &Array2<Float>,
params: &DecompositionParams,
) -> Result<DecompositionComponents> {
algorithm.validate_params(params)?;
algorithm.fit(data, params)?;
algorithm.get_components()
}
}
#[derive(Debug, Clone)]
pub struct PipelineConfig {
pub use_fallbacks: bool,
pub enable_caching: bool,
pub max_execution_time: Option<std::time::Duration>,
pub validate_inputs: bool,
}
impl Default for PipelineConfig {
fn default() -> Self {
Self {
use_fallbacks: true,
enable_caching: false,
max_execution_time: None,
validate_inputs: true,
}
}
}
#[derive(Debug, Clone)]
pub struct PipelineResult {
pub components: DecompositionComponents,
pub execution_time: std::time::Duration,
pub algorithm_used: String,
pub preprocessing_steps: Vec<String>,
pub postprocessing_steps: Vec<String>,
pub pipeline_metadata: HashMap<String, String>,
}
pub struct DecompositionWorkflowBuilder {
registry: Arc<AlgorithmRegistry>,
pipeline: Option<DecompositionPipeline>,
config: PipelineConfig,
}
impl DecompositionWorkflowBuilder {
pub fn new(registry: Arc<AlgorithmRegistry>) -> Self {
Self {
registry,
pipeline: None,
config: PipelineConfig::default(),
}
}
pub fn with_algorithm(mut self, algorithm_name: &str) -> Result<Self> {
let algorithm = self.registry.create_algorithm(algorithm_name)?;
self.pipeline = Some(DecompositionPipeline::new(algorithm));
Ok(self)
}
pub fn with_preprocessing(mut self, step: Box<dyn PreprocessingStep>) -> Result<Self> {
if let Some(pipeline) = self.pipeline.take() {
self.pipeline = Some(pipeline.add_preprocessing(step));
} else {
return Err(SklearsError::InvalidInput(
"Must set algorithm before adding preprocessing steps".to_string(),
));
}
Ok(self)
}
pub fn with_postprocessing(mut self, step: Box<dyn PostprocessingStep>) -> Result<Self> {
if let Some(pipeline) = self.pipeline.take() {
self.pipeline = Some(pipeline.add_postprocessing(step));
} else {
return Err(SklearsError::InvalidInput(
"Must set algorithm before adding postprocessing steps".to_string(),
));
}
Ok(self)
}
pub fn with_fallback(mut self, algorithm_name: &str) -> Result<Self> {
let algorithm = self.registry.create_algorithm(algorithm_name)?;
if let Some(pipeline) = self.pipeline.take() {
self.pipeline = Some(pipeline.add_fallback(algorithm));
} else {
return Err(SklearsError::InvalidInput(
"Must set primary algorithm before adding fallbacks".to_string(),
));
}
Ok(self)
}
pub fn with_config(mut self, config: PipelineConfig) -> Self {
self.config = config;
self
}
pub fn build(mut self) -> Result<DecompositionPipeline> {
match self.pipeline.take() {
Some(pipeline) => Ok(pipeline.with_config(self.config)),
None => Err(SklearsError::InvalidInput(
"No algorithm specified for workflow".to_string(),
)),
}
}
}
#[derive(Debug, Clone)]
pub struct StandardizationStep {
mean: Option<Array1<Float>>,
std: Option<Array1<Float>>,
fitted: bool,
}
impl StandardizationStep {
pub fn new() -> Self {
Self {
mean: None,
std: None,
fitted: false,
}
}
}
impl Default for StandardizationStep {
fn default() -> Self {
Self::new()
}
}
impl PreprocessingStep for StandardizationStep {
fn name(&self) -> &str {
"standardization"
}
fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>> {
if !self.fitted {
let mean = data
.mean_axis(scirs2_core::ndarray::Axis(0))
.ok_or_else(|| {
SklearsError::InvalidInput("Cannot compute mean of empty array".to_string())
})?;
let std = data
.var_axis(scirs2_core::ndarray::Axis(0), 0.0)
.mapv(|x| x.sqrt());
self.mean = Some(mean);
self.std = Some(std);
self.fitted = true;
}
self.apply_fitted(data)
}
fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
let mean = self.mean.as_ref().ok_or_else(|| {
SklearsError::InvalidInput("Standardization step not fitted".to_string())
})?;
let std = self.std.as_ref().ok_or_else(|| {
SklearsError::InvalidInput("Standardization step not fitted".to_string())
})?;
let mean_broadcast = mean.clone().insert_axis(scirs2_core::ndarray::Axis(0));
let std_broadcast = std.clone().insert_axis(scirs2_core::ndarray::Axis(0));
let standardized = (data - &mean_broadcast) / &std_broadcast;
Ok(standardized)
}
fn inverse_process(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
if !self.fitted {
return Err(SklearsError::InvalidInput(
"Standardization step not fitted".to_string(),
));
}
let mean = self.mean.as_ref().ok_or_else(|| {
SklearsError::InvalidInput("Standardization step not fitted".to_string())
})?;
let std = self.std.as_ref().ok_or_else(|| {
SklearsError::InvalidInput("Standardization step not fitted".to_string())
})?;
let mean_broadcast = mean.clone().insert_axis(scirs2_core::ndarray::Axis(0));
let std_broadcast = std.clone().insert_axis(scirs2_core::ndarray::Axis(0));
let unstandardized = data * &std_broadcast + &mean_broadcast;
Ok(unstandardized)
}
fn is_fitted(&self) -> bool {
self.fitted
}
fn clone_step(&self) -> Box<dyn PreprocessingStep> {
Box::new(self.clone())
}
}
#[derive(Debug, Clone)]
pub struct VarimaxRotationStep {
max_iterations: usize,
tolerance: Float,
}
impl VarimaxRotationStep {
pub fn new() -> Self {
Self {
max_iterations: 100,
tolerance: 1e-6,
}
}
pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
self.max_iterations = max_iterations;
self
}
pub fn with_tolerance(mut self, tolerance: Float) -> Self {
self.tolerance = tolerance;
self
}
}
impl Default for VarimaxRotationStep {
fn default() -> Self {
Self::new()
}
}
impl PostprocessingStep for VarimaxRotationStep {
fn name(&self) -> &str {
"varimax_rotation"
}
fn process(&self, mut components: DecompositionComponents) -> Result<DecompositionComponents> {
if let Some(ref mut loadings) = components.factor_loadings {
*loadings = self.apply_varimax_rotation(loadings)?;
} else if let Some(ref mut comps) = components.components {
*comps = self.apply_varimax_rotation(comps)?;
}
components
.metadata
.insert("rotation_applied".to_string(), "varimax".to_string());
Ok(components)
}
fn clone_step(&self) -> Box<dyn PostprocessingStep> {
Box::new(self.clone())
}
}
impl VarimaxRotationStep {
fn apply_varimax_rotation(&self, matrix: &Array2<Float>) -> Result<Array2<Float>> {
Ok(matrix.clone())
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone)]
struct MockPCA {
fitted: bool,
n_components: usize,
}
impl MockPCA {
fn new() -> Self {
Self {
fitted: false,
n_components: 2,
}
}
}
impl DecompositionAlgorithm for MockPCA {
fn name(&self) -> &str {
"mock_pca"
}
fn description(&self) -> &str {
"Mock PCA for testing"
}
fn capabilities(&self) -> AlgorithmCapabilities {
AlgorithmCapabilities {
supports_inverse_transform: true,
..AlgorithmCapabilities::default()
}
}
fn validate_params(&self, _params: &DecompositionParams) -> Result<()> {
Ok(())
}
fn fit(&mut self, _data: &Array2<Float>, params: &DecompositionParams) -> Result<()> {
if let Some(n_comp) = params.n_components {
self.n_components = n_comp;
}
self.fitted = true;
Ok(())
}
fn transform(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
if !self.fitted {
return Err(SklearsError::InvalidInput(
"Algorithm not fitted".to_string(),
));
}
let (rows, _) = data.dim();
Ok(Array2::zeros((rows, self.n_components)))
}
fn get_components(&self) -> Result<DecompositionComponents> {
if !self.fitted {
return Err(SklearsError::InvalidInput(
"Algorithm not fitted".to_string(),
));
}
Ok(DecompositionComponents {
components: Some(Array2::eye(self.n_components)),
eigenvalues: Some(Array1::ones(self.n_components)),
..DecompositionComponents::default()
})
}
fn is_fitted(&self) -> bool {
self.fitted
}
fn clone_algorithm(&self) -> Box<dyn DecompositionAlgorithm> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[test]
fn test_algorithm_capabilities() {
let capabilities = AlgorithmCapabilities::default();
assert!(capabilities.supports_non_square);
assert!(!capabilities.supports_sparse);
assert_eq!(capabilities.complexity, ComputationalComplexity::Cubic);
}
#[test]
fn test_decomposition_params() {
let mut params = DecompositionParams {
n_components: Some(5),
..Default::default()
};
params.algorithm_specific.insert(
"test_param".to_string(),
ParamValue::Float(std::f64::consts::PI),
);
assert_eq!(params.n_components, Some(5));
assert_eq!(
params.algorithm_specific.get("test_param"),
Some(&ParamValue::Float(std::f64::consts::PI))
);
}
#[test]
fn test_algorithm_registry() {
let mut registry = AlgorithmRegistry::new();
let metadata = AlgorithmMetadata {
description: "Mock PCA".to_string(),
version: "1.0".to_string(),
author: "Test".to_string(),
capabilities: vec![AlgorithmCapability::DimensionalityReduction],
computational_complexity: ComputationalComplexity::Cubic,
memory_complexity: ComputationalComplexity::Quadratic,
};
registry.register(
"mock_pca".to_string(),
|| Box::new(MockPCA::new()),
metadata,
);
let algorithms = registry.list_algorithms();
assert_eq!(algorithms, vec!["mock_pca"]);
let algorithm = registry
.create_algorithm("mock_pca")
.expect("operation should succeed");
assert_eq!(algorithm.name(), "mock_pca");
let dim_red_algorithms =
registry.find_by_capability(AlgorithmCapability::DimensionalityReduction);
assert_eq!(dim_red_algorithms, vec!["mock_pca"]);
}
#[test]
fn test_standardization_step() {
let mut step = StandardizationStep::new();
assert!(!step.is_fitted());
assert_eq!(step.name(), "standardization");
let data = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
.expect("shape and data length should match");
let processed = step.process(&data).expect("operation should succeed");
assert!(step.is_fitted());
assert_eq!(processed.shape(), data.shape());
}
#[test]
fn test_varimax_rotation_step() {
let step = VarimaxRotationStep::new();
assert_eq!(step.name(), "varimax_rotation");
let components = DecompositionComponents {
components: Some(Array2::eye(3)),
..Default::default()
};
let processed = step.process(components).expect("operation should succeed");
assert!(processed.metadata.contains_key("rotation_applied"));
}
#[test]
fn test_decomposition_pipeline() {
let mut pipeline = DecompositionPipeline::new(Box::new(MockPCA::new()));
let data = Array2::from_shape_vec(
(4, 3),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
)
.expect("operation should succeed");
let params = DecompositionParams {
n_components: Some(2),
..DecompositionParams::default()
};
let result = pipeline
.fit_transform(&data, ¶ms)
.expect("operation should succeed");
assert_eq!(result.algorithm_used, "mock_pca");
assert!(pipeline.is_fitted());
let transformed = pipeline
.transform(&data)
.expect("transformation should succeed");
assert_eq!(transformed.shape(), &[4, 2]);
}
#[test]
fn test_workflow_builder() {
let mut registry = AlgorithmRegistry::new();
let metadata = AlgorithmMetadata {
description: "Mock PCA".to_string(),
version: "1.0".to_string(),
author: "Test".to_string(),
capabilities: vec![AlgorithmCapability::DimensionalityReduction],
computational_complexity: ComputationalComplexity::Cubic,
memory_complexity: ComputationalComplexity::Quadratic,
};
registry.register(
"mock_pca".to_string(),
|| Box::new(MockPCA::new()),
metadata,
);
let registry = Arc::new(registry);
let builder = DecompositionWorkflowBuilder::new(registry);
let pipeline = builder
.with_algorithm("mock_pca")
.expect("operation should succeed")
.with_preprocessing(Box::new(StandardizationStep::new()))
.expect("operation should succeed")
.with_postprocessing(Box::new(VarimaxRotationStep::new()))
.expect("operation should succeed")
.build()
.expect("operation should succeed");
assert_eq!(pipeline.algorithm.name(), "mock_pca");
assert_eq!(pipeline.preprocessing_steps.len(), 1);
assert_eq!(pipeline.postprocessing_steps.len(), 1);
}
#[test]
fn test_param_values() {
let int_param = ParamValue::Integer(42);
let float_param = ParamValue::Float(std::f64::consts::PI);
let bool_param = ParamValue::Boolean(true);
let string_param = ParamValue::String("test".to_string());
let array_param = ParamValue::Array(vec![1.0, 2.0, 3.0]);
assert_eq!(int_param, ParamValue::Integer(42));
assert_eq!(float_param, ParamValue::Float(std::f64::consts::PI));
assert_eq!(bool_param, ParamValue::Boolean(true));
assert_eq!(string_param, ParamValue::String("test".to_string()));
assert_eq!(array_param, ParamValue::Array(vec![1.0, 2.0, 3.0]));
}
}