use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::{Random, rng};
use scirs2_core::random::distributions::{Normal, StandardNormal, Gamma};
use sklears_core::error::{Result, SklearsError};
use std::f64::consts::PI;
pub fn make_gaussian_mixture(
n_samples: usize,
means: &Array2<f64>,
covariances: &Array2<f64>, weights: &Array1<f64>,
random_state: Option<u64>,
) -> Result<(Array2<f64>, Array1<i32>)> {
if n_samples == 0 {
return Err(SklearsError::InvalidInput(
"n_samples must be positive".to_string(),
));
}
let n_components = means.nrows();
let n_features = means.ncols();
if n_components == 0 || n_features == 0 {
return Err(SklearsError::InvalidInput(
"means matrix cannot have zero dimensions".to_string(),
));
}
if covariances.shape() != means.shape() {
return Err(SklearsError::InvalidInput(
"covariances must have same shape as means".to_string(),
));
}
if weights.len() != n_components {
return Err(SklearsError::InvalidInput(
"weights must have same length as number of components".to_string(),
));
}
let weight_sum = weights.sum();
if (weight_sum - 1.0).abs() > 1e-10 {
return Err(SklearsError::InvalidInput(
"weights must sum to 1.0".to_string(),
));
}
if weights.iter().any(|&w| w < 0.0) {
return Err(SklearsError::InvalidInput(
"all weights must be non-negative".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut data = Array2::zeros((n_samples, n_features));
let mut labels = Array1::zeros(n_samples);
let mut cumulative_weights = Array1::zeros(n_components);
cumulative_weights[0] = weights[0];
for i in 1..n_components {
cumulative_weights[i] = cumulative_weights[i - 1] + weights[i];
}
for sample_idx in 0..n_samples {
let rand_val = rng.gen();
let component = cumulative_weights
.iter()
.position(|&cum_weight| rand_val <= cum_weight)
.unwrap_or(n_components - 1);
labels[sample_idx] = component as i32;
for feature_idx in 0..n_features {
let mean_val = means[[component, feature_idx]];
let std_val = covariances[[component, feature_idx]].sqrt();
let normal = Normal::new(mean_val, std_val).expect("operation should succeed");
data[[sample_idx, feature_idx]] = rng.sample(normal);
}
}
Ok((data, labels))
}
pub fn make_distribution_mixture(
n_samples: usize,
distribution_types: &[&str],
parameters: &Array2<f64>, weights: &Array1<f64>,
random_state: Option<u64>,
) -> Result<(Array1<f64>, Array1<i32>)> {
if n_samples == 0 {
return Err(SklearsError::InvalidInput(
"n_samples must be positive".to_string(),
));
}
let n_distributions = distribution_types.len();
if n_distributions == 0 {
return Err(SklearsError::InvalidInput(
"distribution_types cannot be empty".to_string(),
));
}
if parameters.nrows() != n_distributions {
return Err(SklearsError::InvalidInput(
"parameters must have same number of rows as distributions".to_string(),
));
}
if weights.len() != n_distributions {
return Err(SklearsError::InvalidInput(
"weights must have same length as number of distributions".to_string(),
));
}
let weight_sum = weights.sum();
if (weight_sum - 1.0).abs() > 1e-10 {
return Err(SklearsError::InvalidInput(
"weights must sum to 1.0".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut data = Array1::zeros(n_samples);
let mut labels = Array1::zeros(n_samples);
let mut cumulative_weights = Array1::zeros(n_distributions);
cumulative_weights[0] = weights[0];
for i in 1..n_distributions {
cumulative_weights[i] = cumulative_weights[i - 1] + weights[i];
}
for sample_idx in 0..n_samples {
let rand_val = rng.gen();
let dist_idx = cumulative_weights
.iter()
.position(|&cum_weight| rand_val <= cum_weight)
.unwrap_or(n_distributions - 1);
labels[sample_idx] = dist_idx as i32;
let dist_type = distribution_types[dist_idx];
let params = parameters.row(dist_idx);
data[sample_idx] = match dist_type {
"normal" => {
if params.len() < 2 {
return Err(SklearsError::InvalidInput(
"normal distribution requires 2 parameters (mean, std)".to_string(),
));
}
let normal = Normal::new(params[0], params[1]).expect("operation should succeed");
rng.sample(normal)
}
"uniform" => {
if params.len() < 2 {
return Err(SklearsError::InvalidInput(
"uniform distribution requires 2 parameters (low, high)".to_string(),
));
}
rng.gen_range(params[0]..params[1])
}
"exponential" => {
if params.len() < 1 {
return Err(SklearsError::InvalidInput(
"exponential distribution requires 1 parameter (lambda)".to_string(),
));
}
let u: f64 = rng.gen();
-u.ln() / params[0]
}
"gamma" => {
if params.len() < 2 {
return Err(SklearsError::InvalidInput(
"gamma distribution requires 2 parameters (shape, scale)".to_string(),
));
}
let gamma_dist = Gamma::new(params[0], params[1]).expect("operation should succeed");
rng.sample(gamma_dist)
}
_ => {
return Err(SklearsError::InvalidInput(format!(
"Unknown distribution type: {}",
dist_type
)));
}
};
}
Ok((data, labels))
}
pub fn make_multivariate_mixture(
n_samples: usize,
n_features: usize,
n_components: usize,
covariance_type: &str,
cluster_std: f64,
random_state: Option<u64>,
) -> Result<(Array2<f64>, Array1<i32>)> {
if n_samples == 0 || n_features == 0 || n_components == 0 {
return Err(SklearsError::InvalidInput(
"n_samples, n_features, and n_components must be positive".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut centers = Array2::zeros((n_components, n_features));
for i in 0..n_components {
for j in 0..n_features {
centers[[i, j]] = rng.random_range(-10.0..10.0);
}
}
let weight = 1.0 / n_components as f64;
let weights = Array1::from_elem(n_components, weight);
let covariances = match covariance_type {
"diagonal" => {
let mut covs = Array2::zeros((n_components, n_features));
for i in 0..n_components {
for j in 0..n_features {
covs[[i, j]] = cluster_std * cluster_std * rng.random_range(0.5..2.0);
}
}
covs
}
"spherical" => {
let mut covs = Array2::zeros((n_components, n_features));
for i in 0..n_components {
let variance = cluster_std * cluster_std * rng.random_range(0.5..2.0);
for j in 0..n_features {
covs[[i, j]] = variance;
}
}
covs
}
"tied" => {
let mut covs = Array2::zeros((n_components, n_features));
let base_variance = cluster_std * cluster_std;
for i in 0..n_components {
for j in 0..n_features {
covs[[i, j]] = base_variance;
}
}
covs
}
_ => {
return Err(SklearsError::InvalidInput(format!(
"Unknown covariance_type: {}. Use 'diagonal', 'spherical', or 'tied'",
covariance_type
)));
}
};
make_gaussian_mixture(n_samples, ¢ers, &covariances, &weights, random_state)
}
pub fn make_heavy_tailed_distribution(
n_samples: usize,
distribution_name: &str,
parameters: &[f64],
random_state: Option<u64>,
) -> Result<Array1<f64>> {
if n_samples == 0 {
return Err(SklearsError::InvalidInput(
"n_samples must be positive".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut samples = Array1::zeros(n_samples);
match distribution_name {
"student_t" => {
if parameters.len() < 3 {
return Err(SklearsError::InvalidInput(
"student_t requires 3 parameters: [degrees_of_freedom, location, scale]"
.to_string(),
));
}
let nu = parameters[0];
let location = parameters[1];
let scale = parameters[2];
if nu <= 0.0 || scale <= 0.0 {
return Err(SklearsError::InvalidInput(
"degrees_of_freedom and scale must be positive".to_string(),
));
}
for i in 0..n_samples {
let chi_sq = (0..nu as usize)
.map(|_| {
let normal: f64 = rng.sample(StandardNormal);
normal * normal
})
.sum::<f64>();
let normal: f64 = rng.sample(StandardNormal);
let t_sample = normal / (chi_sq / nu).sqrt();
samples[i] = location + scale * t_sample;
}
}
"pareto" => {
if parameters.len() < 2 {
return Err(SklearsError::InvalidInput(
"pareto requires 2 parameters: [shape, scale]".to_string(),
));
}
let shape = parameters[0];
let scale = parameters[1];
if shape <= 0.0 || scale <= 0.0 {
return Err(SklearsError::InvalidInput(
"shape and scale must be positive".to_string(),
));
}
for i in 0..n_samples {
let u: f64 = rng.gen();
samples[i] = scale * u.powf(-1.0 / shape);
}
}
"cauchy" => {
if parameters.len() < 2 {
return Err(SklearsError::InvalidInput(
"cauchy requires 2 parameters: [location, scale]".to_string(),
));
}
let location = parameters[0];
let scale = parameters[1];
if scale <= 0.0 {
return Err(SklearsError::InvalidInput(
"scale must be positive".to_string(),
));
}
for i in 0..n_samples {
let u: f64 = rng.random_range(-PI / 2.0..PI / 2.0);
samples[i] = location + scale * u.tan();
}
}
"levy" => {
if parameters.len() < 2 {
return Err(SklearsError::InvalidInput(
"levy requires 2 parameters: [location, scale]".to_string(),
));
}
let location = parameters[0];
let scale = parameters[1];
if scale <= 0.0 {
return Err(SklearsError::InvalidInput(
"scale must be positive".to_string(),
));
}
for i in 0..n_samples {
let normal: f64 = rng.sample(StandardNormal);
samples[i] = location + scale / (normal * normal);
}
}
"log_normal" => {
if parameters.len() < 2 {
return Err(SklearsError::InvalidInput(
"log_normal requires 2 parameters: [mu, sigma]".to_string(),
));
}
let mu = parameters[0];
let sigma = parameters[1];
if sigma <= 0.0 {
return Err(SklearsError::InvalidInput(
"sigma must be positive".to_string(),
));
}
let normal_dist = Normal::new(mu, sigma).expect("operation should succeed");
for i in 0..n_samples {
let normal_sample: f64 = rng.sample(normal_dist);
samples[i] = normal_sample.exp();
}
}
"weibull" => {
if parameters.len() < 2 {
return Err(SklearsError::InvalidInput(
"weibull requires 2 parameters: [shape, scale]".to_string(),
));
}
let shape = parameters[0];
let scale = parameters[1];
if shape <= 0.0 || scale <= 0.0 {
return Err(SklearsError::InvalidInput(
"shape and scale must be positive".to_string(),
));
}
for i in 0..n_samples {
let u: f64 = rng.gen();
samples[i] = scale * (-u.ln()).powf(1.0 / shape);
}
}
_ => {
return Err(SklearsError::InvalidInput(format!(
"Unknown heavy-tailed distribution: {}",
distribution_name
)));
}
}
Ok(samples)
}