use scirs2_core::ndarray::{concatenate, s, Array1, Array2, ArrayView1, Axis};
use sklears_core::error::{Result as SklResult, SklearsError};
use sklears_core::traits::{Estimator, Fit, Transform};
use std::collections::HashMap;
use std::marker::PhantomData;
type Result<T> = SklResult<T>;
type Float = f64;
pub type FusionSelectionResult = Result<(
HashMap<String, Vec<usize>>,
Vec<(String, usize)>,
HashMap<String, Array1<Float>>,
Option<Array2<Float>>,
HashMap<usize, (String, usize)>,
usize,
HashMap<String, usize>,
)>;
pub type ConcatenateModalitiesResult = Result<(
Array2<Float>,
HashMap<usize, (String, usize)>,
HashMap<String, usize>,
)>;
#[derive(Debug, Clone)]
pub struct Untrained;
#[derive(Debug, Clone)]
pub struct Trained {
fusion_strategy: String,
selected_features_per_modality: HashMap<String, Vec<usize>>,
combined_selected_features: Vec<(String, usize)>, _feature_scores_per_modality: HashMap<String, Array1<Float>>,
_cross_modal_scores: Option<Array2<Float>>,
_modality_weights: HashMap<String, Float>,
_feature_mapping: HashMap<usize, (String, usize)>, _total_features: usize,
_modality_feature_counts: HashMap<String, usize>,
}
#[derive(Debug, Clone)]
pub struct MultiModalFeatureSelector<State = Untrained> {
fusion_strategy: String,
modality_weights: HashMap<String, Float>,
modality_k: HashMap<String, usize>,
cross_modal_analysis: bool,
cross_modal_threshold: Float,
normalize_modalities: bool,
handle_missing_modalities: bool,
min_modalities_required: usize,
missing_strategy: String,
k: Option<usize>,
score_threshold: Float,
correlation_method: String,
interaction_analysis: bool,
max_interaction_order: usize,
state: PhantomData<State>,
trained_state: Option<Trained>,
}
impl Default for MultiModalFeatureSelector<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl MultiModalFeatureSelector<Untrained> {
pub fn new() -> Self {
Self {
fusion_strategy: "hybrid".to_string(),
modality_weights: HashMap::new(),
modality_k: HashMap::new(),
cross_modal_analysis: true,
cross_modal_threshold: 0.1,
normalize_modalities: true,
handle_missing_modalities: true,
min_modalities_required: 1,
missing_strategy: "ignore".to_string(),
k: None,
score_threshold: 0.1,
correlation_method: "pearson".to_string(),
interaction_analysis: false,
max_interaction_order: 2,
state: PhantomData,
trained_state: None,
}
}
pub fn builder() -> MultiModalFeatureSelectorBuilder {
MultiModalFeatureSelectorBuilder::new()
}
}
#[derive(Debug)]
pub struct MultiModalFeatureSelectorBuilder {
fusion_strategy: String,
modality_weights: HashMap<String, Float>,
modality_k: HashMap<String, usize>,
cross_modal_analysis: bool,
cross_modal_threshold: Float,
normalize_modalities: bool,
handle_missing_modalities: bool,
min_modalities_required: usize,
missing_strategy: String,
k: Option<usize>,
score_threshold: Float,
correlation_method: String,
interaction_analysis: bool,
max_interaction_order: usize,
}
impl Default for MultiModalFeatureSelectorBuilder {
fn default() -> Self {
Self::new()
}
}
impl MultiModalFeatureSelectorBuilder {
pub fn new() -> Self {
Self {
fusion_strategy: "hybrid".to_string(),
modality_weights: HashMap::new(),
modality_k: HashMap::new(),
cross_modal_analysis: true,
cross_modal_threshold: 0.1,
normalize_modalities: true,
handle_missing_modalities: true,
min_modalities_required: 1,
missing_strategy: "ignore".to_string(),
k: None,
score_threshold: 0.1,
correlation_method: "pearson".to_string(),
interaction_analysis: false,
max_interaction_order: 2,
}
}
pub fn fusion_strategy(mut self, strategy: &str) -> Self {
self.fusion_strategy = strategy.to_string();
self
}
pub fn modality_weights<I>(mut self, weights: I) -> Self
where
I: IntoIterator<Item = (&'static str, f64)>,
{
self.modality_weights = weights
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
self
}
pub fn modality_k<I>(mut self, k_values: I) -> Self
where
I: IntoIterator<Item = (&'static str, usize)>,
{
self.modality_k = k_values
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
self
}
pub fn cross_modal_analysis(mut self, enable: bool) -> Self {
self.cross_modal_analysis = enable;
self
}
pub fn cross_modal_threshold(mut self, threshold: Float) -> Self {
self.cross_modal_threshold = threshold;
self
}
pub fn normalize_modalities(mut self, normalize: bool) -> Self {
self.normalize_modalities = normalize;
self
}
pub fn handle_missing_modalities(mut self, handle: bool) -> Self {
self.handle_missing_modalities = handle;
self
}
pub fn min_modalities_required(mut self, min: usize) -> Self {
self.min_modalities_required = min;
self
}
pub fn missing_strategy(mut self, strategy: &str) -> Self {
self.missing_strategy = strategy.to_string();
self
}
pub fn k(mut self, k: usize) -> Self {
self.k = Some(k);
self
}
pub fn score_threshold(mut self, threshold: Float) -> Self {
self.score_threshold = threshold;
self
}
pub fn correlation_method(mut self, method: &str) -> Self {
self.correlation_method = method.to_string();
self
}
pub fn interaction_analysis(mut self, enable: bool) -> Self {
self.interaction_analysis = enable;
self
}
pub fn max_interaction_order(mut self, order: usize) -> Self {
self.max_interaction_order = order;
self
}
pub fn build(self) -> MultiModalFeatureSelector<Untrained> {
MultiModalFeatureSelector {
fusion_strategy: self.fusion_strategy,
modality_weights: self.modality_weights,
modality_k: self.modality_k,
cross_modal_analysis: self.cross_modal_analysis,
cross_modal_threshold: self.cross_modal_threshold,
normalize_modalities: self.normalize_modalities,
handle_missing_modalities: self.handle_missing_modalities,
min_modalities_required: self.min_modalities_required,
missing_strategy: self.missing_strategy,
k: self.k,
score_threshold: self.score_threshold,
correlation_method: self.correlation_method,
interaction_analysis: self.interaction_analysis,
max_interaction_order: self.max_interaction_order,
state: PhantomData,
trained_state: None,
}
}
}
impl Estimator for MultiModalFeatureSelector<Untrained> {
type Config = ();
type Error = sklears_core::error::SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Estimator for MultiModalFeatureSelector<Trained> {
type Config = ();
type Error = sklears_core::error::SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<HashMap<String, Array2<Float>>, Array1<Float>> for MultiModalFeatureSelector<Untrained> {
type Fitted = MultiModalFeatureSelector<Trained>;
fn fit(
self,
modalities: &HashMap<String, Array2<Float>>,
y: &Array1<Float>,
) -> Result<Self::Fitted> {
if modalities.is_empty() {
return Err(SklearsError::InvalidInput(
"At least one modality is required".to_string(),
));
}
if modalities.len() < self.min_modalities_required {
return Err(SklearsError::InvalidInput(format!(
"At least {} modalities are required",
self.min_modalities_required
)));
}
let n_samples = y.len();
for (modality_name, features) in modalities {
if features.nrows() != n_samples {
return Err(SklearsError::InvalidInput(format!(
"Modality '{}' has {} samples, expected {}",
modality_name,
features.nrows(),
n_samples
)));
}
}
let normalized_modalities = if self.normalize_modalities {
normalize_modalities(modalities)?
} else {
modalities.clone()
};
let mut modality_weights = self.modality_weights.clone();
if modality_weights.is_empty() {
let default_weight = 1.0 / modalities.len() as Float;
for modality_name in modalities.keys() {
modality_weights.insert(modality_name.clone(), default_weight);
}
}
let (
selected_features_per_modality,
combined_selected_features,
feature_scores_per_modality,
cross_modal_scores,
feature_mapping,
total_features,
modality_feature_counts,
) = match self.fusion_strategy.as_str() {
"early" => self.early_fusion_selection(&normalized_modalities, y, &modality_weights)?,
"late" => self.late_fusion_selection(&normalized_modalities, y, &modality_weights)?,
"hybrid" => {
self.hybrid_fusion_selection(&normalized_modalities, y, &modality_weights)?
}
_ => {
return Err(SklearsError::InvalidInput(format!(
"Unknown fusion strategy: {}",
self.fusion_strategy
)))
}
};
let trained_state = Trained {
fusion_strategy: self.fusion_strategy.clone(),
selected_features_per_modality,
combined_selected_features,
_feature_scores_per_modality: feature_scores_per_modality,
_cross_modal_scores: cross_modal_scores,
_modality_weights: modality_weights,
_feature_mapping: feature_mapping,
_total_features: total_features,
_modality_feature_counts: modality_feature_counts,
};
Ok(MultiModalFeatureSelector {
fusion_strategy: self.fusion_strategy,
modality_weights: self.modality_weights,
modality_k: self.modality_k,
cross_modal_analysis: self.cross_modal_analysis,
cross_modal_threshold: self.cross_modal_threshold,
normalize_modalities: self.normalize_modalities,
handle_missing_modalities: self.handle_missing_modalities,
min_modalities_required: self.min_modalities_required,
missing_strategy: self.missing_strategy,
k: self.k,
score_threshold: self.score_threshold,
correlation_method: self.correlation_method,
interaction_analysis: self.interaction_analysis,
max_interaction_order: self.max_interaction_order,
state: PhantomData,
trained_state: Some(trained_state),
})
}
}
impl Transform<HashMap<String, Array2<Float>>, Array2<Float>>
for MultiModalFeatureSelector<Trained>
{
fn transform(&self, modalities: &HashMap<String, Array2<Float>>) -> Result<Array2<Float>> {
let trained = self.trained_state.as_ref().ok_or_else(|| {
SklearsError::InvalidState("Selector must be fitted before transforming".to_string())
})?;
match trained.fusion_strategy.as_str() {
"early" => self.transform_early_fusion(modalities, trained),
"late" => self.transform_late_fusion(modalities, trained),
"hybrid" => self.transform_hybrid_fusion(modalities, trained),
_ => Err(SklearsError::InvalidState(format!(
"Unknown fusion strategy: {}",
trained.fusion_strategy
))),
}
}
}
impl MultiModalFeatureSelector<Untrained> {
fn early_fusion_selection(
&self,
modalities: &HashMap<String, Array2<Float>>,
y: &Array1<Float>,
modality_weights: &HashMap<String, Float>,
) -> FusionSelectionResult {
let (combined_features, feature_mapping, modality_feature_counts) =
concatenate_modalities(modalities)?;
let feature_scores = compute_weighted_feature_scores(
&combined_features,
y,
modalities,
modality_weights,
&self.correlation_method,
)?;
let cross_modal_scores = if self.cross_modal_analysis {
Some(compute_cross_modal_correlations(
modalities,
&self.correlation_method,
)?)
} else {
None
};
let selected_indices = if let Some(k) = self.k {
select_top_k_features(&feature_scores, k)
} else {
select_features_by_threshold(&feature_scores, self.score_threshold)
};
let mut selected_features_per_modality = HashMap::new();
let mut combined_selected_features = Vec::new();
for &global_idx in &selected_indices {
if let Some(&(ref modality, local_idx)) = feature_mapping.get(&global_idx) {
selected_features_per_modality
.entry(modality.clone())
.or_insert_with(Vec::new)
.push(local_idx);
combined_selected_features.push((modality.clone(), global_idx));
}
}
let mut feature_scores_per_modality = HashMap::new();
let mut current_idx = 0;
for (modality_name, features) in modalities {
let n_features = features.ncols();
let modality_scores = feature_scores
.slice(s![current_idx..current_idx + n_features])
.to_owned();
feature_scores_per_modality.insert(modality_name.clone(), modality_scores);
current_idx += n_features;
}
Ok((
selected_features_per_modality,
combined_selected_features,
feature_scores_per_modality,
cross_modal_scores,
feature_mapping,
combined_features.ncols(),
modality_feature_counts,
))
}
fn late_fusion_selection(
&self,
modalities: &HashMap<String, Array2<Float>>,
y: &Array1<Float>,
modality_weights: &HashMap<String, Float>,
) -> FusionSelectionResult {
let mut selected_features_per_modality = HashMap::new();
let mut feature_scores_per_modality = HashMap::new();
let mut combined_selected_features = Vec::new();
let mut feature_mapping = HashMap::new();
let mut modality_feature_counts = HashMap::new();
let mut total_features = 0;
let mut global_idx = 0;
for (modality_name, features) in modalities {
let n_features = features.ncols();
modality_feature_counts.insert(modality_name.clone(), n_features);
for local_idx in 0..n_features {
feature_mapping.insert(global_idx, (modality_name.clone(), local_idx));
global_idx += 1;
}
let weight = modality_weights.get(modality_name).cloned().unwrap_or(1.0);
let scores = compute_univariate_scores(features, y, &self.correlation_method)?;
let weighted_scores = scores.mapv(|s| s * weight);
let k = self
.modality_k
.get(modality_name)
.cloned()
.or(self.k.map(|total_k| total_k / modalities.len()))
.unwrap_or(n_features / 2);
let selected_indices = select_top_k_features(&weighted_scores, k.min(n_features));
for &local_idx in &selected_indices {
let global_feature_idx = total_features + local_idx;
combined_selected_features.push((modality_name.clone(), global_feature_idx));
}
selected_features_per_modality.insert(modality_name.clone(), selected_indices);
feature_scores_per_modality.insert(modality_name.clone(), weighted_scores);
total_features += n_features;
}
let cross_modal_scores = if self.cross_modal_analysis {
Some(compute_cross_modal_correlations(
modalities,
&self.correlation_method,
)?)
} else {
None
};
Ok((
selected_features_per_modality,
combined_selected_features,
feature_scores_per_modality,
cross_modal_scores,
feature_mapping,
total_features,
modality_feature_counts,
))
}
fn hybrid_fusion_selection(
&self,
modalities: &HashMap<String, Array2<Float>>,
y: &Array1<Float>,
modality_weights: &HashMap<String, Float>,
) -> FusionSelectionResult {
let (
early_selected,
_early_combined,
early_scores,
early_cross_modal,
early_mapping,
early_total,
early_counts,
) = self.early_fusion_selection(modalities, y, modality_weights)?;
let (
late_selected,
_late_combined,
late_scores,
late_cross_modal,
_late_mapping,
_late_total,
_late_counts,
) = self.late_fusion_selection(modalities, y, modality_weights)?;
let mut final_selected_per_modality = HashMap::new();
let mut final_combined_selected = Vec::new();
let mut final_scores_per_modality = HashMap::new();
for (modality_name, early_indices) in &early_selected {
let late_indices = late_selected
.get(modality_name)
.cloned()
.unwrap_or_default();
let early_modality_scores = &early_scores[modality_name];
let late_modality_scores = &late_scores[modality_name];
let combined_scores = (early_modality_scores + late_modality_scores) / 2.0;
let mut all_candidates: std::collections::HashSet<usize> =
early_indices.iter().cloned().collect();
all_candidates.extend(late_indices.iter());
let mut candidate_scores: Vec<(usize, Float)> = all_candidates
.iter()
.map(|&idx| (idx, combined_scores[idx]))
.collect();
candidate_scores
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let target_k = self
.modality_k
.get(modality_name)
.cloned()
.or(self.k.map(|total_k| total_k / modalities.len()))
.unwrap_or(candidate_scores.len() / 2);
let final_indices: Vec<usize> = candidate_scores
.into_iter()
.take(target_k.min(all_candidates.len()))
.map(|(idx, _)| idx)
.collect();
for &local_idx in &final_indices {
if let Some(base_offset) = compute_modality_offset(modality_name, modalities) {
final_combined_selected.push((modality_name.clone(), base_offset + local_idx));
}
}
final_selected_per_modality.insert(modality_name.clone(), final_indices);
final_scores_per_modality.insert(modality_name.clone(), combined_scores);
}
let cross_modal_scores = early_cross_modal.or(late_cross_modal);
Ok((
final_selected_per_modality,
final_combined_selected,
final_scores_per_modality,
cross_modal_scores,
early_mapping,
early_total,
early_counts,
))
}
}
impl MultiModalFeatureSelector<Trained> {
fn transform_early_fusion(
&self,
modalities: &HashMap<String, Array2<Float>>,
trained: &Trained,
) -> Result<Array2<Float>> {
let (combined_features, _, _) = concatenate_modalities(modalities)?;
let global_indices: Vec<usize> = trained
.combined_selected_features
.iter()
.map(|(_, global_idx)| *global_idx)
.collect();
if global_indices.is_empty() {
return Err(SklearsError::InvalidState(
"No features were selected".to_string(),
));
}
let selected_data = combined_features.select(Axis(1), &global_indices);
Ok(selected_data)
}
fn transform_late_fusion(
&self,
modalities: &HashMap<String, Array2<Float>>,
trained: &Trained,
) -> Result<Array2<Float>> {
let mut selected_features_owned = Vec::new();
for (modality_name, features) in modalities {
if let Some(selected_indices) =
trained.selected_features_per_modality.get(modality_name)
{
if !selected_indices.is_empty() {
let selected_modality_features = features.select(Axis(1), selected_indices);
selected_features_owned.push(selected_modality_features);
}
}
}
if selected_features_owned.is_empty() {
return Err(SklearsError::InvalidState(
"No features were selected from any modality".to_string(),
));
}
let selected_features_list: Vec<_> =
selected_features_owned.iter().map(|a| a.view()).collect();
let combined = concatenate(Axis(1), &selected_features_list).map_err(|e| {
SklearsError::InvalidInput(format!("Failed to concatenate selected features: {}", e))
})?;
Ok(combined)
}
fn transform_hybrid_fusion(
&self,
modalities: &HashMap<String, Array2<Float>>,
trained: &Trained,
) -> Result<Array2<Float>> {
self.transform_late_fusion(modalities, trained)
}
}
fn normalize_modalities(
modalities: &HashMap<String, Array2<Float>>,
) -> Result<HashMap<String, Array2<Float>>> {
let mut normalized = HashMap::new();
for (modality_name, features) in modalities {
let normalized_features = normalize_features(features)?;
normalized.insert(modality_name.clone(), normalized_features);
}
Ok(normalized)
}
fn normalize_features(features: &Array2<Float>) -> Result<Array2<Float>> {
let (n_samples, n_features) = features.dim();
let mut normalized = Array2::zeros((n_samples, n_features));
for j in 0..n_features {
let feature = features.column(j);
let mean = feature.sum() / n_samples as Float;
let variance = feature.mapv(|x| (x - mean).powi(2)).sum() / n_samples as Float;
let std_dev = variance.sqrt();
if std_dev > 1e-8 {
for i in 0..n_samples {
normalized[[i, j]] = (features[[i, j]] - mean) / std_dev;
}
} else {
normalized.column_mut(j).fill(0.0);
}
}
Ok(normalized)
}
fn concatenate_modalities(
modalities: &HashMap<String, Array2<Float>>,
) -> ConcatenateModalitiesResult {
if modalities.is_empty() {
return Err(SklearsError::InvalidInput(
"No modalities provided".to_string(),
));
}
let n_samples = modalities
.values()
.next()
.expect("operation should succeed")
.nrows();
let mut feature_views = Vec::new();
let mut feature_mapping = HashMap::new();
let mut modality_feature_counts = HashMap::new();
let mut global_idx = 0;
for (modality_name, features) in modalities {
if features.nrows() != n_samples {
return Err(SklearsError::InvalidInput(
format!("All modalities must have the same number of samples. Expected {}, got {} for modality '{}'",
n_samples, features.nrows(), modality_name)
));
}
let n_features = features.ncols();
modality_feature_counts.insert(modality_name.clone(), n_features);
for local_idx in 0..n_features {
feature_mapping.insert(global_idx, (modality_name.clone(), local_idx));
global_idx += 1;
}
feature_views.push(features.view());
}
let combined = concatenate(Axis(1), &feature_views).map_err(|e| {
SklearsError::InvalidInput(format!("Failed to concatenate modalities: {}", e))
})?;
Ok((combined, feature_mapping, modality_feature_counts))
}
fn compute_weighted_feature_scores(
features: &Array2<Float>,
y: &Array1<Float>,
modalities: &HashMap<String, Array2<Float>>,
modality_weights: &HashMap<String, Float>,
correlation_method: &str,
) -> Result<Array1<Float>> {
let (_, n_features) = features.dim();
let mut scores = Array1::zeros(n_features);
let mut feature_idx = 0;
for (modality_name, modality_features) in modalities {
let weight = modality_weights.get(modality_name).cloned().unwrap_or(1.0);
let modality_scores = compute_univariate_scores(modality_features, y, correlation_method)?;
let weighted_scores = modality_scores.mapv(|s| s * weight);
let end_idx = feature_idx + modality_features.ncols();
scores
.slice_mut(s![feature_idx..end_idx])
.assign(&weighted_scores);
feature_idx = end_idx;
}
Ok(scores)
}
fn compute_univariate_scores(
features: &Array2<Float>,
y: &Array1<Float>,
method: &str,
) -> Result<Array1<Float>> {
let (_, n_features) = features.dim();
let mut scores = Array1::zeros(n_features);
for j in 0..n_features {
let feature = features.column(j);
let score = match method {
"pearson" => compute_pearson_correlation(&feature, &y.view()),
"spearman" => compute_spearman_correlation(&feature, &y.view()),
"mutual_info" => compute_mutual_information(&feature, &y.view()),
_ => compute_pearson_correlation(&feature, &y.view()),
};
scores[j] = score.abs();
}
Ok(scores)
}
fn compute_cross_modal_correlations(
modalities: &HashMap<String, Array2<Float>>,
method: &str,
) -> Result<Array2<Float>> {
let modality_names: Vec<&String> = modalities.keys().collect();
let n_modalities = modality_names.len();
let mut correlations = Array2::zeros((n_modalities, n_modalities));
for i in 0..n_modalities {
for j in i..n_modalities {
if i == j {
correlations[[i, j]] = 1.0;
} else {
let features_i = &modalities[modality_names[i]];
let features_j = &modalities[modality_names[j]];
let correlation = compute_modality_correlation(features_i, features_j, method)?;
correlations[[i, j]] = correlation;
correlations[[j, i]] = correlation;
}
}
}
Ok(correlations)
}
fn compute_modality_correlation(
features_a: &Array2<Float>,
features_b: &Array2<Float>,
method: &str,
) -> Result<Float> {
let (_, n_features_a) = features_a.dim();
let (_, n_features_b) = features_b.dim();
let mut total_correlation = 0.0;
let mut count = 0;
for i in 0..n_features_a.min(10) {
for j in 0..n_features_b.min(10) {
let feature_a = features_a.column(i);
let feature_b = features_b.column(j);
let correlation = match method {
"pearson" => compute_pearson_correlation(&feature_a, &feature_b),
"spearman" => compute_spearman_correlation(&feature_a, &feature_b),
_ => compute_pearson_correlation(&feature_a, &feature_b),
};
total_correlation += correlation.abs();
count += 1;
}
}
Ok(if count > 0 {
total_correlation / count as Float
} else {
0.0
})
}
fn compute_pearson_correlation(x: &ArrayView1<Float>, y: &ArrayView1<Float>) -> Float {
let n = x.len();
if n != y.len() || n == 0 {
return 0.0;
}
let mean_x = x.sum() / n as Float;
let mean_y = y.sum() / n as Float;
let mut numerator = 0.0;
let mut sum_sq_x = 0.0;
let mut sum_sq_y = 0.0;
for i in 0..n {
let dx = x[i] - mean_x;
let dy = y[i] - mean_y;
numerator += dx * dy;
sum_sq_x += dx * dx;
sum_sq_y += dy * dy;
}
let denominator = (sum_sq_x * sum_sq_y).sqrt();
if denominator == 0.0 {
0.0
} else {
numerator / denominator
}
}
fn compute_spearman_correlation(x: &ArrayView1<Float>, y: &ArrayView1<Float>) -> Float {
let ranks_x = compute_ranks(x);
let ranks_y = compute_ranks(y);
compute_pearson_correlation(&ranks_x.view(), &ranks_y.view())
}
fn compute_mutual_information(x: &ArrayView1<Float>, y: &ArrayView1<Float>) -> Float {
compute_pearson_correlation(x, y).abs()
}
fn compute_ranks(values: &ArrayView1<Float>) -> Array1<Float> {
let n = values.len();
let mut indexed_values: Vec<(usize, Float)> =
values.iter().enumerate().map(|(i, &v)| (i, v)).collect();
indexed_values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let mut ranks = Array1::zeros(n);
for (rank, &(original_idx, _)) in indexed_values.iter().enumerate() {
ranks[original_idx] = rank as Float;
}
ranks
}
fn compute_modality_offset(
modality_name: &str,
modalities: &HashMap<String, Array2<Float>>,
) -> Option<usize> {
let mut offset = 0;
for (name, features) in modalities {
if name == modality_name {
return Some(offset);
}
offset += features.ncols();
}
None
}
fn select_top_k_features(scores: &Array1<Float>, k: usize) -> Vec<usize> {
let mut indexed_scores: Vec<(usize, Float)> = scores
.iter()
.enumerate()
.map(|(i, &score)| (i, score))
.collect();
indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
indexed_scores
.into_iter()
.take(k.min(scores.len()))
.map(|(i, _)| i)
.collect()
}
fn select_features_by_threshold(scores: &Array1<Float>, threshold: Float) -> Vec<usize> {
scores
.iter()
.enumerate()
.filter(|(_, &score)| score >= threshold)
.map(|(i, _)| i)
.collect()
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multi_modal_feature_selector_creation() {
let selector = MultiModalFeatureSelector::new();
assert_eq!(selector.fusion_strategy, "hybrid");
assert!(selector.cross_modal_analysis);
assert!(selector.normalize_modalities);
}
#[test]
fn test_multi_modal_feature_selector_builder() {
let selector = MultiModalFeatureSelector::builder()
.fusion_strategy("early")
.cross_modal_analysis(false)
.k(10)
.build();
assert_eq!(selector.fusion_strategy, "early");
assert!(!selector.cross_modal_analysis);
assert_eq!(selector.k, Some(10));
}
#[test]
fn test_concatenate_modalities() {
let mut modalities = HashMap::new();
let text_features = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
.expect("operation should succeed");
let image_features = Array2::from_shape_vec((3, 2), vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0])
.expect("operation should succeed");
modalities.insert("text".to_string(), text_features);
modalities.insert("image".to_string(), image_features);
let (combined, mapping, counts) =
concatenate_modalities(&modalities).expect("operation should succeed");
assert_eq!(combined.dim(), (3, 4));
assert_eq!(mapping.len(), 4);
assert_eq!(counts.len(), 2);
}
#[test]
fn test_fit_transform_early_fusion() {
let mut modalities = HashMap::new();
let text_features =
Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
.expect("operation should succeed");
let image_features =
Array2::from_shape_vec((4, 2), vec![2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
.expect("operation should succeed");
modalities.insert("text".to_string(), text_features);
modalities.insert("image".to_string(), image_features);
let target = Array1::from_vec(vec![0.0, 1.0, 1.0, 0.0]);
let selector = MultiModalFeatureSelector::builder()
.fusion_strategy("early")
.k(2)
.build();
let trained = selector
.fit(&modalities, &target)
.expect("operation should succeed");
let transformed = trained
.transform(&modalities)
.expect("operation should succeed");
assert_eq!(transformed.ncols(), 2);
assert_eq!(transformed.nrows(), 4);
}
#[test]
fn test_fit_transform_late_fusion() {
let mut modalities = HashMap::new();
let text_features = 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 image_features =
Array2::from_shape_vec((4, 2), vec![2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
.expect("operation should succeed");
modalities.insert("text".to_string(), text_features);
modalities.insert("image".to_string(), image_features);
let target = Array1::from_vec(vec![0.0, 1.0, 1.0, 0.0]);
let selector = MultiModalFeatureSelector::builder()
.fusion_strategy("late")
.modality_k([("text", 2), ("image", 1)])
.build();
let trained = selector
.fit(&modalities, &target)
.expect("operation should succeed");
let transformed = trained
.transform(&modalities)
.expect("operation should succeed");
assert_eq!(transformed.ncols(), 3); assert_eq!(transformed.nrows(), 4);
}
#[test]
fn test_normalize_features() {
let features = Array2::from_shape_vec((3, 2), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0])
.expect("operation should succeed");
let normalized = normalize_features(&features).expect("operation should succeed");
assert_eq!(normalized.dim(), (3, 2));
for j in 0..2 {
let column = normalized.column(j);
let mean = column.sum() / column.len() as Float;
assert!((mean).abs() < 1e-10);
}
}
#[test]
fn test_pearson_correlation() {
let x = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let y = Array1::from_vec(vec![2.0, 4.0, 6.0, 8.0, 10.0]);
let correlation = compute_pearson_correlation(&x.view(), &y.view());
assert!((correlation - 1.0).abs() < 1e-10);
}
}