1use crate::error::{Result, SklearsError};
59use futures_core::future::BoxFuture;
60use serde::{Deserialize, Serialize};
61use std::collections::HashMap;
62use std::sync::{Arc, RwLock};
63use std::time::{Duration, SystemTime};
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
71pub struct NodeId(pub String);
72
73impl NodeId {
74 pub fn new(id: impl Into<String>) -> Self {
76 Self(id.into())
77 }
78
79 pub fn as_str(&self) -> &str {
81 &self.0
82 }
83}
84
85impl std::fmt::Display for NodeId {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 write!(f, "{}", self.0)
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DistributedMessage {
94 pub id: String,
96 pub sender: NodeId,
98 pub receiver: NodeId,
100 pub message_type: MessageType,
102 pub payload: Vec<u8>,
104 pub timestamp: SystemTime,
106 pub priority: MessagePriority,
108 pub retry_count: u32,
110}
111
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114pub enum MessageType {
115 DataTransfer,
117 ParameterSync,
119 GradientAggregation,
121 Coordination,
123 HealthCheck,
125 FaultRecovery,
127 LoadBalance,
129 Custom(String),
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
135pub enum MessagePriority {
136 Low = 0,
138 Normal = 1,
140 High = 2,
142 Critical = 3,
144}
145
146pub trait MessagePassing: Send + Sync {
148 fn send_message(
150 &self,
151 target: NodeId,
152 message: DistributedMessage,
153 ) -> BoxFuture<'_, Result<()>>;
154
155 fn receive_message(&self) -> BoxFuture<'_, Result<DistributedMessage>>;
157
158 fn broadcast_message(&self, message: DistributedMessage) -> BoxFuture<'_, Result<()>>;
160
161 fn send_and_receive(
163 &self,
164 target: NodeId,
165 message: DistributedMessage,
166 ) -> BoxFuture<'_, Result<DistributedMessage>>;
167
168 fn has_pending_messages(&self) -> BoxFuture<'_, Result<bool>>;
170
171 fn pending_message_count(&self) -> BoxFuture<'_, Result<usize>>;
173
174 fn flush_outgoing(&self) -> BoxFuture<'_, Result<()>>;
176}
177
178pub trait ClusterNode: MessagePassing + Send + Sync {
180 fn node_id(&self) -> &NodeId;
182
183 fn cluster_nodes(&self) -> BoxFuture<'_, Result<Vec<NodeId>>>;
185
186 fn is_coordinator(&self) -> bool;
188
189 fn health_status(&self) -> BoxFuture<'_, Result<NodeHealth>>;
191
192 fn resources(&self) -> BoxFuture<'_, Result<NodeResources>>;
194
195 fn join_cluster(&mut self, coordinator: NodeId) -> BoxFuture<'_, Result<()>>;
197
198 fn leave_cluster(&mut self) -> BoxFuture<'_, Result<()>>;
200
201 fn handle_node_failure(&mut self, failed_node: NodeId) -> BoxFuture<'_, Result<()>>;
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct NodeHealth {
208 pub health_score: f64,
210 pub cpu_usage: f64,
212 pub memory_usage: f64,
214 pub network_latency: Duration,
216 pub last_heartbeat: SystemTime,
218 pub recent_errors: u32,
220 pub uptime: Duration,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct NodeResources {
227 pub cpu_cores: u32,
229 pub total_memory: u64,
231 pub available_memory: u64,
233 pub gpu_devices: Vec<GpuDevice>,
235 pub network_bandwidth: u64,
237 pub storage_capacity: u64,
239 pub tags: HashMap<String, String>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct GpuDevice {
246 pub device_id: u32,
248 pub name: String,
250 pub total_memory: u64,
252 pub available_memory: u64,
254 pub compute_capability: String,
256}
257
258pub trait DistributedEstimator: Send + Sync {
264 type TrainingData;
266
267 type PredictionInput;
269
270 type PredictionOutput;
272
273 type Parameters: Serialize + for<'de> Deserialize<'de>;
275
276 fn fit_distributed<'a>(
278 &'a mut self,
279 cluster: &'a dyn DistributedCluster,
280 training_data: &Self::TrainingData,
281 ) -> BoxFuture<'a, Result<()>>;
282
283 fn predict_distributed<'a>(
285 &'a self,
286 cluster: &dyn DistributedCluster,
287 input: &'a Self::PredictionInput,
288 ) -> BoxFuture<'a, Result<Self::PredictionOutput>>;
289
290 fn get_parameters(&self) -> Result<Self::Parameters>;
292
293 fn set_parameters(&mut self, params: Self::Parameters) -> Result<()>;
295
296 fn sync_parameters(&mut self, cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<()>>;
298
299 fn training_progress(&self) -> DistributedTrainingProgress;
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct DistributedTrainingProgress {
306 pub epoch: u32,
308 pub total_epochs: u32,
310 pub training_loss: f64,
312 pub validation_loss: Option<f64>,
314 pub samples_processed: u64,
316 pub start_time: SystemTime,
318 pub estimated_completion: Option<SystemTime>,
320 pub active_nodes: Vec<NodeId>,
322 pub node_statistics: HashMap<NodeId, NodeTrainingStats>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct NodeTrainingStats {
329 pub samples_processed: u64,
331 pub processing_rate: f64,
333 pub current_loss: f64,
335 pub memory_usage: u64,
337 pub cpu_utilization: f64,
339}
340
341pub trait DistributedCluster: Send + Sync {
343 fn active_nodes(&self) -> BoxFuture<'_, Result<Vec<NodeId>>>;
345
346 fn coordinator(&self) -> &NodeId;
348
349 fn configuration(&self) -> &ClusterConfiguration;
351
352 fn add_node(&mut self, node: NodeId) -> BoxFuture<'_, Result<()>>;
354
355 fn remove_node(&mut self, node: NodeId) -> BoxFuture<'_, Result<()>>;
357
358 fn rebalance_load(&mut self) -> BoxFuture<'_, Result<()>>;
360
361 fn cluster_health(&self) -> BoxFuture<'_, Result<ClusterHealth>>;
363
364 fn create_checkpoint(&self) -> BoxFuture<'_, Result<ClusterCheckpoint>>;
366
367 fn restore_checkpoint(&mut self, checkpoint: ClusterCheckpoint) -> BoxFuture<'_, Result<()>>;
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct ClusterConfiguration {
374 pub max_nodes: u32,
376 pub heartbeat_interval: Duration,
378 pub failure_timeout: Duration,
380 pub max_retries: u32,
382 pub load_balancing: LoadBalancingStrategy,
384 pub fault_tolerance: FaultToleranceMode,
386 pub consistency_level: ConsistencyLevel,
388}
389
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
392pub enum LoadBalancingStrategy {
393 RoundRobin,
395 ResourceBased,
397 LoadBased,
399 LocalityAware,
401 Custom(String),
403}
404
405#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
407pub enum FaultToleranceMode {
408 None,
410 BasicRetry,
412 CheckpointRecovery,
414 RedundantComputation,
416 Byzantine,
418}
419
420#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
422pub enum ConsistencyLevel {
423 None,
425 Eventual,
427 Strong,
429 Causal,
431 Sequential,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct ClusterHealth {
438 pub overall_health: f64,
440 pub healthy_nodes: u32,
442 pub failed_nodes: u32,
444 pub average_response_time: Duration,
446 pub total_throughput: f64,
448 pub resource_utilization: ClusterResourceUtilization,
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct ClusterResourceUtilization {
455 pub cpu_utilization: f64,
457 pub memory_utilization: f64,
459 pub network_utilization: f64,
461 pub storage_utilization: f64,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct ClusterCheckpoint {
468 pub checkpoint_id: String,
470 pub timestamp: SystemTime,
472 pub configuration: ClusterConfiguration,
474 pub node_states: HashMap<NodeId, NodeCheckpoint>,
476 pub cluster_state: Vec<u8>,
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct NodeCheckpoint {
483 pub node_id: NodeId,
485 pub state_data: Vec<u8>,
487 pub health: NodeHealth,
489 pub resources: NodeResources,
491}
492
493pub trait DistributedDataset: Send + Sync {
499 type Item;
501
502 type PartitionStrategy;
504
505 fn size(&self) -> u64;
507
508 fn partition_count(&self) -> u32;
510
511 fn partition<'a>(
513 &'a mut self,
514 cluster: &'a dyn DistributedCluster,
515 strategy: Self::PartitionStrategy,
516 ) -> BoxFuture<'a, Result<Vec<DistributedPartition<Self::Item>>>>;
517
518 fn get_partition(
520 &self,
521 partition_id: u32,
522 ) -> BoxFuture<'_, Result<DistributedPartition<Self::Item>>>;
523
524 fn repartition<'a>(
526 &'a mut self,
527 cluster: &'a dyn DistributedCluster,
528 new_strategy: Self::PartitionStrategy,
529 ) -> BoxFuture<'a, Result<()>>;
530
531 fn collect(&self, cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<Vec<Self::Item>>>;
533
534 fn partition_assignment(&self) -> HashMap<NodeId, Vec<u32>>;
536}
537
538#[derive(Debug, Clone)]
540pub struct DistributedPartition<T> {
541 pub partition_id: u32,
543 pub node_id: NodeId,
545 pub data: Vec<T>,
547 pub metadata: PartitionMetadata,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct PartitionMetadata {
554 pub item_count: u64,
556 pub size_bytes: u64,
558 pub schema: Option<String>,
560 pub created_at: SystemTime,
562 pub modified_at: SystemTime,
564 pub checksum: String,
566}
567
568#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
570pub enum PartitioningStrategy {
571 EvenSplit,
573 HashBased(u32),
575 RangeBased,
577 Random,
579 Stratified,
581 Custom(String),
583}
584
585pub trait ParameterServer: Send + Sync {
591 type Parameters: Serialize + for<'de> Deserialize<'de>;
593
594 fn initialize(&mut self, initial_params: Self::Parameters) -> BoxFuture<'_, Result<()>>;
596
597 fn get_parameters(&self) -> BoxFuture<'_, Result<Self::Parameters>>;
599
600 fn update_parameters(&mut self, gradients: Vec<Self::Parameters>) -> BoxFuture<'_, Result<()>>;
602
603 fn push_parameters(&self, cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<()>>;
605
606 fn pull_parameters(&mut self, cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<()>>;
608
609 fn aggregate_gradients(
611 &mut self,
612 gradients: Vec<Self::Parameters>,
613 ) -> BoxFuture<'_, Result<Self::Parameters>>;
614
615 fn apply_optimization(
617 &mut self,
618 aggregated_gradients: Self::Parameters,
619 ) -> BoxFuture<'_, Result<()>>;
620}
621
622#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
624pub enum GradientAggregation {
625 Average,
627 WeightedAverage,
629 FederatedAveraging,
631 ByzantineRobust,
633 Compressed,
635}
636
637pub trait FaultTolerance: Send + Sync {
643 fn detect_failure(
645 &self,
646 cluster: &dyn DistributedCluster,
647 ) -> BoxFuture<'_, Result<Vec<NodeId>>>;
648
649 fn recover_from_failure(
651 &mut self,
652 cluster: &mut dyn DistributedCluster,
653 failed_nodes: Vec<NodeId>,
654 ) -> BoxFuture<'_, Result<()>>;
655
656 fn create_checkpoint(
658 &self,
659 cluster: &dyn DistributedCluster,
660 ) -> BoxFuture<'_, Result<FaultToleranceCheckpoint>>;
661
662 fn restore_checkpoint(
664 &mut self,
665 cluster: &mut dyn DistributedCluster,
666 checkpoint: FaultToleranceCheckpoint,
667 ) -> BoxFuture<'_, Result<()>>;
668
669 fn replicate_data(
671 &self,
672 cluster: &dyn DistributedCluster,
673 data: Vec<u8>,
674 ) -> BoxFuture<'_, Result<()>>;
675
676 fn validate_integrity(
678 &self,
679 cluster: &dyn DistributedCluster,
680 ) -> BoxFuture<'_, Result<IntegrityReport>>;
681}
682
683#[derive(Debug, Clone, Serialize, Deserialize)]
685pub struct FaultToleranceCheckpoint {
686 pub id: String,
688 pub timestamp: SystemTime,
690 pub training_state: Vec<u8>,
692 pub model_parameters: Vec<u8>,
694 pub node_assignments: HashMap<NodeId, Vec<u32>>,
696 pub replication_map: HashMap<String, Vec<NodeId>>,
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize)]
702pub struct IntegrityReport {
703 pub integrity_score: f64,
705 pub data_consistency: bool,
707 pub parameter_sync: bool,
709 pub replication_health: f64,
711 pub inconsistencies: Vec<String>,
713 pub recommendations: Vec<String>,
715}
716
717pub struct DefaultDistributedCluster {
723 configuration: ClusterConfiguration,
725 coordinator: NodeId,
727 nodes: Arc<RwLock<HashMap<NodeId, Arc<dyn ClusterNode>>>>,
729 health_monitor: Arc<RwLock<ClusterHealth>>,
731}
732
733impl std::fmt::Debug for DefaultDistributedCluster {
734 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
735 f.debug_struct("DefaultDistributedCluster")
736 .field("configuration", &self.configuration)
737 .field("coordinator", &self.coordinator)
738 .field("nodes", &"<HashMap<NodeId, Arc<dyn ClusterNode>>>")
739 .field("health_monitor", &self.health_monitor)
740 .finish()
741 }
742}
743
744impl DefaultDistributedCluster {
745 pub fn new(coordinator: NodeId, configuration: ClusterConfiguration) -> Self {
747 Self {
748 configuration,
749 coordinator,
750 nodes: Arc::new(RwLock::new(HashMap::new())),
751 health_monitor: Arc::new(RwLock::new(ClusterHealth {
752 overall_health: 1.0,
753 healthy_nodes: 0,
754 failed_nodes: 0,
755 average_response_time: Duration::from_millis(10),
756 total_throughput: 0.0,
757 resource_utilization: ClusterResourceUtilization {
758 cpu_utilization: 0.0,
759 memory_utilization: 0.0,
760 network_utilization: 0.0,
761 storage_utilization: 0.0,
762 },
763 })),
764 }
765 }
766}
767
768impl DistributedCluster for DefaultDistributedCluster {
769 fn active_nodes(&self) -> BoxFuture<'_, Result<Vec<NodeId>>> {
770 Box::pin(async move {
771 let nodes = self.nodes.read().map_err(|_| {
772 SklearsError::InvalidOperation("Failed to acquire read lock on nodes".to_string())
773 })?;
774 Ok(nodes.keys().cloned().collect())
775 })
776 }
777
778 fn coordinator(&self) -> &NodeId {
779 &self.coordinator
780 }
781
782 fn configuration(&self) -> &ClusterConfiguration {
783 &self.configuration
784 }
785
786 fn add_node(&mut self, _node_id: NodeId) -> BoxFuture<'_, Result<()>> {
787 Box::pin(async move {
788 Ok(())
791 })
792 }
793
794 fn remove_node(&mut self, node_id: NodeId) -> BoxFuture<'_, Result<()>> {
795 Box::pin(async move {
796 let mut nodes = self.nodes.write().map_err(|_| {
797 SklearsError::InvalidOperation("Failed to acquire write lock on nodes".to_string())
798 })?;
799 nodes.remove(&node_id);
800 Ok(())
801 })
802 }
803
804 fn rebalance_load(&mut self) -> BoxFuture<'_, Result<()>> {
805 Box::pin(async move {
806 Ok(())
808 })
809 }
810
811 fn cluster_health(&self) -> BoxFuture<'_, Result<ClusterHealth>> {
812 Box::pin(async move {
813 let health = self.health_monitor.read().map_err(|_| {
814 SklearsError::InvalidOperation(
815 "Failed to acquire read lock on health monitor".to_string(),
816 )
817 })?;
818 Ok(health.clone())
819 })
820 }
821
822 fn create_checkpoint(&self) -> BoxFuture<'_, Result<ClusterCheckpoint>> {
823 Box::pin(async move {
824 let checkpoint = ClusterCheckpoint {
825 checkpoint_id: format!("checkpoint_{}", chrono::Utc::now().timestamp()),
826 timestamp: SystemTime::now(),
827 configuration: self.configuration.clone(),
828 node_states: HashMap::new(), cluster_state: Vec::new(), };
831 Ok(checkpoint)
832 })
833 }
834
835 fn restore_checkpoint(&mut self, _checkpoint: ClusterCheckpoint) -> BoxFuture<'_, Result<()>> {
836 Box::pin(async move {
837 Ok(())
839 })
840 }
841}
842
843impl Default for ClusterConfiguration {
844 fn default() -> Self {
845 Self {
846 max_nodes: 64,
847 heartbeat_interval: Duration::from_secs(30),
848 failure_timeout: Duration::from_secs(120),
849 max_retries: 3,
850 load_balancing: LoadBalancingStrategy::ResourceBased,
851 fault_tolerance: FaultToleranceMode::CheckpointRecovery,
852 consistency_level: ConsistencyLevel::Eventual,
853 }
854 }
855}
856
857#[derive(Debug)]
863pub struct DistributedLinearRegression {
864 parameters: Option<Vec<f64>>,
866 config: DistributedTrainingConfig,
868 progress: DistributedTrainingProgress,
870}
871
872#[derive(Debug, Clone)]
874pub struct DistributedTrainingConfig {
875 pub learning_rate: f64,
877 pub epochs: u32,
879 pub batch_size: u32,
881 pub aggregation: GradientAggregation,
883 pub checkpoint_frequency: u32,
885}
886
887impl Default for DistributedLinearRegression {
888 fn default() -> Self {
889 Self::new()
890 }
891}
892
893impl DistributedLinearRegression {
894 pub fn new() -> Self {
896 Self {
897 parameters: None,
898 config: DistributedTrainingConfig::default(),
899 progress: DistributedTrainingProgress {
900 epoch: 0,
901 total_epochs: 0,
902 training_loss: 0.0,
903 validation_loss: None,
904 samples_processed: 0,
905 start_time: SystemTime::now(),
906 estimated_completion: None,
907 active_nodes: Vec::new(),
908 node_statistics: HashMap::new(),
909 },
910 }
911 }
912
913 pub fn with_config(mut self, config: DistributedTrainingConfig) -> Self {
915 self.config = config;
916 self
917 }
918}
919
920impl Default for DistributedTrainingConfig {
921 fn default() -> Self {
922 Self {
923 learning_rate: 0.01,
924 epochs: 100,
925 batch_size: 32,
926 aggregation: GradientAggregation::Average,
927 checkpoint_frequency: 10,
928 }
929 }
930}
931
932impl DistributedEstimator for DistributedLinearRegression {
933 type TrainingData = (Vec<Vec<f64>>, Vec<f64>); type PredictionInput = Vec<Vec<f64>>;
935 type PredictionOutput = Vec<f64>;
936 type Parameters = Vec<f64>;
937
938 fn fit_distributed<'a>(
939 &'a mut self,
940 _cluster: &'a dyn DistributedCluster,
941 training_data: &Self::TrainingData,
942 ) -> BoxFuture<'a, Result<()>> {
943 let training_data = training_data.clone();
944 Box::pin(async move {
945 let (x, _y) = &training_data;
946
947 if self.parameters.is_none() {
949 let feature_count = x.first().map(|row| row.len()).unwrap_or(0);
950 self.parameters = Some(vec![0.0; feature_count + 1]); }
952
953 self.progress.total_epochs = self.config.epochs;
955 self.progress.start_time = SystemTime::now();
956 self.progress.active_nodes = vec![]; for epoch in 0..self.config.epochs {
960 self.progress.epoch = epoch;
961
962 if let Some(ref mut params) = self.parameters {
971 for param in params.iter_mut() {
973 *param += self.config.learning_rate * 0.1; }
975 }
976
977 self.progress.samples_processed += x.len() as u64;
979 self.progress.training_loss = (epoch as f64 * 0.1).exp().recip(); if epoch % self.config.checkpoint_frequency == 0 {
983 }
986 }
987
988 Ok(())
989 })
990 }
991
992 fn predict_distributed<'a>(
993 &'a self,
994 _cluster: &dyn DistributedCluster,
995 input: &'a Self::PredictionInput,
996 ) -> BoxFuture<'a, Result<Self::PredictionOutput>> {
997 Box::pin(async move {
998 let Some(ref params) = self.parameters else {
999 return Err(SklearsError::InvalidOperation(
1000 "Model not trained. Call fit_distributed first.".to_string(),
1001 ));
1002 };
1003
1004 let predictions = input
1006 .iter()
1007 .map(|features| {
1008 let mut prediction = *params.last().unwrap_or(&0.0); for (feature, weight) in features.iter().zip(params.iter()) {
1010 prediction += feature * weight;
1011 }
1012 prediction
1013 })
1014 .collect();
1015
1016 Ok(predictions)
1017 })
1018 }
1019
1020 fn get_parameters(&self) -> Result<Self::Parameters> {
1021 self.parameters
1022 .clone()
1023 .ok_or_else(|| SklearsError::InvalidOperation("Model not trained".to_string()))
1024 }
1025
1026 fn set_parameters(&mut self, params: Self::Parameters) -> Result<()> {
1027 self.parameters = Some(params);
1028 Ok(())
1029 }
1030
1031 fn sync_parameters(&mut self, _cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<()>> {
1032 Box::pin(async move {
1033 Ok(())
1035 })
1036 }
1037
1038 fn training_progress(&self) -> DistributedTrainingProgress {
1039 self.progress.clone()
1040 }
1041}
1042
1043#[derive(Debug)]
1049pub struct DistributedNumericalDataset {
1050 data: Vec<Vec<f64>>,
1052 partitions: Vec<DistributedPartition<Vec<f64>>>,
1054 assignment: HashMap<NodeId, Vec<u32>>,
1056}
1057
1058impl DistributedNumericalDataset {
1059 pub fn new(data: Vec<Vec<f64>>) -> Self {
1061 Self {
1062 data,
1063 partitions: Vec::new(),
1064 assignment: HashMap::new(),
1065 }
1066 }
1067
1068 fn flush_buckets_into_partitions(&mut self, nodes: &[NodeId], buckets: Vec<Vec<Vec<f64>>>) {
1076 for (node_id, partition_data) in nodes.iter().zip(buckets) {
1077 if partition_data.is_empty() {
1078 continue;
1079 }
1080
1081 let partition_id = self.partitions.len() as u32;
1082 self.partitions.push(build_numerical_partition(
1083 partition_id,
1084 node_id,
1085 partition_data,
1086 ));
1087 self.assignment
1088 .entry(node_id.clone())
1089 .or_default()
1090 .push(partition_id);
1091 }
1092 }
1093}
1094
1095#[inline]
1097fn fnv1a_mix(hash: &mut u64, bytes: &[u8]) {
1098 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
1099 for &byte in bytes {
1100 *hash ^= u64::from(byte);
1101 *hash = hash.wrapping_mul(FNV_PRIME);
1102 }
1103}
1104
1105fn numerical_partition_checksum(rows: &[Vec<f64>]) -> String {
1114 const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
1115
1116 let mut hash = FNV_OFFSET_BASIS;
1117 fnv1a_mix(&mut hash, &(rows.len() as u64).to_le_bytes());
1118 for row in rows {
1119 fnv1a_mix(&mut hash, &(row.len() as u64).to_le_bytes());
1120 for &value in row {
1121 let normalized = if value == 0.0 { 0.0_f64 } else { value };
1122 fnv1a_mix(&mut hash, &normalized.to_bits().to_le_bytes());
1123 }
1124 }
1125 format!("{hash:016x}")
1126}
1127
1128fn hash_numerical_row(row: &[f64], seed: u32) -> u64 {
1132 const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
1133
1134 let mut hash = FNV_OFFSET_BASIS;
1135 fnv1a_mix(&mut hash, &u64::from(seed).to_le_bytes());
1136 for &value in row {
1137 let normalized = if value == 0.0 { 0.0_f64 } else { value };
1138 fnv1a_mix(&mut hash, &normalized.to_bits().to_le_bytes());
1139 }
1140 hash
1141}
1142
1143fn build_numerical_partition(
1146 partition_id: u32,
1147 node_id: &NodeId,
1148 partition_data: Vec<Vec<f64>>,
1149) -> DistributedPartition<Vec<f64>> {
1150 let item_count = partition_data.len() as u64;
1151 let size_bytes = item_count * std::mem::size_of::<f64>() as u64;
1152 let checksum = numerical_partition_checksum(&partition_data);
1153 let now = SystemTime::now();
1154
1155 DistributedPartition {
1156 partition_id,
1157 node_id: node_id.clone(),
1158 metadata: PartitionMetadata {
1159 item_count,
1160 size_bytes,
1161 schema: Some("numerical_array".to_string()),
1162 created_at: now,
1163 modified_at: now,
1164 checksum,
1165 },
1166 data: partition_data,
1167 }
1168}
1169
1170impl DistributedDataset for DistributedNumericalDataset {
1171 type Item = Vec<f64>;
1172 type PartitionStrategy = PartitioningStrategy;
1173
1174 fn size(&self) -> u64 {
1175 self.data.len() as u64
1176 }
1177
1178 fn partition_count(&self) -> u32 {
1179 self.partitions.len() as u32
1180 }
1181
1182 fn partition<'a>(
1183 &'a mut self,
1184 cluster: &'a dyn DistributedCluster,
1185 strategy: Self::PartitionStrategy,
1186 ) -> BoxFuture<'a, Result<Vec<DistributedPartition<Self::Item>>>> {
1187 Box::pin(async move {
1188 let nodes = cluster.active_nodes().await?;
1189 let num_nodes = nodes.len();
1190
1191 if num_nodes == 0 {
1192 return Err(SklearsError::InvalidOperation(
1193 "No active nodes in cluster".to_string(),
1194 ));
1195 }
1196
1197 self.partitions.clear();
1198 self.assignment.clear();
1199
1200 match strategy {
1201 PartitioningStrategy::EvenSplit => {
1202 let chunk_size = self.data.len().div_ceil(num_nodes);
1205
1206 for (i, node_id) in nodes.iter().enumerate() {
1207 let start = i * chunk_size;
1208 let end = std::cmp::min(start + chunk_size, self.data.len());
1209
1210 if start < self.data.len() {
1211 let partition_data = self.data[start..end].to_vec();
1212 self.partitions.push(build_numerical_partition(
1213 i as u32,
1214 node_id,
1215 partition_data,
1216 ));
1217 self.assignment
1218 .entry(node_id.clone())
1219 .or_default()
1220 .push(i as u32);
1221 }
1222 }
1223 }
1224 PartitioningStrategy::RangeBased => {
1225 let mut order: Vec<usize> = (0..self.data.len()).collect();
1232 order.sort_by(|&a, &b| {
1233 let key_a = self.data[a].first().copied().unwrap_or(0.0);
1234 let key_b = self.data[b].first().copied().unwrap_or(0.0);
1235 key_a.total_cmp(&key_b).then_with(|| a.cmp(&b))
1236 });
1237
1238 let chunk_size = order.len().div_ceil(num_nodes);
1239 for (i, node_id) in nodes.iter().enumerate() {
1240 let start = i * chunk_size;
1241 let end = std::cmp::min(start + chunk_size, order.len());
1242
1243 if start < order.len() {
1244 let partition_data: Vec<Vec<f64>> = order[start..end]
1245 .iter()
1246 .map(|&idx| self.data[idx].clone())
1247 .collect();
1248 self.partitions.push(build_numerical_partition(
1249 i as u32,
1250 node_id,
1251 partition_data,
1252 ));
1253 self.assignment
1254 .entry(node_id.clone())
1255 .or_default()
1256 .push(i as u32);
1257 }
1258 }
1259 }
1260 PartitioningStrategy::HashBased(seed) => {
1261 let mut buckets: Vec<Vec<Vec<f64>>> = vec![Vec::new(); num_nodes];
1265 for row in &self.data {
1266 let node_index =
1267 (hash_numerical_row(row, seed) % num_nodes as u64) as usize;
1268 buckets[node_index].push(row.clone());
1269 }
1270 self.flush_buckets_into_partitions(&nodes, buckets);
1271 }
1272 PartitioningStrategy::Random => {
1273 use scirs2_core::random::thread_rng;
1278
1279 let mut rng = thread_rng();
1280 let mut buckets: Vec<Vec<Vec<f64>>> = vec![Vec::new(); num_nodes];
1281 for row in &self.data {
1282 let node_index = rng.gen_range(0..num_nodes);
1283 buckets[node_index].push(row.clone());
1284 }
1285 self.flush_buckets_into_partitions(&nodes, buckets);
1286 }
1287 PartitioningStrategy::Stratified => {
1288 use std::collections::BTreeMap;
1295
1296 let mut strata: BTreeMap<u64, Vec<usize>> = BTreeMap::new();
1297 for (idx, row) in self.data.iter().enumerate() {
1298 let label = row.last().copied().unwrap_or(0.0);
1299 let normalized = if label == 0.0 { 0.0_f64 } else { label };
1300 strata.entry(normalized.to_bits()).or_default().push(idx);
1301 }
1302
1303 let mut buckets: Vec<Vec<Vec<f64>>> = vec![Vec::new(); num_nodes];
1304 let mut cursor = 0_usize;
1305 for indices in strata.values() {
1306 for &idx in indices {
1307 buckets[cursor % num_nodes].push(self.data[idx].clone());
1308 cursor += 1;
1309 }
1310 }
1311 self.flush_buckets_into_partitions(&nodes, buckets);
1312 }
1313 PartitioningStrategy::Custom(name) => match name.as_str() {
1314 "round_robin" | "roundrobin" | "round-robin" => {
1315 let mut buckets: Vec<Vec<Vec<f64>>> = vec![Vec::new(); num_nodes];
1317 for (idx, row) in self.data.iter().enumerate() {
1318 buckets[idx % num_nodes].push(row.clone());
1319 }
1320 self.flush_buckets_into_partitions(&nodes, buckets);
1321 }
1322 other => {
1323 return Err(SklearsError::InvalidOperation(format!(
1324 "Custom partitioning strategy '{other}' is not registered"
1325 )));
1326 }
1327 },
1328 }
1329
1330 Ok(self.partitions.clone())
1331 })
1332 }
1333
1334 fn get_partition(
1335 &self,
1336 partition_id: u32,
1337 ) -> BoxFuture<'_, Result<DistributedPartition<Self::Item>>> {
1338 Box::pin(async move {
1339 self.partitions
1340 .get(partition_id as usize)
1341 .cloned()
1342 .ok_or_else(|| {
1343 SklearsError::InvalidOperation(format!("Partition {} not found", partition_id))
1344 })
1345 })
1346 }
1347
1348 fn repartition<'a>(
1349 &'a mut self,
1350 cluster: &'a dyn DistributedCluster,
1351 new_strategy: Self::PartitionStrategy,
1352 ) -> BoxFuture<'a, Result<()>> {
1353 Box::pin(async move {
1354 let collected_data = self.collect(cluster).await?;
1356 self.data = collected_data;
1357
1358 self.partition(cluster, new_strategy).await?;
1360
1361 Ok(())
1362 })
1363 }
1364
1365 fn collect(&self, _cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<Vec<Self::Item>>> {
1366 Box::pin(async move {
1367 let mut collected = Vec::new();
1368 for partition in &self.partitions {
1369 collected.extend(partition.data.clone());
1370 }
1371 Ok(collected)
1372 })
1373 }
1374
1375 fn partition_assignment(&self) -> HashMap<NodeId, Vec<u32>> {
1376 self.assignment.clone()
1377 }
1378}
1379
1380#[allow(non_snake_case)]
1381#[cfg(test)]
1382mod tests {
1383 use super::*;
1384
1385 #[test]
1386 fn test_node_id_creation() {
1387 let node_id = NodeId::new("worker-01");
1388 assert_eq!(node_id.as_str(), "worker-01");
1389 assert_eq!(node_id.to_string(), "worker-01");
1390 }
1391
1392 #[test]
1393 fn test_message_priority_ordering() {
1394 assert!(MessagePriority::Critical > MessagePriority::High);
1395 assert!(MessagePriority::High > MessagePriority::Normal);
1396 assert!(MessagePriority::Normal > MessagePriority::Low);
1397 }
1398
1399 #[test]
1400 fn test_cluster_configuration_default() {
1401 let config = ClusterConfiguration::default();
1402 assert_eq!(config.max_nodes, 64);
1403 assert_eq!(config.load_balancing, LoadBalancingStrategy::ResourceBased);
1404 assert_eq!(
1405 config.fault_tolerance,
1406 FaultToleranceMode::CheckpointRecovery
1407 );
1408 }
1409
1410 #[test]
1411 fn test_distributed_linear_regression_creation() {
1412 let model = DistributedLinearRegression::new();
1413 assert!(model.parameters.is_none());
1414 assert_eq!(model.progress.epoch, 0);
1415 }
1416
1417 #[test]
1418 fn test_distributed_dataset_size() {
1419 let data = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]];
1420 let dataset = DistributedNumericalDataset::new(data);
1421 assert_eq!(dataset.size(), 3);
1422 assert_eq!(dataset.partition_count(), 0); }
1424
1425 #[test]
1426 fn test_message_type_serialization() {
1427 let msg_type = MessageType::ParameterSync;
1428 let serialized = serde_json::to_string(&msg_type).unwrap_or_default();
1429 let deserialized: MessageType =
1430 serde_json::from_str(&serialized).expect("valid JSON operation");
1431 assert_eq!(msg_type, deserialized);
1432 }
1433
1434 #[test]
1435 fn test_partitioning_strategy_variants() {
1436 let strategies = vec![
1437 PartitioningStrategy::EvenSplit,
1438 PartitioningStrategy::HashBased(4),
1439 PartitioningStrategy::RangeBased,
1440 PartitioningStrategy::Random,
1441 PartitioningStrategy::Stratified,
1442 PartitioningStrategy::Custom("custom_strategy".to_string()),
1443 ];
1444
1445 for strategy in strategies {
1446 let serialized = serde_json::to_string(&strategy).unwrap_or_default();
1447 let _deserialized: PartitioningStrategy =
1448 serde_json::from_str(&serialized).expect("valid JSON operation");
1449 }
1450 }
1451
1452 #[test]
1453 fn test_distributed_training_config() {
1454 let config = DistributedTrainingConfig::default();
1455 assert_eq!(config.learning_rate, 0.01);
1456 assert_eq!(config.epochs, 100);
1457 assert_eq!(config.batch_size, 32);
1458 }
1459
1460 #[cfg(feature = "async_support")]
1461 #[tokio::test]
1462 async fn test_default_cluster_operations() {
1463 let coordinator = NodeId::new("coordinator");
1464 let config = ClusterConfiguration::default();
1465 let cluster = DefaultDistributedCluster::new(coordinator.clone(), config);
1466
1467 assert_eq!(cluster.coordinator(), &coordinator);
1468
1469 let nodes = cluster.active_nodes().await.expect("expected valid value");
1470 assert!(nodes.is_empty()); let health = cluster
1473 .cluster_health()
1474 .await
1475 .expect("expected valid value");
1476 assert_eq!(health.overall_health, 1.0);
1477 }
1478
1479 #[test]
1480 fn test_numerical_partition_checksum_is_deterministic() {
1481 let data = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1482 let first = numerical_partition_checksum(&data);
1483 let second = numerical_partition_checksum(&data);
1484 assert_eq!(
1485 first, second,
1486 "identical data must produce identical checksums"
1487 );
1488 }
1489
1490 #[test]
1491 fn test_numerical_partition_checksum_changes_with_data() {
1492 let data_a = vec![vec![1.0, 2.0, 3.0]];
1493 let data_b = vec![vec![1.0, 2.0, 4.0]];
1494 assert_ne!(
1495 numerical_partition_checksum(&data_a),
1496 numerical_partition_checksum(&data_b),
1497 "different data must produce different checksums"
1498 );
1499 }
1500
1501 #[test]
1502 fn test_numerical_partition_checksum_respects_row_structure() {
1503 let grouped_a = vec![vec![1.0, 2.0], vec![3.0]];
1505 let grouped_b = vec![vec![1.0], vec![2.0, 3.0]];
1506 assert_ne!(
1507 numerical_partition_checksum(&grouped_a),
1508 numerical_partition_checksum(&grouped_b)
1509 );
1510 }
1511
1512 #[test]
1513 fn test_numerical_partition_checksum_is_not_index_placeholder() {
1514 let checksum = numerical_partition_checksum(&[vec![1.0, 2.0]]);
1516 assert!(!checksum.starts_with("checksum_"));
1517 assert_eq!(checksum.len(), 16);
1518 }
1519
1520 #[test]
1521 fn test_hash_numerical_row_determinism_and_seed_sensitivity() {
1522 let row = vec![1.5, -2.25, 3.0];
1523 assert_eq!(hash_numerical_row(&row, 7), hash_numerical_row(&row, 7));
1524 assert_ne!(hash_numerical_row(&row, 7), hash_numerical_row(&row, 8));
1525 }
1526
1527 #[cfg(feature = "async_support")]
1528 struct TestCluster {
1529 coordinator: NodeId,
1530 configuration: ClusterConfiguration,
1531 nodes: Vec<NodeId>,
1532 }
1533
1534 #[cfg(feature = "async_support")]
1535 impl TestCluster {
1536 fn with_nodes(count: usize) -> Self {
1537 let nodes = (0..count)
1538 .map(|i| NodeId::new(format!("node-{i}")))
1539 .collect();
1540 Self {
1541 coordinator: NodeId::new("coordinator"),
1542 configuration: ClusterConfiguration::default(),
1543 nodes,
1544 }
1545 }
1546 }
1547
1548 #[cfg(feature = "async_support")]
1549 impl DistributedCluster for TestCluster {
1550 fn active_nodes(&self) -> BoxFuture<'_, Result<Vec<NodeId>>> {
1551 let nodes = self.nodes.clone();
1552 Box::pin(async move { Ok(nodes) })
1553 }
1554
1555 fn coordinator(&self) -> &NodeId {
1556 &self.coordinator
1557 }
1558
1559 fn configuration(&self) -> &ClusterConfiguration {
1560 &self.configuration
1561 }
1562
1563 fn add_node(&mut self, node: NodeId) -> BoxFuture<'_, Result<()>> {
1564 self.nodes.push(node);
1565 Box::pin(async move { Ok(()) })
1566 }
1567
1568 fn remove_node(&mut self, node: NodeId) -> BoxFuture<'_, Result<()>> {
1569 self.nodes.retain(|existing| existing != &node);
1570 Box::pin(async move { Ok(()) })
1571 }
1572
1573 fn rebalance_load(&mut self) -> BoxFuture<'_, Result<()>> {
1574 Box::pin(async move { Ok(()) })
1575 }
1576
1577 fn cluster_health(&self) -> BoxFuture<'_, Result<ClusterHealth>> {
1578 Box::pin(async move {
1579 Err(SklearsError::InvalidOperation(
1580 "cluster_health is unused in partition tests".to_string(),
1581 ))
1582 })
1583 }
1584
1585 fn create_checkpoint(&self) -> BoxFuture<'_, Result<ClusterCheckpoint>> {
1586 Box::pin(async move {
1587 Err(SklearsError::InvalidOperation(
1588 "create_checkpoint is unused in partition tests".to_string(),
1589 ))
1590 })
1591 }
1592
1593 fn restore_checkpoint(
1594 &mut self,
1595 _checkpoint: ClusterCheckpoint,
1596 ) -> BoxFuture<'_, Result<()>> {
1597 Box::pin(async move { Ok(()) })
1598 }
1599 }
1600
1601 #[cfg(feature = "async_support")]
1602 fn parse_node_index(node_id: &NodeId) -> usize {
1603 node_id
1604 .as_str()
1605 .strip_prefix("node-")
1606 .and_then(|suffix| suffix.parse().ok())
1607 .expect("test node ids follow the node-<index> convention")
1608 }
1609
1610 #[cfg(feature = "async_support")]
1611 #[tokio::test]
1612 async fn test_partition_without_nodes_errors() {
1613 let mut dataset = DistributedNumericalDataset::new(vec![vec![1.0]]);
1614 let cluster = TestCluster::with_nodes(0);
1615 let result = dataset
1616 .partition(&cluster, PartitioningStrategy::EvenSplit)
1617 .await;
1618 assert!(result.is_err(), "partitioning with no nodes must error");
1619 }
1620
1621 #[cfg(feature = "async_support")]
1622 #[tokio::test]
1623 async fn test_even_split_covers_all_items() {
1624 let data: Vec<Vec<f64>> = (0..10).map(|i| vec![i as f64, (i * 2) as f64]).collect();
1625 let mut dataset = DistributedNumericalDataset::new(data.clone());
1626 let cluster = TestCluster::with_nodes(3);
1627
1628 let partitions = dataset
1629 .partition(&cluster, PartitioningStrategy::EvenSplit)
1630 .await
1631 .expect("even split must succeed");
1632
1633 let mut collected: Vec<Vec<f64>> = partitions.iter().flat_map(|p| p.data.clone()).collect();
1634 collected.sort_by(|a, b| a[0].total_cmp(&b[0]));
1635 assert_eq!(collected, data, "union of partitions must cover all items");
1636 }
1637
1638 #[cfg(feature = "async_support")]
1639 #[tokio::test]
1640 async fn test_round_robin_custom_strategy_assignment() {
1641 let data: Vec<Vec<f64>> = (0..9).map(|i| vec![i as f64]).collect();
1642 let mut dataset = DistributedNumericalDataset::new(data);
1643 let cluster = TestCluster::with_nodes(3);
1644
1645 let partitions = dataset
1646 .partition(
1647 &cluster,
1648 PartitioningStrategy::Custom("round_robin".to_string()),
1649 )
1650 .await
1651 .expect("round-robin custom strategy must succeed");
1652
1653 assert_eq!(partitions.len(), 3);
1654 for partition in &partitions {
1657 let node_index = parse_node_index(&partition.node_id);
1658 for row in &partition.data {
1659 assert_eq!((row[0] as usize) % 3, node_index);
1660 }
1661 assert_eq!(partition.data.len(), 3);
1662 }
1663 }
1664
1665 #[cfg(feature = "async_support")]
1666 #[tokio::test]
1667 async fn test_hash_based_partitioning_is_deterministic() {
1668 let data: Vec<Vec<f64>> = (0..20).map(|i| vec![i as f64, (i % 4) as f64]).collect();
1669 let cluster = TestCluster::with_nodes(4);
1670
1671 let mut first_dataset = DistributedNumericalDataset::new(data.clone());
1672 let mut second_dataset = DistributedNumericalDataset::new(data.clone());
1673
1674 let first = first_dataset
1675 .partition(&cluster, PartitioningStrategy::HashBased(13))
1676 .await
1677 .expect("hash partitioning must succeed");
1678 let second = second_dataset
1679 .partition(&cluster, PartitioningStrategy::HashBased(13))
1680 .await
1681 .expect("hash partitioning must succeed");
1682
1683 let summarize = |partitions: &[DistributedPartition<Vec<f64>>]| {
1684 partitions
1685 .iter()
1686 .map(|p| (p.partition_id, p.data.clone()))
1687 .collect::<Vec<_>>()
1688 };
1689 assert_eq!(
1690 summarize(&first),
1691 summarize(&second),
1692 "hash partitioning must be deterministic for identical data and seed"
1693 );
1694
1695 for partition in &first {
1697 let node_index = parse_node_index(&partition.node_id);
1698 for row in &partition.data {
1699 assert_eq!((hash_numerical_row(row, 13) % 4) as usize, node_index);
1700 }
1701 }
1702
1703 let mut all: Vec<Vec<f64>> = first.iter().flat_map(|p| p.data.clone()).collect();
1705 all.sort_by(|a, b| a[0].total_cmp(&b[0]));
1706 assert_eq!(all, data);
1707 }
1708
1709 #[cfg(feature = "async_support")]
1710 #[tokio::test]
1711 async fn test_range_based_partitioning_orders_by_value() {
1712 let data: Vec<Vec<f64>> = vec![
1713 vec![9.0],
1714 vec![1.0],
1715 vec![7.0],
1716 vec![3.0],
1717 vec![5.0],
1718 vec![2.0],
1719 ];
1720 let mut dataset = DistributedNumericalDataset::new(data.clone());
1721 let cluster = TestCluster::with_nodes(3);
1722
1723 let partitions = dataset
1724 .partition(&cluster, PartitioningStrategy::RangeBased)
1725 .await
1726 .expect("range partitioning must succeed");
1727
1728 let ordered_keys: Vec<f64> = partitions
1730 .iter()
1731 .flat_map(|p| p.data.iter().map(|row| row[0]))
1732 .collect();
1733 let mut expected: Vec<f64> = data.iter().map(|row| row[0]).collect();
1734 expected.sort_by(|a, b| a.total_cmp(b));
1735 assert_eq!(
1736 ordered_keys, expected,
1737 "range partitions must be value-ordered"
1738 );
1739
1740 assert_eq!(partitions.len(), 3);
1742 for partition in &partitions {
1743 assert_eq!(partition.data.len(), 2);
1744 }
1745 }
1746
1747 #[cfg(feature = "async_support")]
1748 #[tokio::test]
1749 async fn test_stratified_partitioning_balances_classes() {
1750 let mut data: Vec<Vec<f64>> = Vec::new();
1752 for class in 0..3 {
1753 for k in 0..6 {
1754 data.push(vec![k as f64, class as f64]);
1755 }
1756 }
1757 let mut dataset = DistributedNumericalDataset::new(data.clone());
1758 let cluster = TestCluster::with_nodes(3);
1759
1760 let partitions = dataset
1761 .partition(&cluster, PartitioningStrategy::Stratified)
1762 .await
1763 .expect("stratified partitioning must succeed");
1764
1765 let total: usize = partitions.iter().map(|p| p.data.len()).sum();
1766 assert_eq!(total, data.len(), "stratified must not drop items");
1767
1768 for partition in &partitions {
1770 let mut class_counts: HashMap<i64, usize> = HashMap::new();
1771 for row in &partition.data {
1772 *class_counts.entry(row[1] as i64).or_insert(0) += 1;
1773 }
1774 assert_eq!(
1775 class_counts.len(),
1776 3,
1777 "each partition must hold all classes"
1778 );
1779 for count in class_counts.values() {
1780 assert_eq!(*count, 2, "each class must be evenly represented");
1781 }
1782 }
1783 }
1784
1785 #[cfg(feature = "async_support")]
1786 #[tokio::test]
1787 async fn test_random_partitioning_covers_all_items() {
1788 let data: Vec<Vec<f64>> = (0..30).map(|i| vec![i as f64]).collect();
1789 let mut dataset = DistributedNumericalDataset::new(data);
1790 let cluster = TestCluster::with_nodes(4);
1791
1792 let partitions = dataset
1793 .partition(&cluster, PartitioningStrategy::Random)
1794 .await
1795 .expect("random partitioning must succeed");
1796
1797 let mut all: Vec<f64> = partitions
1798 .iter()
1799 .flat_map(|p| p.data.iter().map(|row| row[0]))
1800 .collect();
1801 all.sort_by(|a, b| a.total_cmp(b));
1802 let expected: Vec<f64> = (0..30).map(|i| i as f64).collect();
1803 assert_eq!(
1804 all, expected,
1805 "random partitioning must not lose or duplicate items"
1806 );
1807 }
1808
1809 #[cfg(feature = "async_support")]
1810 #[tokio::test]
1811 async fn test_unknown_custom_strategy_errors() {
1812 let mut dataset = DistributedNumericalDataset::new(vec![vec![1.0], vec![2.0]]);
1813 let cluster = TestCluster::with_nodes(2);
1814
1815 let result = dataset
1816 .partition(
1817 &cluster,
1818 PartitioningStrategy::Custom("totally_unknown".to_string()),
1819 )
1820 .await;
1821 assert!(
1822 result.is_err(),
1823 "unknown custom strategy must return an honest error"
1824 );
1825 }
1826
1827 #[cfg(feature = "async_support")]
1828 #[tokio::test]
1829 async fn test_partition_checksums_reflect_content() {
1830 let cluster = TestCluster::with_nodes(1);
1831
1832 let mut dataset_a = DistributedNumericalDataset::new(vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
1833 let mut dataset_b = DistributedNumericalDataset::new(vec![vec![1.0, 2.0], vec![3.0, 5.0]]);
1834 let mut dataset_a_again =
1835 DistributedNumericalDataset::new(vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
1836
1837 let partitions_a = dataset_a
1838 .partition(&cluster, PartitioningStrategy::EvenSplit)
1839 .await
1840 .expect("partition a");
1841 let partitions_b = dataset_b
1842 .partition(&cluster, PartitioningStrategy::EvenSplit)
1843 .await
1844 .expect("partition b");
1845 let partitions_a_again = dataset_a_again
1846 .partition(&cluster, PartitioningStrategy::EvenSplit)
1847 .await
1848 .expect("partition a again");
1849
1850 assert_eq!(partitions_a.len(), 1);
1851 assert_eq!(partitions_b.len(), 1);
1852 assert_ne!(
1853 partitions_a[0].metadata.checksum, partitions_b[0].metadata.checksum,
1854 "different data must yield different checksums"
1855 );
1856 assert_eq!(
1857 partitions_a[0].metadata.checksum, partitions_a_again[0].metadata.checksum,
1858 "identical data must yield identical checksums"
1859 );
1860 assert!(!partitions_a[0].metadata.checksum.starts_with("checksum_"));
1861 }
1862}