1use alloc::vec;
39use alloc::vec::Vec;
40
41use super::astrocyte::{AstrocyteGate, AstrocyteMode};
42use super::eprop::{
43 compute_learning_signal_fixed, update_eligibility_fixed, update_output_weights_fixed,
44 update_pre_trace_fixed, update_weights_fixed,
45};
46use super::lif::{lif_step, surrogate_gradient_pwl};
47use super::readout::ReadoutNeuron;
48use super::spike_encoding::DeltaEncoderFixed;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77#[non_exhaustive]
78pub enum Precision {
79 Float,
83 Fixed,
87}
88
89#[allow(clippy::derivable_impls)]
93impl Default for Precision {
94 fn default() -> Self {
95 #[cfg(feature = "std")]
98 {
99 Precision::Float
100 }
101 #[cfg(not(feature = "std"))]
102 {
103 Precision::Fixed
104 }
105 }
106}
107
108#[derive(Debug, Clone)]
123pub struct SpikeNetFixedConfig {
124 pub n_input: usize,
126 pub n_hidden: usize,
128 pub n_output: usize,
130 pub alpha: i16,
132 pub kappa: i16,
134 pub kappa_out: i16,
136 pub eta: i16,
138 pub v_thr: i16,
140 pub gamma: i16,
142 pub spike_threshold: i16,
144 pub seed: u64,
146 pub weight_init_range: i16,
148 pub use_astrocyte: bool,
150 pub astrocyte_tau: f64,
152 pub astrocyte_mode: AstrocyteMode,
158}
159
160impl Default for SpikeNetFixedConfig {
161 fn default() -> Self {
162 Self {
163 n_input: 1,
164 n_hidden: 64,
165 n_output: 1,
166 alpha: 15565, kappa: 16220, kappa_out: 14746, eta: 16, v_thr: 8192, gamma: 4915, spike_threshold: 819, seed: 42,
174 weight_init_range: 1638, use_astrocyte: false,
176 astrocyte_tau: 1000.0,
177 astrocyte_mode: AstrocyteMode::WeightMod,
178 }
179 }
180}
181
182use crate::rng::xorshift64;
183
184#[inline]
188fn xorshift64_i16(state: &mut u64, range: i16) -> i16 {
189 let raw = xorshift64(state);
190 let abs_range = if range < 0 { -range } else { range };
191 if abs_range == 0 {
192 return 0;
193 }
194 let abs_u64 = abs_range as u64;
195 let modulus = 2 * abs_u64 + 1;
196 ((raw % modulus) as i16) - abs_range
197}
198
199pub struct SpikeNetFixed {
210 config: SpikeNetFixedConfig,
211 n_input_encoded: usize, membrane: Vec<i16>, spikes: Vec<u8>, prev_spikes: Vec<u8>, pre_trace_in: Vec<i16>, pre_trace_hid: Vec<i16>, w_input: Vec<i16>, w_recurrent: Vec<i16>, w_output: Vec<i16>, feedback: Vec<i16>, elig_in: Vec<i16>, elig_rec: Vec<i16>, readout: Vec<ReadoutNeuron>, encoder: DeltaEncoderFixed,
237
238 spike_buf: Vec<u8>, error_buf: Vec<i16>, astrocyte: Option<AstrocyteGate>,
246
247 n_samples: u64,
249}
250
251unsafe impl Send for SpikeNetFixed {}
253unsafe impl Sync for SpikeNetFixed {}
254
255impl SpikeNetFixed {
256 pub fn new(config: SpikeNetFixedConfig) -> Self {
261 let n_in = config.n_input;
262 let n_hid = config.n_hidden;
263 let n_out = config.n_output;
264 let n_enc = 2 * n_in;
265
266 let mut rng_state = if config.seed == 0 { 1 } else { config.seed };
267 let range = config.weight_init_range;
268
269 let w_input: Vec<i16> = (0..n_hid * n_enc)
271 .map(|_| xorshift64_i16(&mut rng_state, range))
272 .collect();
273
274 let w_recurrent: Vec<i16> = (0..n_hid * n_hid)
276 .map(|_| xorshift64_i16(&mut rng_state, range))
277 .collect();
278
279 let w_output: Vec<i16> = (0..n_out * n_hid)
281 .map(|_| xorshift64_i16(&mut rng_state, range))
282 .collect();
283
284 let feedback: Vec<i16> = (0..n_hid * n_out)
286 .map(|_| xorshift64_i16(&mut rng_state, range))
287 .collect();
288
289 let readout: Vec<ReadoutNeuron> = (0..n_out)
290 .map(|_| ReadoutNeuron::new(config.kappa_out))
291 .collect();
292
293 let encoder = DeltaEncoderFixed::new(n_in, config.spike_threshold);
294
295 let astrocyte = if config.use_astrocyte {
296 Some(AstrocyteGate::with_mode(
297 n_hid,
298 config.astrocyte_tau,
299 config.astrocyte_mode,
300 ))
301 } else {
302 None
303 };
304
305 Self {
306 n_input_encoded: n_enc,
307 membrane: vec![0; n_hid],
308 spikes: vec![0; n_hid],
309 prev_spikes: vec![0; n_hid],
310 pre_trace_in: vec![0; n_enc],
311 pre_trace_hid: vec![0; n_hid],
312 w_input,
313 w_recurrent,
314 w_output,
315 feedback,
316 elig_in: vec![0; n_hid * n_enc],
317 elig_rec: vec![0; n_hid * n_hid],
318 readout,
319 encoder,
320 spike_buf: vec![0; n_enc],
321 error_buf: vec![0; n_out],
322 astrocyte,
323 n_samples: 0,
324 config,
325 }
326 }
327
328 pub fn forward(&mut self, input_i16: &[i16]) {
337 let n_hid = self.config.n_hidden;
338 let n_enc = self.n_input_encoded;
339
340 self.encoder.encode(input_i16, &mut self.spike_buf);
342
343 self.prev_spikes.copy_from_slice(&self.spikes);
345
346 for j in 0..n_hid {
348 let mut current: i32 = 0;
350 let w_in_offset = j * n_enc;
351 for i in 0..n_enc {
352 if self.spike_buf[i] != 0 {
353 let w = match &self.astrocyte {
358 Some(astro) if astro.mode() == AstrocyteMode::WeightMod => {
359 astro.modulate_weight(j, self.w_input[w_in_offset + i])
360 }
361 _ => self.w_input[w_in_offset + i],
362 };
363 current += w as i32;
364 }
365 }
366
367 let w_rec_offset = j * n_hid;
369 for i in 0..n_hid {
370 if self.prev_spikes[i] != 0 {
371 current += self.w_recurrent[w_rec_offset + i] as i32;
372 }
373 }
374
375 let (v_new, spike) = lif_step(
377 self.membrane[j],
378 self.config.alpha,
379 current,
380 self.config.v_thr,
381 );
382 self.membrane[j] = v_new;
383 self.spikes[j] = spike as u8;
384 }
385
386 if let Some(ref mut astro) = self.astrocyte {
388 astro.update(&self.spikes);
389 }
390
391 let n_out = self.config.n_output;
393 for k in 0..n_out {
394 let w_out_offset = k * n_hid;
395 let mut weighted_input: i32 = 0;
396 for j in 0..n_hid {
397 if self.spikes[j] != 0 {
398 weighted_input += self.w_output[w_out_offset + j] as i32;
399 }
400 }
401 self.readout[k].step(weighted_input);
402 }
403 }
404
405 pub fn train_step(&mut self, input_i16: &[i16], target_i16: &[i16]) {
415 let n_hid = self.config.n_hidden;
416 let n_enc = self.n_input_encoded;
417 let n_out = self.config.n_output;
418
419 self.forward(input_i16);
421
422 for (k, &target_k) in target_i16.iter().enumerate().take(n_out) {
424 let readout_clamped = self.readout[k]
425 .output_i32()
426 .clamp(i16::MIN as i32, i16::MAX as i32) as i16;
427 self.error_buf[k] = target_k.saturating_sub(readout_clamped);
428 }
429
430 update_pre_trace_fixed(&mut self.pre_trace_in, &self.spike_buf, self.config.alpha);
432 update_pre_trace_fixed(&mut self.pre_trace_hid, &self.spikes, self.config.alpha);
433
434 for j in 0..n_hid {
436 let psi =
438 surrogate_gradient_pwl(self.membrane[j], self.config.v_thr, self.config.gamma);
439
440 let elig_in_start = j * n_enc;
442 let elig_in_end = elig_in_start + n_enc;
443 update_eligibility_fixed(
444 &mut self.elig_in[elig_in_start..elig_in_end],
445 psi,
446 &self.pre_trace_in,
447 self.config.kappa,
448 );
449
450 let elig_rec_start = j * n_hid;
452 let elig_rec_end = elig_rec_start + n_hid;
453 update_eligibility_fixed(
454 &mut self.elig_rec[elig_rec_start..elig_rec_end],
455 psi,
456 &self.pre_trace_hid,
457 self.config.kappa,
458 );
459
460 let fb_start = j * n_out;
462 let fb_end = fb_start + n_out;
463 let learning_signal = compute_learning_signal_fixed(
464 &self.feedback[fb_start..fb_end],
465 &self.error_buf[..n_out],
466 );
467
468 let eta_j = match &self.astrocyte {
473 Some(astro) if astro.mode() == AstrocyteMode::LearningRateGate => {
474 astro.effective_eta_q14(j, self.config.eta)
475 }
476 _ => self.config.eta,
477 };
478
479 let w_in_start = j * n_enc;
481 let w_in_end = w_in_start + n_enc;
482 update_weights_fixed(
483 &mut self.w_input[w_in_start..w_in_end],
484 &self.elig_in[elig_in_start..elig_in_end],
485 learning_signal,
486 eta_j,
487 );
488
489 let w_rec_start = j * n_hid;
491 let w_rec_end = w_rec_start + n_hid;
492 update_weights_fixed(
493 &mut self.w_recurrent[w_rec_start..w_rec_end],
494 &self.elig_rec[elig_rec_start..elig_rec_end],
495 learning_signal,
496 eta_j,
497 );
498 }
499
500 for k in 0..n_out {
502 let w_out_start = k * n_hid;
503 let w_out_end = w_out_start + n_hid;
504 update_output_weights_fixed(
505 &mut self.w_output[w_out_start..w_out_end],
506 self.error_buf[k],
507 &self.spikes,
508 self.config.eta,
509 );
510 }
511
512 self.n_samples += 1;
513 }
514
515 pub fn predict_raw(&self) -> Vec<i32> {
520 self.readout.iter().map(|r| r.output_i32()).collect()
521 }
522
523 pub fn predict_f64(&self, output_scale: f64) -> f64 {
529 if self.readout.is_empty() {
530 return 0.0;
531 }
532 self.readout[0].output_f64(output_scale)
533 }
534
535 pub fn predict_all_f64(&self, output_scale: f64) -> Vec<f64> {
541 self.readout
542 .iter()
543 .map(|r| r.output_f64(output_scale))
544 .collect()
545 }
546
547 pub fn n_samples_seen(&self) -> u64 {
549 self.n_samples
550 }
551
552 pub fn config(&self) -> &SpikeNetFixedConfig {
554 &self.config
555 }
556
557 pub fn n_hidden(&self) -> usize {
559 self.config.n_hidden
560 }
561
562 pub fn n_input_encoded(&self) -> usize {
564 self.n_input_encoded
565 }
566
567 pub fn hidden_spikes(&self) -> &[u8] {
569 &self.spikes
570 }
571
572 pub fn hidden_membrane(&self) -> &[i16] {
574 &self.membrane
575 }
576
577 pub fn memory_bytes(&self) -> usize {
581 let n_hid = self.config.n_hidden;
582 let n_enc = self.n_input_encoded;
583 let n_out = self.config.n_output;
584 let n_in = self.config.n_input;
585
586 let size_of_i16 = core::mem::size_of::<i16>();
587 let size_of_u8 = core::mem::size_of::<u8>();
588
589 let membrane = n_hid * size_of_i16;
591 let spikes = n_hid * size_of_u8;
592 let prev_spikes = n_hid * size_of_u8;
593
594 let pre_trace_in = n_enc * size_of_i16;
596 let pre_trace_hid = n_hid * size_of_i16;
597
598 let w_input = n_hid * n_enc * size_of_i16;
600 let w_recurrent = n_hid * n_hid * size_of_i16;
601 let w_output = n_out * n_hid * size_of_i16;
602 let feedback = n_hid * n_out * size_of_i16;
603
604 let elig_in = n_hid * n_enc * size_of_i16;
606 let elig_rec = n_hid * n_hid * size_of_i16;
607
608 let readout_size = n_out * core::mem::size_of::<ReadoutNeuron>();
610
611 let encoder_prev = n_in * size_of_i16;
613 let encoder_thr = n_in * size_of_i16;
614
615 let spike_buf = n_enc * size_of_u8;
617
618 let error_buf = n_out * size_of_i16;
620
621 let struct_overhead = core::mem::size_of::<Self>();
623
624 let vec_contents = membrane
626 + spikes
627 + prev_spikes
628 + pre_trace_in
629 + pre_trace_hid
630 + w_input
631 + w_recurrent
632 + w_output
633 + feedback
634 + elig_in
635 + elig_rec
636 + readout_size
637 + encoder_prev
638 + encoder_thr
639 + spike_buf
640 + error_buf;
641
642 struct_overhead + vec_contents
643 }
644
645 pub fn reset(&mut self) {
650 for v in self.membrane.iter_mut() {
652 *v = 0;
653 }
654 for s in self.spikes.iter_mut() {
655 *s = 0;
656 }
657 for s in self.prev_spikes.iter_mut() {
658 *s = 0;
659 }
660
661 for t in self.pre_trace_in.iter_mut() {
663 *t = 0;
664 }
665 for t in self.pre_trace_hid.iter_mut() {
666 *t = 0;
667 }
668 for e in self.elig_in.iter_mut() {
669 *e = 0;
670 }
671 for e in self.elig_rec.iter_mut() {
672 *e = 0;
673 }
674
675 for r in self.readout.iter_mut() {
677 r.reset();
678 }
679
680 self.encoder.reset();
682
683 for s in self.spike_buf.iter_mut() {
685 *s = 0;
686 }
687
688 for e in self.error_buf.iter_mut() {
690 *e = 0;
691 }
692
693 if let Some(ref mut astro) = self.astrocyte {
695 astro.reset();
696 }
697
698 let mut rng_state = if self.config.seed == 0 {
700 1
701 } else {
702 self.config.seed
703 };
704 let range = self.config.weight_init_range;
705
706 for w in self.w_input.iter_mut() {
707 *w = xorshift64_i16(&mut rng_state, range);
708 }
709 for w in self.w_recurrent.iter_mut() {
710 *w = xorshift64_i16(&mut rng_state, range);
711 }
712 for w in self.w_output.iter_mut() {
713 *w = xorshift64_i16(&mut rng_state, range);
714 }
715 for w in self.feedback.iter_mut() {
716 *w = xorshift64_i16(&mut rng_state, range);
717 }
718
719 self.n_samples = 0;
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726 use crate::snn::lif::{f64_to_q14, Q14_ONE};
727
728 fn default_small_config() -> SpikeNetFixedConfig {
729 SpikeNetFixedConfig {
730 n_input: 2,
731 n_hidden: 8,
732 n_output: 1,
733 alpha: f64_to_q14(0.95),
734 kappa: f64_to_q14(0.99),
735 kappa_out: f64_to_q14(0.9),
736 eta: f64_to_q14(0.01),
737 v_thr: f64_to_q14(0.5),
738 gamma: f64_to_q14(0.3),
739 spike_threshold: f64_to_q14(0.05),
740 seed: 42,
741 weight_init_range: f64_to_q14(0.1),
742 use_astrocyte: false,
743 astrocyte_tau: 1000.0,
744 astrocyte_mode: AstrocyteMode::WeightMod,
745 }
746 }
747
748 #[test]
749 fn construction_initializes_all_buffers() {
750 let config = default_small_config();
751 let net = SpikeNetFixed::new(config);
752
753 assert_eq!(net.membrane.len(), 8);
754 assert_eq!(net.spikes.len(), 8);
755 assert_eq!(net.n_input_encoded(), 4);
756 assert_eq!(net.w_input.len(), 8 * 4);
757 assert_eq!(net.w_recurrent.len(), 8 * 8);
758 assert_eq!(net.w_output.len(), 8);
759 assert_eq!(net.feedback.len(), 8);
760 assert_eq!(net.elig_in.len(), 8 * 4);
761 assert_eq!(net.elig_rec.len(), 8 * 8);
762 assert_eq!(net.readout.len(), 1);
763 assert_eq!(net.n_samples_seen(), 0);
764 }
765
766 #[test]
767 fn forward_does_not_crash() {
768 let config = default_small_config();
769 let mut net = SpikeNetFixed::new(config);
770
771 net.forward(&[f64_to_q14(0.5), f64_to_q14(-0.3)]);
773 net.forward(&[f64_to_q14(0.8), f64_to_q14(0.2)]);
775
776 let raw = net.predict_raw();
778 assert_eq!(raw.len(), 1, "should have one readout output");
779 }
780
781 #[test]
782 fn train_step_increments_counter() {
783 let config = default_small_config();
784 let mut net = SpikeNetFixed::new(config);
785
786 let input = [f64_to_q14(0.5), f64_to_q14(-0.3)];
787 let target = [f64_to_q14(0.7)];
788
789 net.train_step(&input, &target);
790 assert_eq!(net.n_samples_seen(), 1);
791
792 net.train_step(&input, &target);
793 assert_eq!(net.n_samples_seen(), 2);
794 }
795
796 #[test]
797 fn predictions_change_after_training() {
798 let config = SpikeNetFixedConfig {
799 n_input: 2,
800 n_hidden: 16,
801 n_output: 1,
802 alpha: f64_to_q14(0.9),
803 kappa: f64_to_q14(0.95),
804 kappa_out: f64_to_q14(0.85),
805 eta: f64_to_q14(0.05), v_thr: f64_to_q14(0.3), gamma: f64_to_q14(0.5),
808 spike_threshold: f64_to_q14(0.01), seed: 12345,
810 weight_init_range: f64_to_q14(0.2),
811 use_astrocyte: false,
812 astrocyte_tau: 1000.0,
813 astrocyte_mode: AstrocyteMode::WeightMod,
814 };
815
816 let mut net = SpikeNetFixed::new(config);
817 let scale = 1.0 / Q14_ONE as f64;
818
819 net.forward(&[0, 0]);
821 let pred_before = net.predict_f64(scale);
822
823 for step in 0..200 {
825 let x = if step % 2 == 0 {
826 [f64_to_q14(0.8), f64_to_q14(-0.5)]
827 } else {
828 [f64_to_q14(-0.3), f64_to_q14(0.6)]
829 };
830 let target = if step % 2 == 0 {
831 [f64_to_q14(1.0)]
832 } else {
833 [f64_to_q14(-1.0)]
834 };
835 net.train_step(&x, &target);
836 }
837
838 let pred_after = net.predict_f64(scale);
839
840 assert!(
841 (pred_after - pred_before).abs() > 1e-10,
842 "prediction should change after training: before={}, after={}",
843 pred_before,
844 pred_after
845 );
846 }
847
848 #[test]
849 fn reset_restores_initial_state() {
850 let config = default_small_config();
851 let mut net = SpikeNetFixed::new(config.clone());
852 let fresh = SpikeNetFixed::new(config);
853
854 net.train_step(&[1000, -500], &[2000]);
856 net.train_step(&[-1000, 500], &[-2000]);
857 assert!(net.n_samples_seen() > 0);
858
859 net.reset();
861
862 assert_eq!(net.n_samples_seen(), 0);
864 assert_eq!(net.membrane, fresh.membrane);
865 assert_eq!(net.spikes, fresh.spikes);
866 assert_eq!(
867 net.w_input, fresh.w_input,
868 "weights should be re-initialized from seed"
869 );
870 assert_eq!(net.w_recurrent, fresh.w_recurrent);
871 assert_eq!(net.w_output, fresh.w_output);
872 assert_eq!(net.feedback, fresh.feedback);
873 }
874
875 #[test]
876 fn memory_bytes_is_reasonable() {
877 let config = SpikeNetFixedConfig {
878 n_input: 10,
879 n_hidden: 64,
880 n_output: 1,
881 ..SpikeNetFixedConfig::default()
882 };
883 let net = SpikeNetFixed::new(config);
884 let mem = net.memory_bytes();
885
886 assert!(
892 mem > 20_000,
893 "memory should be at least 20KB for 10-in/64-hid/1-out, got {}",
894 mem
895 );
896 assert!(
897 mem < 100_000,
898 "memory should be under 100KB for small network, got {}",
899 mem
900 );
901 }
902
903 #[test]
904 fn deterministic_with_same_seed() {
905 let config = default_small_config();
906 let mut net1 = SpikeNetFixed::new(config.clone());
907 let mut net2 = SpikeNetFixed::new(config);
908
909 let input = [f64_to_q14(0.3), f64_to_q14(-0.7)];
910 let target = [f64_to_q14(0.5)];
911
912 for _ in 0..10 {
913 net1.train_step(&input, &target);
914 net2.train_step(&input, &target);
915 }
916
917 let scale = 1.0 / Q14_ONE as f64;
918 let p1 = net1.predict_f64(scale);
919 let p2 = net2.predict_f64(scale);
920 assert_eq!(p1, p2, "same seed should produce identical predictions");
921 }
922
923 #[test]
924 fn multi_output_network() {
925 let config = SpikeNetFixedConfig {
926 n_input: 3,
927 n_hidden: 8,
928 n_output: 3,
929 ..SpikeNetFixedConfig::default()
930 };
931 let mut net = SpikeNetFixed::new(config);
932
933 net.forward(&[1000, -500, 200]);
934 net.forward(&[1500, 0, -300]);
935
936 let raw = net.predict_raw();
937 assert_eq!(raw.len(), 3, "should have 3 readout outputs");
938
939 let scale = 1.0 / Q14_ONE as f64;
940 let all = net.predict_all_f64(scale);
941 assert_eq!(all.len(), 3);
942 }
943
944 #[test]
945 fn train_step_with_multi_output() {
946 let config = SpikeNetFixedConfig {
947 n_input: 2,
948 n_hidden: 8,
949 n_output: 2,
950 ..SpikeNetFixedConfig::default()
951 };
952 let mut net = SpikeNetFixed::new(config);
953
954 net.train_step(&[1000, -500], &[2000, -1000]);
956 assert_eq!(net.n_samples_seen(), 1);
957 }
958
959 #[test]
960 fn network_with_astrocyte_runs() {
961 let config = SpikeNetFixedConfig {
962 use_astrocyte: true,
963 astrocyte_tau: 100.0,
964 ..default_small_config()
965 };
966 let mut net = SpikeNetFixed::new(config);
967 for _ in 0..50 {
968 net.train_step(&[1000, -500], &[2000]);
969 }
970 assert_eq!(net.n_samples_seen(), 50);
971 let raw = net.predict_raw();
972 assert_eq!(raw.len(), 1);
973 }
974
975 #[test]
992 fn agmp_modulates_learning_rate_not_weights() {
993 use crate::snn::lif::f64_to_q14;
994
995 let config = SpikeNetFixedConfig {
996 use_astrocyte: true,
997 astrocyte_tau: 10.0, astrocyte_mode: AstrocyteMode::LearningRateGate,
999 n_input: 2,
1000 n_hidden: 16,
1001 n_output: 1,
1002 ..SpikeNetFixedConfig::default()
1003 };
1004
1005 let mut net = SpikeNetFixed::new(config);
1006
1007 let input = [f64_to_q14(0.5), f64_to_q14(-0.3)];
1008 let target = [f64_to_q14(1.0)];
1009
1010 for _ in 0..200 {
1011 net.train_step(&input, &target);
1012 }
1013
1014 let scale = 1.0 / Q14_ONE as f64;
1015 let pred = net.predict_f64(scale);
1016
1017 assert!(
1021 pred.is_finite(),
1022 "LearningRateGate network should produce finite prediction after training, got {pred}"
1023 );
1024
1025 assert_eq!(net.n_samples_seen(), 200);
1027 }
1028
1029 #[test]
1030 fn hidden_spikes_accessible() {
1031 let config = default_small_config();
1032 let mut net = SpikeNetFixed::new(config);
1033
1034 net.forward(&[0, 0]);
1035 net.forward(&[Q14_ONE, -Q14_ONE]); let spikes = net.hidden_spikes();
1038 assert_eq!(spikes.len(), 8);
1039 for &s in spikes {
1041 assert!(s == 0 || s == 1, "spike should be 0 or 1, got {}", s);
1042 }
1043 }
1044
1045 #[test]
1046 fn config_default_is_sensible() {
1047 let config = SpikeNetFixedConfig::default();
1048 assert!(config.alpha > 0, "alpha should be positive");
1049 assert!(config.v_thr > 0, "v_thr should be positive");
1050 assert!(config.eta > 0, "eta should be positive");
1051 assert!(config.n_hidden > 0, "n_hidden should be positive");
1052 }
1053
1054 #[test]
1059 fn precision_default_is_float_in_std() {
1060 let p = Precision::default();
1061 assert_eq!(
1063 p,
1064 Precision::Float,
1065 "Precision::default() must be Float on std targets, got {p:?}"
1066 );
1067
1068 assert_ne!(
1070 Precision::Float,
1071 Precision::Fixed,
1072 "Float and Fixed must be distinct"
1073 );
1074 }
1075}