use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::{Random, rng};
use scirs2_core::random::distributions::{Normal, StandardNormal};
use sklears_core::error::{Result, SklearsError};
use std::f64::consts::PI;
pub trait ManifoldGenerator {
fn generate_point(&self, parameters: &[f64]) -> Array1<f64>;
fn parameter_bounds(&self) -> Vec<(f64, f64)>;
}
pub fn make_custom_manifold<T: ManifoldGenerator>(
n_samples: usize,
generator: &T,
noise_level: f64,
random_state: Option<u64>,
) -> Result<Array2<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 bounds = generator.parameter_bounds();
let n_params = bounds.len();
if n_params == 0 {
return Err(SklearsError::InvalidInput(
"Manifold generator must have at least one parameter".to_string(),
));
}
let mut params = vec![0.0; n_params];
for (i, &(min_val, max_val)) in bounds.iter().enumerate() {
params[i] = rng.gen_range(min_val..max_val);
}
let first_point = generator.generate_point(¶ms);
let embedding_dim = first_point.len();
let mut samples = Array2::zeros((n_samples, embedding_dim));
for i in 0..n_samples {
for (j, &(min_val, max_val)) in bounds.iter().enumerate() {
params[j] = rng.gen_range(min_val..max_val);
}
let mut point = generator.generate_point(¶ms);
if noise_level > 0.0 {
for j in 0..embedding_dim {
let noise = rng.sample(Normal::new(0.0, noise_level).expect("sampling should succeed"));
point[j] += noise;
}
}
for j in 0..embedding_dim {
samples[[i, j]] = point[j];
}
}
Ok(samples)
}
pub fn make_n_sphere(
n_samples: usize,
n_dim: usize,
radius: f64,
noise_level: f64,
random_state: Option<u64>,
) -> Result<Array2<f64>> {
if n_samples == 0 || n_dim == 0 {
return Err(SklearsError::InvalidInput(
"n_samples and n_dim must be positive".to_string(),
));
}
if radius <= 0.0 {
return Err(SklearsError::InvalidInput(
"radius must be positive".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut samples = Array2::zeros((n_samples, n_dim));
for i in 0..n_samples {
let mut point = Array1::zeros(n_dim);
for j in 0..n_dim {
point[j] = rng.sample(StandardNormal);
}
let norm = point.iter().map(|&x| x * x).sum::<f64>().sqrt();
for j in 0..n_dim {
point[j] = radius * point[j] / norm;
if noise_level > 0.0 {
let noise = rng.sample(Normal::new(0.0, noise_level).expect("sampling should succeed"));
point[j] += noise;
}
samples[[i, j]] = point[j];
}
}
Ok(samples)
}
pub fn make_n_torus(
n_samples: usize,
n_dim: usize,
major_radius: f64,
minor_radius: f64,
noise_level: f64,
random_state: Option<u64>,
) -> Result<Array2<f64>> {
if n_samples == 0 || n_dim == 0 {
return Err(SklearsError::InvalidInput(
"n_samples and n_dim must be positive".to_string(),
));
}
if major_radius <= 0.0 || minor_radius <= 0.0 {
return Err(SklearsError::InvalidInput(
"radii must be positive".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let embedding_dim = 2 * n_dim;
let mut samples = Array2::zeros((n_samples, embedding_dim));
for i in 0..n_samples {
let mut angles = vec![0.0; n_dim];
for j in 0..n_dim {
angles[j] = rng.random_range(0.0..2.0 * PI);
}
for j in 0..n_dim {
let cos_angle = angles[j].cos();
let sin_angle = angles[j].sin();
samples[[i, j * 2]] = (major_radius + minor_radius * cos_angle) * angles[j].cos();
samples[[i, j * 2 + 1]] = (major_radius + minor_radius * cos_angle) * sin_angle;
if noise_level > 0.0 {
let noise1 = rng.sample(Normal::new(0.0, noise_level).expect("sampling should succeed"));
let noise2 = rng.sample(Normal::new(0.0, noise_level).expect("sampling should succeed"));
samples[[i, j * 2]] += noise1;
samples[[i, j * 2 + 1]] += noise2;
}
}
}
Ok(samples)
}
pub fn make_spatial_point_pattern(
n_points: usize,
pattern_type: &str,
intensity: f64,
clustering_parameter: f64,
domain_size: f64,
random_state: Option<u64>,
) -> Result<Array2<f64>> {
if n_points == 0 {
return Err(SklearsError::InvalidInput(
"n_points must be positive".to_string(),
));
}
if intensity <= 0.0 || domain_size <= 0.0 {
return Err(SklearsError::InvalidInput(
"intensity and domain_size must be positive".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut points = Array2::zeros((n_points, 2));
match pattern_type {
"random" => {
for i in 0..n_points {
points[[i, 0]] = rng.random_range(0.0..domain_size);
points[[i, 1]] = rng.random_range(0.0..domain_size);
}
}
"clustered" => {
let n_clusters = (n_points as f64 / clustering_parameter).max(1.0) as usize;
let points_per_cluster = n_points / n_clusters;
let mut cluster_centers = Array2::zeros((n_clusters, 2));
for i in 0..n_clusters {
cluster_centers[[i, 0]] = rng.random_range(0.0..domain_size);
cluster_centers[[i, 1]] = rng.random_range(0.0..domain_size);
}
for i in 0..n_points {
let cluster_idx = i / points_per_cluster;
let cluster_idx = cluster_idx.min(n_clusters - 1);
let center_x = cluster_centers[[cluster_idx, 0]];
let center_y = cluster_centers[[cluster_idx, 1]];
let cluster_std = domain_size / (10.0 * clustering_parameter.sqrt());
let dx = rng.sample(Normal::new(0.0, cluster_std).expect("sampling should succeed"));
let dy = rng.sample(Normal::new(0.0, cluster_std).expect("sampling should succeed"));
points[[i, 0]] = (center_x + dx).clamp(0.0, domain_size);
points[[i, 1]] = (center_y + dy).clamp(0.0, domain_size);
}
}
"regular" => {
let grid_size = (n_points as f64).sqrt() as usize;
let cell_size = domain_size / grid_size as f64;
let perturbation = cell_size * (1.0 - clustering_parameter) / 4.0;
for i in 0..n_points {
let row = i / grid_size;
let col = i % grid_size;
let base_x = (col as f64 + 0.5) * cell_size;
let base_y = (row as f64 + 0.5) * cell_size;
let dx = rng.gen_range(-perturbation..perturbation);
let dy = rng.gen_range(-perturbation..perturbation);
points[[i, 0]] = (base_x + dx).clamp(0.0, domain_size);
points[[i, 1]] = (base_y + dy).clamp(0.0, domain_size);
}
}
"inhibited" => {
let min_distance = clustering_parameter * domain_size / (n_points as f64).sqrt();
let mut placed_points = Vec::new();
for i in 0..n_points {
let mut attempts = 0;
let max_attempts = 1000;
loop {
let x = rng.random_range(0.0..domain_size);
let y = rng.random_range(0.0..domain_size);
let mut valid = true;
for &(px, py) in &placed_points {
let dist = ((x - px).powi(2) + (y - py).powi(2)).sqrt();
if dist < min_distance {
valid = false;
break;
}
}
if valid || attempts > max_attempts {
points[[i, 0]] = x;
points[[i, 1]] = y;
placed_points.push((x, y));
break;
}
attempts += 1;
}
}
}
_ => {
return Err(SklearsError::InvalidInput(format!(
"Unknown pattern_type: {}. Use 'random', 'clustered', 'regular', or 'inhibited'",
pattern_type
)));
}
}
Ok(points)
}
pub fn make_geostatistical_data(
n_points: usize,
domain_size: f64,
correlation_range: f64,
nugget: f64,
sill: f64,
correlation_function: &str,
random_state: Option<u64>,
) -> Result<(Array2<f64>, Array1<f64>)> {
if n_points == 0 {
return Err(SklearsError::InvalidInput(
"n_points must be positive".to_string(),
));
}
if domain_size <= 0.0 || correlation_range <= 0.0 || sill <= 0.0 {
return Err(SklearsError::InvalidInput(
"domain_size, correlation_range, and sill must be positive".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut coordinates = Array2::zeros((n_points, 2));
for i in 0..n_points {
coordinates[[i, 0]] = rng.random_range(0.0..domain_size);
coordinates[[i, 1]] = rng.random_range(0.0..domain_size);
}
let mut correlation_matrix = Array2::zeros((n_points, n_points));
for i in 0..n_points {
for j in 0..n_points {
let dx = coordinates[[i, 0]] - coordinates[[j, 0]];
let dy = coordinates[[i, 1]] - coordinates[[j, 1]];
let distance = (dx * dx + dy * dy).sqrt();
let correlation = match correlation_function {
"exponential" => {
if i == j {
sill
} else {
(sill - nugget) * (-distance / correlation_range).exp() +
if i == j { nugget } else { 0.0 }
}
}
"gaussian" => {
if i == j {
sill
} else {
(sill - nugget) * (-(distance / correlation_range).powi(2)).exp() +
if i == j { nugget } else { 0.0 }
}
}
"spherical" => {
if i == j {
sill
} else if distance <= correlation_range {
let h_over_a = distance / correlation_range;
(sill - nugget) * (1.0 - 1.5 * h_over_a + 0.5 * h_over_a.powi(3))
} else {
nugget
}
}
_ => {
return Err(SklearsError::InvalidInput(format!(
"Unknown correlation_function: {}. Use 'exponential', 'gaussian', or 'spherical'",
correlation_function
)));
}
};
correlation_matrix[[i, j]] = correlation;
}
}
let mut values = Array1::zeros(n_points);
for i in 0..n_points {
values[i] = rng.sample(StandardNormal);
}
let mut correlated_values = Array1::zeros(n_points);
for i in 0..n_points {
let mut sum = 0.0;
for j in 0..=i {
sum += correlation_matrix[[i, j]].sqrt() * values[j];
}
correlated_values[i] = sum / (i + 1) as f64; }
Ok((coordinates, correlated_values))
}