1use scirs2_core::random::prelude::*;
8use scirs2_core::random::ChaCha8Rng;
9use scirs2_core::random::{Rng, SeedableRng};
10use scirs2_core::SliceRandomExt;
11use std::collections::HashMap;
12use std::time::{Duration, Instant};
13use thiserror::Error;
14
15use crate::ising::{IsingError, IsingModel};
16use crate::simulator::{AnnealingParams, AnnealingSolution, QuantumAnnealingSimulator};
17
18#[derive(Error, Debug)]
20pub enum QbmError {
21 #[error("Ising error: {0}")]
23 IsingError(#[from] IsingError),
24
25 #[error("Invalid model: {0}")]
27 InvalidModel(String),
28
29 #[error("Training error: {0}")]
31 TrainingError(String),
32
33 #[error("Sampling error: {0}")]
35 SamplingError(String),
36
37 #[error("Data error: {0}")]
39 DataError(String),
40}
41
42pub type QbmResult<T> = Result<T, QbmError>;
44
45#[derive(Debug, Clone)]
57struct CdGradients {
58 weight_grad: Vec<Vec<f64>>,
60 visible_bias_grad: Vec<f64>,
62 hidden_bias_grad: Vec<f64>,
64 count: usize,
66}
67
68impl CdGradients {
69 fn new(num_visible: usize, num_hidden: usize) -> Self {
70 Self {
71 weight_grad: vec![vec![0.0; num_hidden]; num_visible],
72 visible_bias_grad: vec![0.0; num_visible],
73 hidden_bias_grad: vec![0.0; num_hidden],
74 count: 0,
75 }
76 }
77
78 fn accumulate(&mut self, v_pos: &[f64], h_pos: &[f64], v_neg: &[f64], h_neg: &[f64]) {
87 for (i, weight_row) in self.weight_grad.iter_mut().enumerate() {
88 let vp = v_pos[i];
89 let vn = v_neg[i];
90 for (j, w) in weight_row.iter_mut().enumerate() {
91 *w += vp.mul_add(h_pos[j], -(vn * h_neg[j]));
92 }
93 }
94 for (i, g) in self.visible_bias_grad.iter_mut().enumerate() {
95 *g += v_pos[i] - v_neg[i];
96 }
97 for (j, g) in self.hidden_bias_grad.iter_mut().enumerate() {
98 *g += h_pos[j] - h_neg[j];
99 }
100 self.count += 1;
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum UnitType {
107 Binary,
109 Gaussian,
111 Softmax,
113}
114
115#[derive(Debug, Clone)]
117pub struct LayerConfig {
118 pub num_units: usize,
120
121 pub unit_type: UnitType,
123
124 pub name: String,
126
127 pub bias_init_range: (f64, f64),
129
130 pub quantum_sampling: bool,
132}
133
134impl LayerConfig {
135 #[must_use]
137 pub const fn new(name: String, num_units: usize, unit_type: UnitType) -> Self {
138 Self {
139 num_units,
140 unit_type,
141 name,
142 bias_init_range: (-0.1, 0.1),
143 quantum_sampling: true,
144 }
145 }
146
147 #[must_use]
149 pub const fn with_bias_range(mut self, min: f64, max: f64) -> Self {
150 self.bias_init_range = (min, max);
151 self
152 }
153
154 #[must_use]
156 pub const fn with_quantum_sampling(mut self, enabled: bool) -> Self {
157 self.quantum_sampling = enabled;
158 self
159 }
160}
161
162#[derive(Debug)]
164pub struct QuantumRestrictedBoltzmannMachine {
165 visible_config: LayerConfig,
167
168 hidden_config: LayerConfig,
170
171 visible_biases: Vec<f64>,
173
174 hidden_biases: Vec<f64>,
176
177 weights: Vec<Vec<f64>>,
179
180 training_config: QbmTrainingConfig,
182
183 rng: ChaCha8Rng,
185
186 training_stats: Option<QbmTrainingStats>,
188}
189
190#[derive(Debug, Clone)]
192pub struct QbmTrainingConfig {
193 pub learning_rate: f64,
195
196 pub epochs: usize,
198
199 pub batch_size: usize,
201
202 pub k_steps: usize,
204
205 pub persistent_cd: bool,
207
208 pub weight_decay: f64,
210
211 pub momentum: f64,
213
214 pub annealing_params: AnnealingParams,
216
217 pub seed: Option<u64>,
219
220 pub error_threshold: Option<f64>,
222
223 pub log_frequency: usize,
225}
226
227impl Default for QbmTrainingConfig {
228 fn default() -> Self {
229 Self {
230 learning_rate: 0.01,
231 epochs: 100,
232 batch_size: 32,
233 k_steps: 1,
234 persistent_cd: false,
235 weight_decay: 0.0001,
236 momentum: 0.5,
237 annealing_params: AnnealingParams::default(),
238 seed: None,
239 error_threshold: None,
240 log_frequency: 10,
241 }
242 }
243}
244
245#[derive(Debug, Clone)]
247pub struct QbmTrainingStats {
248 pub total_training_time: Duration,
250
251 pub reconstruction_errors: Vec<f64>,
253
254 pub free_energy_diffs: Vec<f64>,
256
257 pub epochs_completed: usize,
259
260 pub final_reconstruction_error: f64,
262
263 pub converged: bool,
265
266 pub quantum_sampling_stats: QuantumSamplingStats,
268}
269
270#[derive(Debug, Clone)]
272pub struct QuantumSamplingStats {
273 pub total_sampling_time: Duration,
275
276 pub sampling_calls: usize,
278
279 pub average_annealing_energy: f64,
281
282 pub success_rate: f64,
284
285 pub classical_fallback_rate: f64,
287}
288
289impl Default for QuantumSamplingStats {
290 fn default() -> Self {
291 Self {
292 total_sampling_time: Duration::from_secs(0),
293 sampling_calls: 0,
294 average_annealing_energy: 0.0,
295 success_rate: 1.0,
296 classical_fallback_rate: 0.0,
297 }
298 }
299}
300
301#[derive(Debug, Clone)]
303pub struct TrainingSample {
304 pub data: Vec<f64>,
306
307 pub label: Option<Vec<f64>>,
309}
310
311impl TrainingSample {
312 #[must_use]
314 pub const fn new(data: Vec<f64>) -> Self {
315 Self { data, label: None }
316 }
317
318 #[must_use]
320 pub const fn labeled(data: Vec<f64>, label: Vec<f64>) -> Self {
321 Self {
322 data,
323 label: Some(label),
324 }
325 }
326}
327
328#[derive(Debug, Clone)]
330pub struct QbmInferenceResult {
331 pub reconstruction: Vec<f64>,
333
334 pub hidden_activations: Vec<f64>,
336
337 pub free_energy: f64,
339
340 pub probability: f64,
342}
343
344impl QuantumRestrictedBoltzmannMachine {
345 pub fn new(
347 visible_config: LayerConfig,
348 hidden_config: LayerConfig,
349 training_config: QbmTrainingConfig,
350 ) -> QbmResult<Self> {
351 if visible_config.num_units == 0 || hidden_config.num_units == 0 {
352 return Err(QbmError::InvalidModel(
353 "Layer sizes must be > 0".to_string(),
354 ));
355 }
356
357 let rng = match training_config.seed {
358 Some(seed) => ChaCha8Rng::seed_from_u64(seed),
359 None => ChaCha8Rng::seed_from_u64(thread_rng().random()),
360 };
361
362 let mut rbm = Self {
363 visible_config: visible_config.clone(),
364 hidden_config: hidden_config.clone(),
365 visible_biases: vec![0.0; visible_config.num_units],
366 hidden_biases: vec![0.0; hidden_config.num_units],
367 weights: vec![vec![0.0; hidden_config.num_units]; visible_config.num_units],
368 training_config,
369 rng,
370 training_stats: None,
371 };
372
373 rbm.initialize_parameters()?;
374 Ok(rbm)
375 }
376
377 fn initialize_parameters(&mut self) -> QbmResult<()> {
379 let (v_min, v_max) = self.visible_config.bias_init_range;
381 for bias in &mut self.visible_biases {
382 *bias = self.rng.random_range(v_min..v_max);
383 }
384
385 let (h_min, h_max) = self.hidden_config.bias_init_range;
387 for bias in &mut self.hidden_biases {
388 *bias = self.rng.random_range(h_min..h_max);
389 }
390
391 let fan_in = self.visible_config.num_units as f64;
393 let fan_out = self.hidden_config.num_units as f64;
394 let xavier_std = (2.0 / (fan_in + fan_out)).sqrt();
395
396 for i in 0..self.visible_config.num_units {
397 for j in 0..self.hidden_config.num_units {
398 self.weights[i][j] = self.rng.random_range(-xavier_std..xavier_std);
399 }
400 }
401
402 Ok(())
403 }
404
405 pub fn train(&mut self, dataset: &[TrainingSample]) -> QbmResult<()> {
407 if dataset.is_empty() {
408 return Err(QbmError::DataError("Dataset is empty".to_string()));
409 }
410
411 let expected_size = self.visible_config.num_units;
413 for (i, sample) in dataset.iter().enumerate() {
414 if sample.data.len() != expected_size {
415 return Err(QbmError::DataError(format!(
416 "Sample {} has {} features, expected {}",
417 i,
418 sample.data.len(),
419 expected_size
420 )));
421 }
422 }
423
424 println!("Starting QBM training with {} samples", dataset.len());
425
426 let start_time = Instant::now();
427 let mut reconstruction_errors = Vec::new();
428 let mut free_energy_diffs = Vec::new();
429 let mut quantum_stats = QuantumSamplingStats::default();
430
431 let mut weight_momentum =
433 vec![vec![0.0; self.hidden_config.num_units]; self.visible_config.num_units];
434 let mut visible_bias_momentum = vec![0.0; self.visible_config.num_units];
435 let mut hidden_bias_momentum = vec![0.0; self.hidden_config.num_units];
436
437 let mut persistent_chains = if self.training_config.persistent_cd {
439 Some(self.initialize_persistent_chains(self.training_config.batch_size)?)
440 } else {
441 None
442 };
443
444 for epoch in 0..self.training_config.epochs {
445 let epoch_start = Instant::now();
446 let mut epoch_error = 0.0;
447 let mut epoch_free_energy_diff = 0.0;
448 let mut num_batches = 0;
449
450 let mut shuffled_indices: Vec<usize> = (0..dataset.len()).collect();
452 use scirs2_core::random::prelude::*;
453 shuffled_indices.shuffle(&mut self.rng);
454
455 for batch_start in (0..dataset.len()).step_by(self.training_config.batch_size) {
457 let batch_end = (batch_start + self.training_config.batch_size).min(dataset.len());
458 let batch_indices = &shuffled_indices[batch_start..batch_end];
459
460 let batch_samples: Vec<&TrainingSample> =
461 batch_indices.iter().map(|&i| &dataset[i]).collect();
462
463 let (batch_error, batch_fe_diff, batch_gradients, batch_quantum_stats) =
467 self.contrastive_divergence_batch(&batch_samples, &mut persistent_chains)?;
468
469 self.update_parameters_with_momentum(
471 &batch_gradients,
472 &mut weight_momentum,
473 &mut visible_bias_momentum,
474 &mut hidden_bias_momentum,
475 )?;
476
477 epoch_error += batch_error;
478 epoch_free_energy_diff += batch_fe_diff;
479 quantum_stats.merge(&batch_quantum_stats);
480 num_batches += 1;
481 }
482
483 let avg_error = epoch_error / f64::from(num_batches);
484 let avg_fe_diff = epoch_free_energy_diff / f64::from(num_batches);
485
486 reconstruction_errors.push(avg_error);
487 free_energy_diffs.push(avg_fe_diff);
488
489 if epoch % self.training_config.log_frequency == 0 {
491 println!(
492 "Epoch {}: Error = {:.6}, FE Diff = {:.6}, Time = {:.2?}",
493 epoch,
494 avg_error,
495 avg_fe_diff,
496 epoch_start.elapsed()
497 );
498 }
499
500 if let Some(threshold) = self.training_config.error_threshold {
502 if avg_error < threshold {
503 println!("Converged at epoch {epoch} with error {avg_error:.6}");
504 break;
505 }
506 }
507 }
508
509 let total_time = start_time.elapsed();
510
511 self.training_stats = Some(QbmTrainingStats {
513 total_training_time: total_time,
514 reconstruction_errors: reconstruction_errors.clone(),
515 free_energy_diffs,
516 epochs_completed: reconstruction_errors.len(),
517 final_reconstruction_error: reconstruction_errors.last().copied().unwrap_or(0.0),
518 converged: self.training_config.error_threshold.map_or(false, |t| {
519 reconstruction_errors.last().unwrap_or(&f64::INFINITY) < &t
520 }),
521 quantum_sampling_stats: quantum_stats,
522 });
523
524 println!("Training completed in {total_time:.2?}");
525 Ok(())
526 }
527
528 fn contrastive_divergence_batch(
537 &mut self,
538 batch: &[&TrainingSample],
539 persistent_chains: &mut Option<Vec<Vec<f64>>>,
540 ) -> QbmResult<(f64, f64, CdGradients, QuantumSamplingStats)> {
541 let mut total_error = 0.0;
542 let mut total_fe_diff = 0.0;
543 let mut quantum_stats = QuantumSamplingStats::default();
544 let mut gradients =
545 CdGradients::new(self.visible_config.num_units, self.hidden_config.num_units);
546
547 for (i, sample) in batch.iter().enumerate() {
548 let hidden_probs_pos = self.sample_hidden_given_visible(&sample.data)?;
550
551 let (visible_recon, hidden_probs_neg, sampling_stats) =
553 if self.training_config.persistent_cd {
554 if let Some(ref mut chains) = persistent_chains {
555 let chain_index = i % chains.len();
556 let mut chain = chains[chain_index].clone();
557 for _ in 0..self.training_config.k_steps {
558 let h_probs = self.sample_hidden_given_visible(&chain)?;
559 let h_states = self.sample_binary_units(&h_probs)?;
560 chain = self.sample_visible_given_hidden(&h_states)?;
561 }
562 chains[chain_index] = chain.clone();
563 let h_probs = self.sample_hidden_given_visible(&chain)?;
564 (chain, h_probs, QuantumSamplingStats::default())
565 } else {
566 return Err(QbmError::TrainingError(
567 "Persistent chains not initialized".to_string(),
568 ));
569 }
570 } else {
571 let mut v_states = sample.data.clone();
573 let mut sampling_stats = QuantumSamplingStats::default();
574
575 for _ in 0..self.training_config.k_steps {
576 let h_probs = self.sample_hidden_given_visible(&v_states)?;
577 let h_states = if self.hidden_config.quantum_sampling {
578 let (states, stats) = self.quantum_sample_hidden(&h_probs)?;
579 sampling_stats.merge(&stats);
580 states
581 } else {
582 self.sample_binary_units(&h_probs)?
583 };
584
585 v_states = if self.visible_config.quantum_sampling {
586 let (states, stats) = self.quantum_sample_visible(&h_states)?;
587 sampling_stats.merge(&stats);
588 states
589 } else {
590 self.sample_visible_given_hidden(&h_states)?
591 };
592 }
593
594 let h_probs_neg = self.sample_hidden_given_visible(&v_states)?;
595 (v_states, h_probs_neg, sampling_stats)
596 };
597
598 gradients.accumulate(
604 &sample.data,
605 &hidden_probs_pos,
606 &visible_recon,
607 &hidden_probs_neg,
608 );
609
610 let error = sample
612 .data
613 .iter()
614 .zip(visible_recon.iter())
615 .map(|(orig, recon)| (orig - recon).powi(2))
616 .sum::<f64>()
617 / sample.data.len() as f64;
618
619 let fe_pos = self.free_energy(&sample.data)?;
621 let fe_neg = self.free_energy(&visible_recon)?;
622 let fe_diff = fe_pos - fe_neg;
623
624 total_error += error;
625 total_fe_diff += fe_diff;
626 quantum_stats.merge(&sampling_stats);
627 }
628
629 Ok((
630 total_error / batch.len() as f64,
631 total_fe_diff / batch.len() as f64,
632 gradients,
633 quantum_stats,
634 ))
635 }
636
637 fn update_parameters_with_momentum(
646 &mut self,
647 gradients: &CdGradients,
648 weight_momentum: &mut [Vec<f64>],
649 visible_bias_momentum: &mut [f64],
650 hidden_bias_momentum: &mut [f64],
651 ) -> QbmResult<()> {
652 let lr = self.training_config.learning_rate;
653 let momentum = self.training_config.momentum;
654 let decay = self.training_config.weight_decay;
655
656 let inv_count = if gradients.count > 0 {
658 1.0 / gradients.count as f64
659 } else {
660 return Ok(());
661 };
662
663 for i in 0..self.visible_config.num_units {
665 for j in 0..self.hidden_config.num_units {
666 let gradient = gradients.weight_grad[i][j] * inv_count;
667 weight_momentum[i][j] = momentum.mul_add(weight_momentum[i][j], lr * gradient);
668 self.weights[i][j] += weight_momentum[i][j] - lr * decay * self.weights[i][j];
670 }
671 }
672
673 for i in 0..self.visible_config.num_units {
675 let gradient = gradients.visible_bias_grad[i] * inv_count;
676 visible_bias_momentum[i] = momentum.mul_add(visible_bias_momentum[i], lr * gradient);
677 self.visible_biases[i] += visible_bias_momentum[i];
678 }
679
680 for j in 0..self.hidden_config.num_units {
682 let gradient = gradients.hidden_bias_grad[j] * inv_count;
683 hidden_bias_momentum[j] = momentum.mul_add(hidden_bias_momentum[j], lr * gradient);
684 self.hidden_biases[j] += hidden_bias_momentum[j];
685 }
686
687 Ok(())
688 }
689
690 fn initialize_persistent_chains(&mut self, num_chains: usize) -> QbmResult<Vec<Vec<f64>>> {
692 let mut chains = Vec::new();
693
694 for _ in 0..num_chains {
695 let chain: Vec<f64> = (0..self.visible_config.num_units)
696 .map(|_| if self.rng.random_bool(0.5) { 1.0 } else { 0.0 })
697 .collect();
698 chains.push(chain);
699 }
700
701 Ok(chains)
702 }
703
704 fn sample_hidden_given_visible(&self, visible: &[f64]) -> QbmResult<Vec<f64>> {
706 if visible.len() != self.visible_config.num_units {
707 return Err(QbmError::DataError("Visible size mismatch".to_string()));
708 }
709
710 let mut hidden_probs = vec![0.0; self.hidden_config.num_units];
711
712 for j in 0..self.hidden_config.num_units {
713 let activation = self.hidden_biases[j]
714 + visible
715 .iter()
716 .enumerate()
717 .map(|(i, &v)| v * self.weights[i][j])
718 .sum::<f64>();
719
720 hidden_probs[j] = match self.hidden_config.unit_type {
721 UnitType::Binary => sigmoid(activation),
722 UnitType::Gaussian => activation, UnitType::Softmax => activation, };
725 }
726
727 if self.hidden_config.unit_type == UnitType::Softmax {
729 softmax_normalize(&mut hidden_probs);
730 }
731
732 Ok(hidden_probs)
733 }
734
735 fn sample_visible_given_hidden(&self, hidden: &[f64]) -> QbmResult<Vec<f64>> {
737 if hidden.len() != self.hidden_config.num_units {
738 return Err(QbmError::DataError("Hidden size mismatch".to_string()));
739 }
740
741 let mut visible_probs = vec![0.0; self.visible_config.num_units];
742
743 for i in 0..self.visible_config.num_units {
744 let activation = self.visible_biases[i]
745 + hidden
746 .iter()
747 .enumerate()
748 .map(|(j, &h)| h * self.weights[i][j])
749 .sum::<f64>();
750
751 visible_probs[i] = match self.visible_config.unit_type {
752 UnitType::Binary => sigmoid(activation),
753 UnitType::Gaussian => activation,
754 UnitType::Softmax => activation,
755 };
756 }
757
758 if self.visible_config.unit_type == UnitType::Softmax {
759 softmax_normalize(&mut visible_probs);
760 }
761
762 Ok(visible_probs)
763 }
764
765 fn sample_binary_units(&mut self, probabilities: &[f64]) -> QbmResult<Vec<f64>> {
767 Ok(probabilities
768 .iter()
769 .map(|&p| if self.rng.random_bool(p) { 1.0 } else { 0.0 })
770 .collect())
771 }
772
773 fn quantum_sample_hidden(
775 &mut self,
776 probabilities: &[f64],
777 ) -> QbmResult<(Vec<f64>, QuantumSamplingStats)> {
778 let start_time = Instant::now();
779
780 let mut ising_model = IsingModel::new(probabilities.len());
782
783 for (i, &prob) in probabilities.iter().enumerate() {
785 let bias = -2.0 * (prob.ln() - (1.0 - prob).ln()); ising_model.set_bias(i, bias)?;
787 }
788
789 if let Ok(sample) = self.quantum_annealing_sample(&ising_model) {
791 let sampling_time = start_time.elapsed();
792 let annealing_energy = ising_model
796 .energy(&sample)
797 .map_err(|e| QbmError::SamplingError(e.to_string()))?;
798 let stats = QuantumSamplingStats {
799 total_sampling_time: sampling_time,
800 sampling_calls: 1,
801 average_annealing_energy: annealing_energy,
802 success_rate: 1.0,
803 classical_fallback_rate: 0.0,
804 };
805
806 let binary_sample = sample
808 .iter()
809 .map(|&s| if s > 0 { 1.0 } else { 0.0 })
810 .collect();
811
812 Ok((binary_sample, stats))
813 } else {
814 let sample = self.sample_binary_units(probabilities)?;
819 let spins: Vec<i8> = sample
820 .iter()
821 .map(|&v| if v > 0.5 { 1 } else { -1 })
822 .collect();
823 let annealing_energy = ising_model
824 .energy(&spins)
825 .map_err(|e| QbmError::SamplingError(e.to_string()))?;
826 let stats = QuantumSamplingStats {
827 total_sampling_time: start_time.elapsed(),
828 sampling_calls: 1,
829 average_annealing_energy: annealing_energy,
830 success_rate: 0.0,
831 classical_fallback_rate: 1.0,
832 };
833 Ok((sample, stats))
834 }
835 }
836
837 fn quantum_sample_visible(
839 &mut self,
840 hidden_states: &[f64],
841 ) -> QbmResult<(Vec<f64>, QuantumSamplingStats)> {
842 let visible_probs = self.sample_visible_given_hidden(hidden_states)?;
843 self.quantum_sample_hidden(&visible_probs) }
845
846 fn quantum_annealing_sample(&self, model: &IsingModel) -> QbmResult<Vec<i8>> {
848 let mut simulator =
849 QuantumAnnealingSimulator::new(self.training_config.annealing_params.clone())
850 .map_err(|e| QbmError::SamplingError(e.to_string()))?;
851
852 let result = simulator
853 .solve(model)
854 .map_err(|e| QbmError::SamplingError(e.to_string()))?;
855
856 Ok(result.best_spins)
857 }
858
859 fn free_energy(&self, visible: &[f64]) -> QbmResult<f64> {
861 if visible.len() != self.visible_config.num_units {
862 return Err(QbmError::DataError("Visible size mismatch".to_string()));
863 }
864
865 let visible_term: f64 = visible
867 .iter()
868 .zip(self.visible_biases.iter())
869 .map(|(&v, &b)| v * b)
870 .sum();
871
872 let hidden_term: f64 = (0..self.hidden_config.num_units)
874 .map(|j| {
875 let activation = self.hidden_biases[j]
876 + visible
877 .iter()
878 .enumerate()
879 .map(|(i, &v)| v * self.weights[i][j])
880 .sum::<f64>();
881 activation.exp().ln_1p()
882 })
883 .sum();
884
885 Ok(-(visible_term + hidden_term))
886 }
887
888 pub fn infer(&mut self, input: &[f64]) -> QbmResult<QbmInferenceResult> {
890 if input.len() != self.visible_config.num_units {
891 return Err(QbmError::DataError("Input size mismatch".to_string()));
892 }
893
894 let hidden_probs = self.sample_hidden_given_visible(input)?;
896 let hidden_states = self.sample_binary_units(&hidden_probs)?;
897
898 let reconstruction = self.sample_visible_given_hidden(&hidden_states)?;
900
901 let free_energy = self.free_energy(input)?;
903 let probability = (-free_energy).exp(); Ok(QbmInferenceResult {
906 reconstruction,
907 hidden_activations: hidden_probs,
908 free_energy,
909 probability,
910 })
911 }
912
913 pub fn generate_samples(&mut self, num_samples: usize) -> QbmResult<Vec<Vec<f64>>> {
915 let mut samples = Vec::new();
916
917 for _ in 0..num_samples {
918 let mut visible: Vec<f64> = (0..self.visible_config.num_units)
920 .map(|_| if self.rng.random_bool(0.5) { 1.0 } else { 0.0 })
921 .collect();
922
923 for _ in 0..100 {
925 let hidden_probs = self.sample_hidden_given_visible(&visible)?;
926 let hidden_states = self.sample_binary_units(&hidden_probs)?;
927 visible = self.sample_visible_given_hidden(&hidden_states)?;
928 }
929
930 samples.push(visible);
931 }
932
933 Ok(samples)
934 }
935
936 #[must_use]
938 pub const fn get_training_stats(&self) -> Option<&QbmTrainingStats> {
939 self.training_stats.as_ref()
940 }
941
942 pub fn save_model(&self, path: &str) -> QbmResult<()> {
948 let params = QbmModelParameters {
949 num_visible: self.visible_config.num_units,
950 num_hidden: self.hidden_config.num_units,
951 visible_biases: self.visible_biases.clone(),
952 hidden_biases: self.hidden_biases.clone(),
953 weights: self.weights.clone(),
954 };
955
956 let json = serde_json::to_string_pretty(¶ms)
957 .map_err(|e| QbmError::DataError(format!("Failed to serialize model: {e}")))?;
958
959 std::fs::write(path, json)
960 .map_err(|e| QbmError::DataError(format!("Failed to write model to {path}: {e}")))?;
961
962 Ok(())
963 }
964
965 pub fn load_model(&mut self, path: &str) -> QbmResult<()> {
973 let contents = std::fs::read_to_string(path)
974 .map_err(|e| QbmError::DataError(format!("Failed to read model from {path}: {e}")))?;
975
976 let params: QbmModelParameters = serde_json::from_str(&contents)
977 .map_err(|e| QbmError::DataError(format!("Failed to deserialize model: {e}")))?;
978
979 if params.num_visible != self.visible_config.num_units
980 || params.num_hidden != self.hidden_config.num_units
981 {
982 return Err(QbmError::InvalidModel(format!(
983 "Model dimensions ({}x{}) do not match this RBM ({}x{})",
984 params.num_visible,
985 params.num_hidden,
986 self.visible_config.num_units,
987 self.hidden_config.num_units
988 )));
989 }
990
991 if params.visible_biases.len() != self.visible_config.num_units
992 || params.hidden_biases.len() != self.hidden_config.num_units
993 || params.weights.len() != self.visible_config.num_units
994 || params
995 .weights
996 .iter()
997 .any(|row| row.len() != self.hidden_config.num_units)
998 {
999 return Err(QbmError::InvalidModel(
1000 "Stored parameter arrays have inconsistent shapes".to_string(),
1001 ));
1002 }
1003
1004 self.visible_biases = params.visible_biases;
1005 self.hidden_biases = params.hidden_biases;
1006 self.weights = params.weights;
1007
1008 Ok(())
1009 }
1010}
1011
1012#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1016struct QbmModelParameters {
1017 num_visible: usize,
1018 num_hidden: usize,
1019 visible_biases: Vec<f64>,
1020 hidden_biases: Vec<f64>,
1021 weights: Vec<Vec<f64>>,
1022}
1023
1024impl QuantumSamplingStats {
1025 fn merge(&mut self, other: &Self) {
1027 self.total_sampling_time += other.total_sampling_time;
1028 self.sampling_calls += other.sampling_calls;
1029
1030 if self.sampling_calls > 0 {
1031 let total_calls = self.sampling_calls as f64;
1032 self.average_annealing_energy = self.average_annealing_energy.mul_add(
1033 total_calls - other.sampling_calls as f64,
1034 other.average_annealing_energy * other.sampling_calls as f64,
1035 ) / total_calls;
1036
1037 self.success_rate = self.success_rate.mul_add(
1038 total_calls - other.sampling_calls as f64,
1039 other.success_rate * other.sampling_calls as f64,
1040 ) / total_calls;
1041
1042 self.classical_fallback_rate = self.classical_fallback_rate.mul_add(
1043 total_calls - other.sampling_calls as f64,
1044 other.classical_fallback_rate * other.sampling_calls as f64,
1045 ) / total_calls;
1046 }
1047 }
1048}
1049
1050fn sigmoid(x: f64) -> f64 {
1052 1.0 / (1.0 + (-x).exp())
1053}
1054
1055fn softmax_normalize(values: &mut [f64]) {
1057 let max_val = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1058 let sum: f64 = values.iter().map(|&x| (x - max_val).exp()).sum();
1059
1060 for value in values.iter_mut() {
1061 *value = (*value - max_val).exp() / sum;
1062 }
1063}
1064
1065pub fn create_binary_rbm(
1069 num_visible: usize,
1070 num_hidden: usize,
1071 training_config: QbmTrainingConfig,
1072) -> QbmResult<QuantumRestrictedBoltzmannMachine> {
1073 let visible_config = LayerConfig::new("visible".to_string(), num_visible, UnitType::Binary);
1074 let hidden_config = LayerConfig::new("hidden".to_string(), num_hidden, UnitType::Binary);
1075
1076 QuantumRestrictedBoltzmannMachine::new(visible_config, hidden_config, training_config)
1077}
1078
1079pub fn create_gaussian_bernoulli_rbm(
1081 num_visible: usize,
1082 num_hidden: usize,
1083 training_config: QbmTrainingConfig,
1084) -> QbmResult<QuantumRestrictedBoltzmannMachine> {
1085 let visible_config = LayerConfig::new("visible".to_string(), num_visible, UnitType::Gaussian);
1086 let hidden_config = LayerConfig::new("hidden".to_string(), num_hidden, UnitType::Binary);
1087
1088 QuantumRestrictedBoltzmannMachine::new(visible_config, hidden_config, training_config)
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::*;
1094
1095 #[test]
1096 fn test_rbm_creation() {
1097 let training_config = QbmTrainingConfig {
1098 epochs: 10,
1099 ..Default::default()
1100 };
1101
1102 let rbm = create_binary_rbm(4, 3, training_config).expect("failed to create binary RBM");
1103 assert_eq!(rbm.visible_config.num_units, 4);
1104 assert_eq!(rbm.hidden_config.num_units, 3);
1105 }
1106
1107 #[test]
1108 fn test_sigmoid_function() {
1109 assert!((sigmoid(0.0) - 0.5).abs() < 1e-10);
1110 assert!(sigmoid(10.0) > 0.99);
1111 assert!(sigmoid(-10.0) < 0.01);
1112 }
1113
1114 #[test]
1115 fn test_softmax_normalization() {
1116 let mut values = vec![1.0, 2.0, 3.0];
1117 softmax_normalize(&mut values);
1118
1119 let sum: f64 = values.iter().sum();
1120 assert!((sum - 1.0).abs() < 1e-10);
1121 assert!(values.iter().all(|&x| x > 0.0 && x < 1.0));
1122 }
1123
1124 #[test]
1125 fn test_training_sample_creation() {
1126 let sample = TrainingSample::new(vec![1.0, 0.0, 1.0]);
1127 assert_eq!(sample.data.len(), 3);
1128 assert!(sample.label.is_none());
1129
1130 let labeled_sample = TrainingSample::labeled(vec![1.0, 0.0], vec![1.0]);
1131 assert_eq!(labeled_sample.data.len(), 2);
1132 assert_eq!(
1133 labeled_sample
1134 .label
1135 .as_ref()
1136 .expect("label should exist")
1137 .len(),
1138 1
1139 );
1140 }
1141
1142 #[test]
1143 fn test_layer_config() {
1144 let config = LayerConfig::new("test".to_string(), 10, UnitType::Binary)
1145 .with_bias_range(-0.5, 0.5)
1146 .with_quantum_sampling(false);
1147
1148 assert_eq!(config.num_units, 10);
1149 assert_eq!(config.unit_type, UnitType::Binary);
1150 assert_eq!(config.bias_init_range, (-0.5, 0.5));
1151 assert!(!config.quantum_sampling);
1152 }
1153}