1use rand::Rng;
12use serde::{Deserialize, Serialize};
13
14use crate::activation::Activation;
15use crate::error::PcError;
16use crate::layer::{Layer, LayerDef};
17use crate::linalg::cpu::CpuLinAlg;
18use crate::linalg::LinAlg;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct PcActorConfig {
48 pub input_size: usize,
50 pub hidden_layers: Vec<LayerDef>,
52 pub output_size: usize,
54 pub output_activation: Activation,
56 pub alpha: f64,
60 pub tol: f64,
64 pub min_steps: usize,
67 pub max_steps: usize,
70 pub lr_weights: f64,
72 pub synchronous: bool,
74 pub temperature: f64,
76 #[serde(default = "default_local_lambda")]
90 pub local_lambda: f64,
91 #[serde(default)]
97 pub residual: bool,
98 #[serde(default = "default_rezero_init")]
107 pub rezero_init: f64,
108}
109
110fn default_rezero_init() -> f64 {
112 0.001
113}
114
115fn default_local_lambda() -> f64 {
117 1.0
118}
119
120#[derive(Debug, Clone)]
127pub struct InferResult<L: LinAlg = CpuLinAlg> {
128 pub y_conv: L::Vector,
130 pub latent_concat: L::Vector,
132 pub hidden_states: Vec<L::Vector>,
134 pub prediction_errors: Vec<L::Vector>,
137 pub surprise_score: f64,
139 pub steps_used: usize,
141 pub converged: bool,
143 pub tanh_components: Vec<Option<L::Vector>>,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum SelectionMode {
152 Training,
154 Play,
156}
157
158#[derive(Debug)]
191pub struct PcActor<L: LinAlg = CpuLinAlg> {
192 pub(crate) layers: Vec<Layer<L>>,
194 pub config: PcActorConfig,
196 pub(crate) rezero_alpha: Vec<f64>,
198 pub(crate) skip_projections: Vec<Option<L::Matrix>>,
201}
202
203impl<L: LinAlg> PcActor<L> {
204 pub fn new(config: PcActorConfig, rng: &mut impl Rng) -> Result<Self, PcError> {
216 if config.input_size == 0 {
217 return Err(PcError::ConfigValidation("input_size must be > 0".into()));
218 }
219 if config.output_size == 0 {
220 return Err(PcError::ConfigValidation("output_size must be > 0".into()));
221 }
222 if config.temperature <= 0.0 {
223 return Err(PcError::ConfigValidation(format!(
224 "temperature must be positive, got {}",
225 config.temperature
226 )));
227 }
228 if !(0.0..=1.0).contains(&config.local_lambda) {
229 return Err(PcError::ConfigValidation(format!(
230 "local_lambda must be in [0.0, 1.0], got {}",
231 config.local_lambda
232 )));
233 }
234 if config.rezero_init < 0.0 {
235 return Err(PcError::ConfigValidation(format!(
236 "rezero_init must be >= 0, got {}",
237 config.rezero_init
238 )));
239 }
240 let mut layers: Vec<Layer<L>> = Vec::new();
241 let mut prev_size = config.input_size;
242
243 for def in &config.hidden_layers {
244 layers.push(Layer::<L>::new(prev_size, def.size, def.activation, rng));
245 prev_size = def.size;
246 }
247
248 layers.push(Layer::<L>::new(
250 prev_size,
251 config.output_size,
252 config.output_activation,
253 rng,
254 ));
255
256 let (rezero_alpha, skip_projections) = if config.residual {
258 let mut alphas = Vec::new();
259 let mut projs = Vec::new();
260 for i in 1..config.hidden_layers.len() {
261 alphas.push(config.rezero_init);
262 if config.hidden_layers[i].size != config.hidden_layers[i - 1].size {
263 projs.push(Some(L::xavier_mat(
264 config.hidden_layers[i].size,
265 config.hidden_layers[i - 1].size,
266 rng,
267 )));
268 } else {
269 projs.push(None);
270 }
271 }
272 (alphas, projs)
273 } else {
274 (Vec::new(), Vec::new())
275 };
276
277 Ok(Self {
278 layers,
279 config,
280 rezero_alpha,
281 skip_projections,
282 })
283 }
284
285 pub fn crossover(
304 parent_a: &PcActor<L>,
305 parent_b: &PcActor<L>,
306 caches_a: &[L::Matrix],
307 caches_b: &[L::Matrix],
308 alpha: f64,
309 child_config: PcActorConfig,
310 rng: &mut impl Rng,
311 ) -> Result<Self, PcError> {
312 let num_child_hidden = child_config.hidden_layers.len();
313 if num_child_hidden == 0 {
314 return Err(PcError::ConfigValidation(
315 "crossover requires at least one hidden layer".into(),
316 ));
317 }
318 let num_parent_a_hidden = parent_a.config.hidden_layers.len();
319 let num_parent_b_hidden = parent_b.config.hidden_layers.len();
320
321 let mut layers: Vec<Layer<L>> = Vec::new();
322 let mut prev_perm: Option<Vec<usize>> = None;
324
325 let child_h0 = &child_config.hidden_layers[0];
327
328 if parent_a.config.input_size == child_config.input_size
329 && parent_b.config.input_size == child_config.input_size
330 {
331 let cache_a_0 = caches_a.first();
332 let cache_b_0 = caches_b.first();
333 let (layer, perm) = cca_align_and_blend_layer::<L>(
334 &parent_a.layers[0],
335 &parent_b.layers[0],
336 cache_a_0,
337 cache_b_0,
338 None, child_h0.size,
340 L::mat_cols(&parent_a.layers[0].weights),
341 child_h0.activation,
342 alpha,
343 rng,
344 )?;
345 layers.push(layer);
346 prev_perm = perm;
347 } else {
348 layers.push(Layer::<L>::new(
349 child_config.input_size,
350 child_h0.size,
351 child_h0.activation,
352 rng,
353 ));
354 }
355
356 for h_idx in 1..num_child_hidden {
358 let child_def = &child_config.hidden_layers[h_idx];
359 let prev_child_size = child_config.hidden_layers[h_idx - 1].size;
360
361 let a_has = h_idx < num_parent_a_hidden;
362 let b_has = h_idx < num_parent_b_hidden;
363
364 if a_has && b_has {
365 let cache_a_h = caches_a.get(h_idx);
366 let cache_b_h = caches_b.get(h_idx);
367 let (layer, perm) = cca_align_and_blend_layer::<L>(
368 &parent_a.layers[h_idx],
369 &parent_b.layers[h_idx],
370 cache_a_h,
371 cache_b_h,
372 prev_perm.as_deref(),
373 child_def.size,
374 prev_child_size,
375 child_def.activation,
376 alpha,
377 rng,
378 )?;
379 layers.push(layer);
380 prev_perm = perm;
381 } else {
382 layers.push(Layer::<L>::new(
383 prev_child_size,
384 child_def.size,
385 child_def.activation,
386 rng,
387 ));
388 prev_perm = None;
389 }
390 }
391
392 let last_child_hidden = child_config.hidden_layers.last().map(|d| d.size).unwrap();
394 let a_out = parent_a.layers.last().unwrap();
395 let b_out = parent_b.layers.last().unwrap();
396 let a_out_in = L::mat_cols(&a_out.weights);
397 let b_out_in = L::mat_cols(&b_out.weights);
398
399 if a_out_in == last_child_hidden && b_out_in == last_child_hidden {
400 let b_out_permuted = if let Some(ref pp) = prev_perm {
402 permute_cols::<L>(&b_out.weights, pp)
403 } else {
404 b_out.weights.clone()
405 };
406 let out_rows = child_config.output_size;
407 let mut weights = L::zeros_mat(out_rows, last_child_hidden);
408 let mut biases = L::zeros_vec(out_rows);
409 let blend_rows = out_rows
410 .min(L::mat_rows(&a_out.weights))
411 .min(L::mat_rows(&b_out_permuted));
412 for r in 0..blend_rows {
413 for c in 0..last_child_hidden {
414 let va = L::mat_get(&a_out.weights, r, c);
415 let vb = L::mat_get(&b_out_permuted, r, c);
416 L::mat_set(&mut weights, r, c, alpha * va + (1.0 - alpha) * vb);
417 }
418 let ba = L::vec_get(&a_out.bias, r);
419 let bb = L::vec_get(&b_out.bias, r);
420 L::vec_set(&mut biases, r, alpha * ba + (1.0 - alpha) * bb);
421 }
422 layers.push(Layer {
423 weights,
424 bias: biases,
425 activation: child_config.output_activation,
426 });
427 } else {
428 layers.push(Layer::<L>::new(
429 last_child_hidden,
430 child_config.output_size,
431 child_config.output_activation,
432 rng,
433 ));
434 }
435
436 let (rezero_alpha, skip_projections) = if child_config.residual {
438 let mut alphas = Vec::new();
439 let mut projs = Vec::new();
440 for i in 1..num_child_hidden {
441 let a_has_rz = i - 1 < parent_a.rezero_alpha.len();
443 let b_has_rz = i - 1 < parent_b.rezero_alpha.len();
444 let rz = if a_has_rz && b_has_rz {
445 alpha * parent_a.rezero_alpha[i - 1]
446 + (1.0 - alpha) * parent_b.rezero_alpha[i - 1]
447 } else if a_has_rz {
448 parent_a.rezero_alpha[i - 1]
449 } else if b_has_rz {
450 parent_b.rezero_alpha[i - 1]
451 } else {
452 child_config.rezero_init
453 };
454 alphas.push(rz);
455
456 let cur_size = child_config.hidden_layers[i].size;
458 let prev_size = child_config.hidden_layers[i - 1].size;
459 if cur_size != prev_size {
460 let a_proj = parent_a
461 .skip_projections
462 .get(i - 1)
463 .and_then(|p| p.as_ref());
464 let b_proj = parent_b
465 .skip_projections
466 .get(i - 1)
467 .and_then(|p| p.as_ref());
468 if let (Some(ap), Some(bp)) = (a_proj, b_proj) {
469 if L::mat_rows(ap) == cur_size
470 && L::mat_cols(ap) == prev_size
471 && L::mat_rows(bp) == cur_size
472 && L::mat_cols(bp) == prev_size
473 {
474 let mut proj = L::zeros_mat(cur_size, prev_size);
476 for r in 0..cur_size {
477 for c in 0..prev_size {
478 let va = L::mat_get(ap, r, c);
479 let vb = L::mat_get(bp, r, c);
480 L::mat_set(&mut proj, r, c, alpha * va + (1.0 - alpha) * vb);
481 }
482 }
483 projs.push(Some(proj));
484 } else {
485 projs.push(Some(L::xavier_mat(cur_size, prev_size, rng)));
486 }
487 } else {
488 projs.push(Some(L::xavier_mat(cur_size, prev_size, rng)));
489 }
490 } else {
491 projs.push(None);
492 }
493 }
494 (alphas, projs)
495 } else {
496 (Vec::new(), Vec::new())
497 };
498
499 Ok(Self {
500 layers,
501 config: child_config,
502 rezero_alpha,
503 skip_projections,
504 })
505 }
506
507 pub fn latent_size(&self) -> usize {
509 self.config.hidden_layers.iter().map(|def| def.size).sum()
510 }
511
512 fn is_skip_layer(&self, i: usize) -> bool {
525 self.config.residual && i >= 1
526 }
527
528 fn skip_alpha_index(&self, i: usize) -> Option<usize> {
530 if !self.is_skip_layer(i) {
531 return None;
532 }
533 Some(i - 1)
534 }
535
536 pub fn infer(&self, input: &[f64]) -> InferResult<L> {
537 assert_eq!(
538 input.len(),
539 self.config.input_size,
540 "input size mismatch: got {}, expected {}",
541 input.len(),
542 self.config.input_size
543 );
544
545 let input_vec = L::vec_from_slice(input);
546 let n_hidden = self.config.hidden_layers.len();
547
548 let mut hidden_states: Vec<L::Vector> = Vec::with_capacity(n_hidden);
550 let mut tanh_components: Vec<Option<L::Vector>> = Vec::with_capacity(n_hidden);
551 let mut prev = input_vec.clone();
552 for (i, layer) in self.layers[..n_hidden].iter().enumerate() {
553 let tanh_out = layer.forward(&prev);
554 if let Some(alpha_idx) = self.skip_alpha_index(i) {
555 let alpha = self.rezero_alpha[alpha_idx];
556 let scaled = L::vec_scale(&tanh_out, alpha);
557 let skip_path = if let Some(ref proj) = self.skip_projections[alpha_idx] {
558 L::mat_vec_mul(proj, &prev)
559 } else {
560 prev.clone()
561 };
562 prev = L::vec_add(&skip_path, &scaled);
563 tanh_components.push(Some(tanh_out));
564 } else {
565 prev = tanh_out;
566 tanh_components.push(None);
567 }
568 hidden_states.push(prev.clone());
569 }
570 let last_input = if n_hidden > 0 {
572 &hidden_states[n_hidden - 1]
573 } else {
574 &input_vec
575 };
576 let mut y = self.layers[n_hidden].forward(last_input);
577
578 let mut steps_used = 0;
580 let mut converged = false;
581 let mut surprise_score = 0.0;
582 let mut last_errors: Vec<L::Vector> = Vec::new();
583
584 for step in 0..self.config.max_steps {
585 steps_used = step + 1;
586
587 let snap_h: Vec<L::Vector>;
592 let snap_tc: Vec<Option<L::Vector>>;
593 let use_snapshot = self.config.synchronous;
594 if use_snapshot {
595 snap_h = hidden_states.clone();
596 snap_tc = tanh_components.clone();
597 } else {
598 snap_h = Vec::new();
599 snap_tc = Vec::new();
600 }
601
602 let mut error_vecs: Vec<L::Vector> = Vec::new();
603
604 for i in (0..n_hidden).rev() {
605 let state_above = if i == n_hidden - 1 {
607 &y
608 } else if use_snapshot {
609 snap_tc[i + 1].as_ref().unwrap_or(&snap_h[i + 1])
610 } else {
611 tanh_components[i + 1]
612 .as_ref()
613 .unwrap_or(&hidden_states[i + 1])
614 };
615
616 let target = if use_snapshot {
618 snap_tc[i].as_ref().unwrap_or(&snap_h[i]).clone()
619 } else {
620 tanh_components[i]
621 .as_ref()
622 .unwrap_or(&hidden_states[i])
623 .clone()
624 };
625
626 let prediction = self.layers[i + 1]
627 .transpose_forward(state_above, self.config.hidden_layers[i].activation);
628
629 let error = L::vec_sub(&prediction, &target);
630 error_vecs.push(error.clone());
631
632 let updated_target = L::vec_add(&target, &L::vec_scale(&error, self.config.alpha));
633 if let Some(alpha_idx) = self.skip_alpha_index(i) {
634 tanh_components[i] = Some(updated_target.clone());
635 let alpha = self.rezero_alpha[alpha_idx];
636 let prev_h = if i > 0 {
637 &hidden_states[i - 1]
638 } else {
639 &input_vec
640 };
641 let skip_path = if let Some(ref proj) = self.skip_projections[alpha_idx] {
642 L::mat_vec_mul(proj, prev_h)
643 } else {
644 prev_h.clone()
645 };
646 hidden_states[i] =
647 L::vec_add(&skip_path, &L::vec_scale(&updated_target, alpha));
648 } else {
649 hidden_states[i] = updated_target;
650 }
651 }
652
653 let top_hidden = if n_hidden > 0 {
654 &hidden_states[n_hidden - 1]
655 } else {
656 &input_vec
657 };
658 y = self.layers[n_hidden].forward(top_hidden);
659
660 let refs: Vec<&L::Vector> = error_vecs.iter().collect();
661 surprise_score = L::rms_error(&refs);
662 last_errors = error_vecs;
663
664 if self.config.alpha > 0.0
666 && step + 1 >= self.config.min_steps
667 && surprise_score < self.config.tol
668 {
669 converged = true;
670 break;
671 }
672 }
673
674 let mut latent_raw: Vec<f64> = Vec::new();
676 for h in &hidden_states {
677 latent_raw.extend_from_slice(&L::vec_to_vec(h));
678 }
679 let latent_concat = L::vec_from_slice(&latent_raw);
680
681 InferResult {
682 y_conv: y,
683 latent_concat,
684 hidden_states,
685 prediction_errors: last_errors,
686 surprise_score,
687 steps_used,
688 converged,
689 tanh_components,
690 }
691 }
692
693 pub fn select_action(
706 &self,
707 y_conv: &L::Vector,
708 valid_actions: &[usize],
709 mode: SelectionMode,
710 rng: &mut impl Rng,
711 ) -> usize {
712 assert!(!valid_actions.is_empty(), "valid_actions must not be empty");
713
714 let scaled = L::vec_scale(y_conv, 1.0 / self.config.temperature);
716
717 let probs = L::softmax_masked(&scaled, valid_actions);
718
719 match mode {
720 SelectionMode::Play => L::argmax_masked(&probs, valid_actions),
721 SelectionMode::Training => L::sample_from_probs(&probs, valid_actions, rng),
722 }
723 }
724
725 pub fn update_weights(
741 &mut self,
742 output_delta: &[f64],
743 infer_result: &InferResult<L>,
744 input: &[f64],
745 surprise_scale: f64,
746 ) {
747 assert_eq!(
748 input.len(),
749 self.config.input_size,
750 "input size mismatch: got {}, expected {}",
751 input.len(),
752 self.config.input_size
753 );
754
755 self.update_weights_hybrid(
756 output_delta,
757 infer_result,
758 input,
759 surprise_scale,
760 self.config.local_lambda,
761 );
762 }
763
764 fn update_weights_hybrid(
775 &mut self,
776 output_delta: &[f64],
777 infer_result: &InferResult<L>,
778 input: &[f64],
779 surprise_scale: f64,
780 lambda: f64,
781 ) {
782 let input_vec = L::vec_from_slice(input);
783 let output_delta_vec = L::vec_from_slice(output_delta);
784 let n_hidden = self.config.hidden_layers.len();
785 let n_layers = self.layers.len();
786
787 let output_input = if n_hidden > 0 {
789 &infer_result.hidden_states[n_hidden - 1]
790 } else {
791 &input_vec
792 };
793 let output_output = &infer_result.y_conv;
794 let mut bp_delta = self.layers[n_layers - 1].backward(
795 output_input,
796 output_output,
797 &output_delta_vec,
798 self.config.lr_weights,
799 surprise_scale,
800 );
801
802 for i in (0..n_hidden).rev() {
804 let layer_input = if i > 0 {
805 &infer_result.hidden_states[i - 1]
806 } else {
807 &input_vec
808 };
809
810 let effective_delta = if (lambda - 1.0).abs() < f64::EPSILON {
812 bp_delta.clone()
813 } else if lambda.abs() < f64::EPSILON {
814 let error_idx = n_hidden - 1 - i;
815 infer_result.prediction_errors[error_idx].clone()
816 } else {
817 let error_idx = n_hidden - 1 - i;
818 let pc_error = &infer_result.prediction_errors[error_idx];
819 let bp_scaled = L::vec_scale(&bp_delta, lambda);
820 let pc_scaled = L::vec_scale(pc_error, 1.0 - lambda);
821 L::vec_add(&bp_scaled, &pc_scaled)
822 };
823
824 if let Some(alpha_idx) = self.skip_alpha_index(i) {
825 let tanh_out = infer_result.tanh_components[i].as_ref().unwrap();
828 let alpha = self.rezero_alpha[alpha_idx];
829 let effective_lr = self.config.lr_weights * surprise_scale;
830
831 let scaled_delta = L::vec_scale(&effective_delta, alpha);
833
834 let propagated = self.layers[i].backward(
836 layer_input,
837 tanh_out,
838 &scaled_delta,
839 self.config.lr_weights,
840 surprise_scale,
841 );
842
843 let grad_alpha: f64 = L::vec_dot(&effective_delta, tanh_out);
845 self.rezero_alpha[alpha_idx] -= effective_lr * grad_alpha;
846
847 if let Some(ref mut proj) = self.skip_projections[alpha_idx] {
849 let proj_t = L::mat_transpose(proj);
851 let skip_delta = L::mat_vec_mul(&proj_t, &effective_delta);
852 let dw_proj = L::outer_product(&effective_delta, layer_input);
854 L::mat_scale_add(proj, &dw_proj, -effective_lr);
855 bp_delta = L::vec_add(&propagated, &skip_delta);
856 } else {
857 bp_delta = L::vec_add(&propagated, &effective_delta);
859 }
860 } else {
861 let layer_output = &infer_result.hidden_states[i];
863 bp_delta = self.layers[i].backward(
864 layer_input,
865 layer_output,
866 &effective_delta,
867 self.config.lr_weights,
868 surprise_scale,
869 );
870 }
871 }
872 }
873
874 pub fn to_weights(&self) -> crate::serializer::PcActorWeights {
878 let cpu_layers: Vec<Layer<CpuLinAlg>> = self
879 .layers
880 .iter()
881 .map(|layer| {
882 let rows = L::mat_rows(&layer.weights);
883 let cols = L::mat_cols(&layer.weights);
884 let mut cpu_weights = crate::matrix::Matrix::zeros(rows, cols);
885 for r in 0..rows {
886 for c in 0..cols {
887 cpu_weights.set(r, c, L::mat_get(&layer.weights, r, c));
888 }
889 }
890 let bias_data = L::vec_to_vec(&layer.bias);
891 Layer {
892 weights: cpu_weights,
893 bias: bias_data,
894 activation: layer.activation,
895 }
896 })
897 .collect();
898 let cpu_projs: Vec<Option<crate::matrix::Matrix>> = self
899 .skip_projections
900 .iter()
901 .map(|opt| {
902 opt.as_ref().map(|m| {
903 let rows = L::mat_rows(m);
904 let cols = L::mat_cols(m);
905 let mut cpu_m = crate::matrix::Matrix::zeros(rows, cols);
906 for r in 0..rows {
907 for c in 0..cols {
908 cpu_m.set(r, c, L::mat_get(m, r, c));
909 }
910 }
911 cpu_m
912 })
913 })
914 .collect();
915 crate::serializer::PcActorWeights {
916 layers: cpu_layers,
917 rezero_alpha: self.rezero_alpha.clone(),
918 skip_projections: cpu_projs,
919 }
920 }
921
922 pub fn from_weights(
933 config: PcActorConfig,
934 weights: crate::serializer::PcActorWeights,
935 ) -> Result<Self, PcError> {
936 let n_hidden = config.hidden_layers.len();
937 let expected_layers = n_hidden + 1;
938
939 if weights.layers.len() != expected_layers {
940 return Err(PcError::DimensionMismatch {
941 expected: expected_layers,
942 got: weights.layers.len(),
943 context: "actor layer count",
944 });
945 }
946
947 let mut prev_size = config.input_size;
949 for (i, cpu_layer) in weights.layers.iter().enumerate() {
950 let (expected_rows, expected_cols) = if i < n_hidden {
951 (config.hidden_layers[i].size, prev_size)
952 } else {
953 (config.output_size, prev_size)
954 };
955
956 if cpu_layer.weights.rows != expected_rows {
957 return Err(PcError::DimensionMismatch {
958 expected: expected_rows,
959 got: cpu_layer.weights.rows,
960 context: "actor layer weight rows",
961 });
962 }
963 if cpu_layer.weights.cols != expected_cols {
964 return Err(PcError::DimensionMismatch {
965 expected: expected_cols,
966 got: cpu_layer.weights.cols,
967 context: "actor layer weight cols",
968 });
969 }
970 if cpu_layer.bias.len() != expected_rows {
971 return Err(PcError::DimensionMismatch {
972 expected: expected_rows,
973 got: cpu_layer.bias.len(),
974 context: "actor layer bias length",
975 });
976 }
977
978 if i < n_hidden {
979 prev_size = config.hidden_layers[i].size;
980 }
981 }
982
983 if config.residual {
985 let expected_residual = n_hidden.saturating_sub(1);
986 if weights.rezero_alpha.len() != expected_residual {
987 return Err(PcError::DimensionMismatch {
988 expected: expected_residual,
989 got: weights.rezero_alpha.len(),
990 context: "actor rezero_alpha count",
991 });
992 }
993 if weights.skip_projections.len() != expected_residual {
994 return Err(PcError::DimensionMismatch {
995 expected: expected_residual,
996 got: weights.skip_projections.len(),
997 context: "actor skip_projections count",
998 });
999 }
1000 }
1001
1002 let layers: Vec<Layer<L>> = weights
1004 .layers
1005 .into_iter()
1006 .map(|cpu_layer| {
1007 let rows = cpu_layer.weights.rows;
1008 let cols = cpu_layer.weights.cols;
1009 let mut mat = L::zeros_mat(rows, cols);
1010 for r in 0..rows {
1011 for c in 0..cols {
1012 L::mat_set(&mut mat, r, c, cpu_layer.weights.get(r, c));
1013 }
1014 }
1015 let bias = L::vec_from_slice(&cpu_layer.bias);
1016 Layer {
1017 weights: mat,
1018 bias,
1019 activation: cpu_layer.activation,
1020 }
1021 })
1022 .collect();
1023 let skip_projections: Vec<Option<L::Matrix>> = weights
1024 .skip_projections
1025 .into_iter()
1026 .map(|opt| {
1027 opt.map(|cpu_m| {
1028 let rows = cpu_m.rows;
1029 let cols = cpu_m.cols;
1030 let mut mat = L::zeros_mat(rows, cols);
1031 for r in 0..rows {
1032 for c in 0..cols {
1033 L::mat_set(&mut mat, r, c, cpu_m.get(r, c));
1034 }
1035 }
1036 mat
1037 })
1038 })
1039 .collect();
1040 Ok(Self {
1041 layers,
1042 config,
1043 rezero_alpha: weights.rezero_alpha,
1044 skip_projections,
1045 })
1046 }
1047}
1048
1049pub(crate) fn permute_cols<L: LinAlg>(m: &L::Matrix, perm: &[usize]) -> L::Matrix {
1052 let rows = L::mat_rows(m);
1053 let cols = L::mat_cols(m);
1054 let perm_len = perm.len();
1055 let mut result = L::zeros_mat(rows, cols);
1056 for (dst, &src) in perm.iter().enumerate().take(cols.min(perm_len)) {
1057 if src < cols {
1058 for r in 0..rows {
1059 L::mat_set(&mut result, r, dst, L::mat_get(m, r, src));
1060 }
1061 }
1062 }
1063 for dst in perm_len..cols {
1065 for r in 0..rows {
1066 L::mat_set(&mut result, r, dst, L::mat_get(m, r, dst));
1067 }
1068 }
1069 result
1070}
1071
1072pub(crate) fn permute_rows<L: LinAlg>(m: &L::Matrix, perm: &[usize], n: usize) -> L::Matrix {
1075 let cols = L::mat_cols(m);
1076 let perm_len = perm.len();
1077 let mut result = L::zeros_mat(n, cols);
1078 for (dst, &src) in perm.iter().enumerate().take(n.min(perm_len)) {
1079 if src < L::mat_rows(m) {
1080 for c in 0..cols {
1081 L::mat_set(&mut result, dst, c, L::mat_get(m, src, c));
1082 }
1083 }
1084 }
1085 for dst in perm_len..n {
1087 if dst < L::mat_rows(m) {
1088 for c in 0..cols {
1089 L::mat_set(&mut result, dst, c, L::mat_get(m, dst, c));
1090 }
1091 }
1092 }
1093 result
1094}
1095
1096pub(crate) fn permute_vec<L: LinAlg>(v: &L::Vector, perm: &[usize], n: usize) -> L::Vector {
1098 let perm_len = perm.len();
1099 let mut result = L::zeros_vec(n);
1100 for (dst, &src) in perm.iter().enumerate().take(n.min(perm_len)) {
1101 if src < L::vec_len(v) {
1102 L::vec_set(&mut result, dst, L::vec_get(v, src));
1103 }
1104 }
1105 for dst in perm_len..n {
1106 if dst < L::vec_len(v) {
1107 L::vec_set(&mut result, dst, L::vec_get(v, dst));
1108 }
1109 }
1110 result
1111}
1112
1113#[allow(clippy::too_many_arguments)]
1120pub(crate) fn blend_layer_weights<L: LinAlg>(
1121 parent_a: (&L::Matrix, &L::Vector, usize),
1122 parent_b: (&L::Matrix, &L::Vector, usize),
1123 n_child: usize,
1124 child_cols: usize,
1125 alpha: f64,
1126 rng: &mut impl Rng,
1127) -> (L::Matrix, L::Vector) {
1128 let (a_weights, a_biases, n_a) = parent_a;
1129 let (b_weights, b_biases, n_b) = parent_b;
1130 let n_min = n_a.min(n_b);
1131 let n_max = n_a.max(n_b);
1132 let a_cols = L::mat_cols(a_weights);
1133 let b_cols = L::mat_cols(b_weights);
1134 let use_cols = child_cols.min(a_cols).min(b_cols);
1135
1136 let mut weights = L::zeros_mat(n_child, child_cols);
1137 let mut biases = L::zeros_vec(n_child);
1138
1139 let blend_end = n_min.min(n_child);
1141 for r in 0..blend_end {
1142 for c in 0..use_cols {
1143 let va = L::mat_get(a_weights, r, c);
1144 let vb = L::mat_get(b_weights, r, c);
1145 L::mat_set(&mut weights, r, c, alpha * va + (1.0 - alpha) * vb);
1146 }
1147 let ba = L::vec_get(a_biases, r);
1148 let bb = L::vec_get(b_biases, r);
1149 L::vec_set(&mut biases, r, alpha * ba + (1.0 - alpha) * bb);
1150 }
1151
1152 let copy_end = n_max.min(n_child);
1154 if copy_end > blend_end {
1155 let (larger_w, larger_b) = if n_a >= n_b {
1156 (a_weights, a_biases)
1157 } else {
1158 (b_weights, b_biases)
1159 };
1160 let larger_cols = L::mat_cols(larger_w);
1161 for r in blend_end..copy_end {
1162 for c in 0..child_cols.min(larger_cols) {
1163 L::mat_set(&mut weights, r, c, L::mat_get(larger_w, r, c));
1164 }
1165 L::vec_set(&mut biases, r, L::vec_get(larger_b, r));
1166 }
1167 }
1168
1169 if n_child > n_max {
1171 let xavier = L::xavier_mat(n_child - n_max, child_cols, rng);
1172 for r in n_max..n_child {
1173 for c in 0..child_cols {
1174 L::mat_set(&mut weights, r, c, L::mat_get(&xavier, r - n_max, c));
1175 }
1176 }
1178 }
1179
1180 (weights, biases)
1181}
1182
1183#[allow(clippy::too_many_arguments)]
1192pub(crate) fn cca_align_and_blend_layer<L: LinAlg>(
1193 a_layer: &Layer<L>,
1194 b_layer: &Layer<L>,
1195 cache_a: Option<&L::Matrix>,
1196 cache_b: Option<&L::Matrix>,
1197 prev_perm: Option<&[usize]>,
1198 child_rows: usize,
1199 child_cols: usize,
1200 child_activation: Activation,
1201 alpha: f64,
1202 rng: &mut impl Rng,
1203) -> Result<(Layer<L>, Option<Vec<usize>>), crate::error::PcError> {
1204 let n_a = L::mat_rows(&a_layer.weights);
1205 let n_b = L::mat_rows(&b_layer.weights);
1206
1207 let perm = if let (Some(ca), Some(cb)) = (cache_a, cache_b) {
1209 Some(crate::matrix::cca_neuron_alignment::<L>(ca, cb)?)
1210 } else {
1211 None
1212 };
1213
1214 let b_weights_col = if let Some(pp) = prev_perm {
1216 permute_cols::<L>(&b_layer.weights, pp)
1217 } else {
1218 b_layer.weights.clone()
1219 };
1220
1221 let b_weights_aligned = if let Some(ref p) = perm {
1223 permute_rows::<L>(&b_weights_col, p, n_b)
1224 } else {
1225 b_weights_col
1226 };
1227 let b_bias_aligned = if let Some(ref p) = perm {
1228 permute_vec::<L>(&b_layer.bias, p, n_b)
1229 } else {
1230 b_layer.bias.clone()
1231 };
1232
1233 let (weights, biases) = blend_layer_weights::<L>(
1234 (&a_layer.weights, &a_layer.bias, n_a),
1235 (&b_weights_aligned, &b_bias_aligned, n_b),
1236 child_rows,
1237 child_cols,
1238 alpha,
1239 rng,
1240 );
1241
1242 Ok((
1243 Layer {
1244 weights,
1245 bias: biases,
1246 activation: child_activation,
1247 },
1248 perm,
1249 ))
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use super::*;
1255 use crate::activation::Activation;
1256 use crate::layer::LayerDef;
1257 use crate::matrix::WEIGHT_CLIP;
1258 use rand::rngs::StdRng;
1259 use rand::SeedableRng;
1260
1261 fn make_rng() -> StdRng {
1262 StdRng::seed_from_u64(42)
1263 }
1264
1265 fn default_config() -> PcActorConfig {
1266 PcActorConfig {
1267 input_size: 9,
1268 hidden_layers: vec![LayerDef {
1269 size: 18,
1270 activation: Activation::Tanh,
1271 }],
1272 output_size: 9,
1273 output_activation: Activation::Tanh,
1274 alpha: 0.1,
1275 tol: 0.01,
1276 min_steps: 1,
1277 max_steps: 20,
1278 lr_weights: 0.01,
1279 synchronous: true,
1280 temperature: 1.0,
1281 local_lambda: 1.0,
1282 residual: false,
1283 rezero_init: 0.001,
1284 }
1285 }
1286
1287 fn two_hidden_config() -> PcActorConfig {
1288 PcActorConfig {
1289 hidden_layers: vec![
1290 LayerDef {
1291 size: 18,
1292 activation: Activation::Tanh,
1293 },
1294 LayerDef {
1295 size: 12,
1296 activation: Activation::Tanh,
1297 },
1298 ],
1299 ..default_config()
1300 }
1301 }
1302
1303 #[test]
1306 fn test_infer_converges_on_zero_board() {
1307 let mut rng = make_rng();
1308 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1309 let result = actor.infer(&[0.0; 9]);
1310 for &v in &result.y_conv {
1312 assert!(v.is_finite());
1313 }
1314 }
1315
1316 #[test]
1317 fn test_infer_steps_used_at_least_min_steps() {
1318 let mut rng = make_rng();
1319 let config = PcActorConfig {
1320 min_steps: 3,
1321 ..default_config()
1322 };
1323 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
1324 let result = actor.infer(&[0.0; 9]);
1325 assert!(result.steps_used >= 3);
1326 }
1327
1328 #[test]
1329 fn test_infer_alpha_zero_does_not_converge() {
1330 let mut rng = make_rng();
1331 let config = PcActorConfig {
1332 alpha: 0.0,
1333 ..default_config()
1334 };
1335 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
1336 let result = actor.infer(&[0.0; 9]);
1337 assert!(!result.converged);
1338 assert_eq!(result.steps_used, 20);
1339 }
1340
1341 #[test]
1342 fn test_infer_does_not_modify_weights() {
1343 let mut rng = make_rng();
1344 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1345 let weights_before: Vec<Vec<f64>> = actor
1346 .layers
1347 .iter()
1348 .map(|l| l.weights.data.clone())
1349 .collect();
1350 let _ = actor.infer(&[0.0; 9]);
1351 for (i, layer) in actor.layers.iter().enumerate() {
1352 assert_eq!(layer.weights.data, weights_before[i]);
1353 }
1354 }
1355
1356 #[test]
1357 fn test_infer_latent_size_single_hidden() {
1358 let mut rng = make_rng();
1359 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1360 let result = actor.infer(&[0.0; 9]);
1361 assert_eq!(result.latent_concat.len(), 18);
1362 }
1363
1364 #[test]
1365 fn test_infer_latent_size_two_hidden() {
1366 let mut rng = make_rng();
1367 let actor: PcActor = PcActor::new(two_hidden_config(), &mut rng).unwrap();
1368 let result = actor.infer(&[0.0; 9]);
1369 assert_eq!(result.latent_concat.len(), 30);
1370 }
1371
1372 #[test]
1373 fn test_infer_latent_size_matches_latent_size_method() {
1374 let mut rng = make_rng();
1375 let actor: PcActor = PcActor::new(two_hidden_config(), &mut rng).unwrap();
1376 let result = actor.infer(&[0.0; 9]);
1377 assert_eq!(result.latent_concat.len(), actor.latent_size());
1378 }
1379
1380 #[test]
1381 fn test_infer_y_conv_length_equals_output_size() {
1382 let mut rng = make_rng();
1383 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1384 let result = actor.infer(&[0.0; 9]);
1385 assert_eq!(result.y_conv.len(), 9);
1386 }
1387
1388 #[test]
1389 fn test_infer_hidden_states_count_matches_hidden_layers() {
1390 let mut rng = make_rng();
1391 let actor: PcActor = PcActor::new(two_hidden_config(), &mut rng).unwrap();
1392 let result = actor.infer(&[0.0; 9]);
1393 assert_eq!(result.hidden_states.len(), 2);
1394 }
1395
1396 #[test]
1397 fn test_infer_all_outputs_finite() {
1398 let mut rng = make_rng();
1399 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1400 let result = actor.infer(&[1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5]);
1401 for &v in &result.y_conv {
1402 assert!(v.is_finite());
1403 }
1404 for &v in &result.latent_concat {
1405 assert!(v.is_finite());
1406 }
1407 assert!(result.surprise_score.is_finite());
1408 }
1409
1410 #[test]
1411 fn test_infer_surprise_score_nonnegative() {
1412 let mut rng = make_rng();
1413 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1414 let result = actor.infer(&[0.0; 9]);
1415 assert!(result.surprise_score >= 0.0);
1416 }
1417
1418 #[test]
1419 fn test_infer_synchronous_and_inplace_both_converge() {
1420 let mut rng = make_rng();
1421 let sync_actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1422 let mut rng2 = make_rng();
1423 let inplace_config = PcActorConfig {
1424 synchronous: false,
1425 ..default_config()
1426 };
1427 let inplace_actor: PcActor = PcActor::new(inplace_config, &mut rng2).unwrap();
1428 let sync_result = sync_actor.infer(&[0.0; 9]);
1429 let inplace_result = inplace_actor.infer(&[0.0; 9]);
1430 assert!(sync_result.steps_used > 0);
1432 assert!(inplace_result.steps_used > 0);
1433 }
1434
1435 #[test]
1436 fn test_infer_synchronous_produces_different_result_than_inplace() {
1437 let mut rng = make_rng();
1438 let config = PcActorConfig {
1439 hidden_layers: vec![
1440 LayerDef {
1441 size: 18,
1442 activation: Activation::Tanh,
1443 },
1444 LayerDef {
1445 size: 12,
1446 activation: Activation::Tanh,
1447 },
1448 ],
1449 alpha: 0.3,
1450 tol: 1e-15,
1451 min_steps: 1,
1452 max_steps: 3,
1453 ..default_config()
1454 };
1455 let sync_actor: PcActor = PcActor::new(config.clone(), &mut rng).unwrap();
1456 let mut rng2 = make_rng();
1457 let inplace_config = PcActorConfig {
1458 synchronous: false,
1459 ..config
1460 };
1461 let inplace_actor: PcActor = PcActor::new(inplace_config, &mut rng2).unwrap();
1462 let input = [1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5];
1463 let sync_result = sync_actor.infer(&input);
1464 let inplace_result = inplace_actor.infer(&input);
1465 let differs = sync_result
1467 .latent_concat
1468 .iter()
1469 .zip(inplace_result.latent_concat.iter())
1470 .any(|(a, b)| (a - b).abs() > 1e-12);
1471 assert!(
1472 differs,
1473 "Synchronous and in-place should produce different results"
1474 );
1475 }
1476
1477 #[test]
1478 #[should_panic(expected = "input size")]
1479 fn test_infer_panics_wrong_input_length() {
1480 let mut rng = make_rng();
1481 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1482 let _ = actor.infer(&[0.0; 5]);
1483 }
1484
1485 #[test]
1488 fn test_select_action_training_always_in_valid() {
1489 let mut rng = make_rng();
1490 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1491 let logits = vec![0.1, -0.2, 0.5, -0.1, 0.3, 0.0, -0.3, 0.2, 0.4];
1492 let valid = vec![0, 2, 4, 6, 8];
1493 for _ in 0..20 {
1494 let action = actor.select_action(&logits, &valid, SelectionMode::Training, &mut rng);
1495 assert!(valid.contains(&action));
1496 }
1497 }
1498
1499 #[test]
1500 fn test_select_action_play_mode_deterministic() {
1501 let mut rng1 = StdRng::seed_from_u64(1);
1502 let mut rng2 = StdRng::seed_from_u64(99);
1503 let mut rng_init = make_rng();
1504 let actor: PcActor = PcActor::new(default_config(), &mut rng_init).unwrap();
1505 let logits = vec![0.1, -0.2, 0.5, -0.1, 0.3, 0.0, -0.3, 0.2, 0.4];
1506 let valid = vec![0, 2, 4, 6, 8];
1507 let a1 = actor.select_action(&logits, &valid, SelectionMode::Play, &mut rng1);
1508 let a2 = actor.select_action(&logits, &valid, SelectionMode::Play, &mut rng2);
1509 assert_eq!(a1, a2, "Play mode should be deterministic");
1510 }
1511
1512 #[test]
1513 fn test_select_action_temperature_gt_one_more_uniform() {
1514 let mut rng = make_rng();
1515 let hot_config = PcActorConfig {
1516 temperature: 5.0,
1517 ..default_config()
1518 };
1519 let actor: PcActor = PcActor::new(hot_config, &mut rng).unwrap();
1520 let logits = vec![10.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1522 let valid: Vec<usize> = (0..9).collect();
1523 let mut seen = std::collections::HashSet::new();
1524 let mut rng2 = StdRng::seed_from_u64(123);
1525 for _ in 0..100 {
1526 let a = actor.select_action(&logits, &valid, SelectionMode::Training, &mut rng2);
1527 seen.insert(a);
1528 }
1529 assert!(seen.len() > 1, "High temperature should explore more");
1530 }
1531
1532 #[test]
1533 #[should_panic]
1534 fn test_select_action_empty_valid_panics() {
1535 let mut rng = make_rng();
1536 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1537 let logits = vec![0.1; 9];
1538 let _ = actor.select_action(&logits, &[], SelectionMode::Training, &mut rng);
1539 }
1540
1541 #[test]
1544 fn test_update_weights_changes_first_layer() {
1545 let mut rng = make_rng();
1546 let mut actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1547 let input = vec![1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5];
1548 let infer_result = actor.infer(&input);
1549 let weights_before = actor.layers[0].weights.data.clone();
1550 let delta = vec![0.1; 9];
1551 actor.update_weights(&delta, &infer_result, &input, 1.0);
1552 assert_ne!(actor.layers[0].weights.data, weights_before);
1553 }
1554
1555 #[test]
1556 fn test_update_weights_clips_all_layers() {
1557 let mut rng = make_rng();
1558 let mut actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1559 let input = vec![1.0; 9];
1560 let infer_result = actor.infer(&input);
1561 let delta = vec![1e6; 9];
1562 actor.update_weights(&delta, &infer_result, &input, 1.0);
1563 for layer in &actor.layers {
1564 for &w in &layer.weights.data {
1565 assert!(
1566 w.abs() <= WEIGHT_CLIP + 1e-12,
1567 "Weight {w} exceeds WEIGHT_CLIP"
1568 );
1569 }
1570 }
1571 }
1572
1573 #[test]
1574 fn test_update_weights_two_hidden_changes_both_layers() {
1575 let mut rng = make_rng();
1576 let mut actor: PcActor = PcActor::new(two_hidden_config(), &mut rng).unwrap();
1577 let input = vec![0.5; 9];
1578 let infer_result = actor.infer(&input);
1579 let w0_before = actor.layers[0].weights.data.clone();
1580 let w1_before = actor.layers[1].weights.data.clone();
1581 let delta = vec![0.1; 9];
1582 actor.update_weights(&delta, &infer_result, &input, 1.0);
1583 assert_ne!(
1584 actor.layers[0].weights.data, w0_before,
1585 "Layer 0 should change"
1586 );
1587 assert_ne!(
1588 actor.layers[1].weights.data, w1_before,
1589 "Layer 1 should change"
1590 );
1591 }
1592
1593 #[test]
1594 #[should_panic(expected = "input size")]
1595 fn test_update_weights_panics_wrong_x_size() {
1596 let mut rng = make_rng();
1597 let mut actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1598 let input = vec![0.0; 9];
1599 let infer_result = actor.infer(&input);
1600 let delta = vec![0.1; 9];
1601 actor.update_weights(&delta, &infer_result, &[0.0; 5], 1.0);
1602 }
1603
1604 #[test]
1607 fn test_infer_zero_hidden_layers_produces_finite_output() {
1608 let mut rng = make_rng();
1609 let config = PcActorConfig {
1610 hidden_layers: vec![],
1611 ..default_config()
1612 };
1613 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
1614 let result = actor.infer(&[0.5; 9]);
1615 assert_eq!(result.y_conv.len(), 9);
1616 assert!(result.y_conv.iter().all(|v| v.is_finite()));
1617 assert!(result.latent_concat.is_empty());
1618 assert!(result.hidden_states.is_empty());
1619 }
1620
1621 #[test]
1624 fn test_new_zero_input_size_returns_error() {
1625 let mut rng = make_rng();
1626 let config = PcActorConfig {
1627 input_size: 0,
1628 ..default_config()
1629 };
1630 let result: Result<PcActor, _> = PcActor::new(config, &mut rng);
1631 assert!(result.is_err());
1632 let err = result.unwrap_err();
1633 assert!(matches!(err, crate::error::PcError::ConfigValidation(_)));
1634 }
1635
1636 #[test]
1637 fn test_new_zero_output_size_returns_error() {
1638 let mut rng = make_rng();
1639 let config = PcActorConfig {
1640 output_size: 0,
1641 ..default_config()
1642 };
1643 let result: Result<PcActor, _> = PcActor::new(config, &mut rng);
1644 assert!(result.is_err());
1645 }
1646
1647 #[test]
1648 fn test_new_zero_temperature_returns_error() {
1649 let mut rng = make_rng();
1650 let config = PcActorConfig {
1651 temperature: 0.0,
1652 ..default_config()
1653 };
1654 let result: Result<PcActor, _> = PcActor::new(config, &mut rng);
1655 assert!(result.is_err());
1656 }
1657
1658 #[test]
1659 fn test_new_negative_temperature_returns_error() {
1660 let mut rng = make_rng();
1661 let config = PcActorConfig {
1662 temperature: -1.0,
1663 ..default_config()
1664 };
1665 let result: Result<PcActor, _> = PcActor::new(config, &mut rng);
1666 assert!(result.is_err());
1667 }
1668
1669 #[test]
1672 fn test_default_config_residual_false() {
1673 let config = default_config();
1674 assert!(!config.residual);
1675 }
1676
1677 #[test]
1678 fn test_default_config_rezero_init() {
1679 let config = default_config();
1680 assert!((config.rezero_init - 0.001).abs() < 1e-12);
1681 }
1682
1683 #[test]
1684 fn test_new_negative_rezero_init_returns_error() {
1685 let mut rng = make_rng();
1686 let config = PcActorConfig {
1687 residual: true,
1688 rezero_init: -0.1,
1689 ..default_config()
1690 };
1691 let result: Result<PcActor, _> = PcActor::new(config, &mut rng);
1692 assert!(result.is_err());
1693 }
1694
1695 #[test]
1696 fn test_residual_mixed_sizes_accepted() {
1697 let mut rng = make_rng();
1698 let config = PcActorConfig {
1699 residual: true,
1700 hidden_layers: vec![
1701 LayerDef {
1702 size: 27,
1703 activation: Activation::Tanh,
1704 },
1705 LayerDef {
1706 size: 18,
1707 activation: Activation::Tanh,
1708 },
1709 ],
1710 ..default_config()
1711 };
1712 let result: Result<PcActor, _> = PcActor::new(config, &mut rng);
1713 assert!(result.is_ok());
1714 }
1715
1716 #[test]
1717 fn test_residual_mixed_sizes_all_skip() {
1718 let mut rng = make_rng();
1720 let config = PcActorConfig {
1721 residual: true,
1722 hidden_layers: vec![
1723 LayerDef {
1724 size: 27,
1725 activation: Activation::Tanh,
1726 },
1727 LayerDef {
1728 size: 27,
1729 activation: Activation::Tanh,
1730 },
1731 LayerDef {
1732 size: 18,
1733 activation: Activation::Tanh,
1734 },
1735 ],
1736 ..default_config()
1737 };
1738 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
1739 assert_eq!(actor.rezero_alpha.len(), 2);
1741 }
1742
1743 #[test]
1744 fn test_residual_heterogeneous_has_projection() {
1745 let mut rng = make_rng();
1747 let config = PcActorConfig {
1748 residual: true,
1749 hidden_layers: vec![
1750 LayerDef {
1751 size: 27,
1752 activation: Activation::Tanh,
1753 },
1754 LayerDef {
1755 size: 18,
1756 activation: Activation::Tanh,
1757 },
1758 ],
1759 ..default_config()
1760 };
1761 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
1762 assert_eq!(actor.rezero_alpha.len(), 1);
1763 assert_eq!(actor.skip_projections.len(), 1);
1764 assert!(actor.skip_projections[0].is_some());
1765 let proj = actor.skip_projections[0].as_ref().unwrap();
1766 assert_eq!(proj.rows, 18); assert_eq!(proj.cols, 27); }
1769
1770 #[test]
1771 fn test_residual_homogeneous_no_projection() {
1772 let mut rng = make_rng();
1774 let actor: PcActor = PcActor::new(residual_two_hidden_config(), &mut rng).unwrap();
1775 assert_eq!(actor.skip_projections.len(), 1);
1776 assert!(actor.skip_projections[0].is_none());
1777 }
1778
1779 #[test]
1780 fn test_residual_mixed_sizes_infer_finite() {
1781 let mut rng = make_rng();
1782 let config = PcActorConfig {
1783 residual: true,
1784 hidden_layers: vec![
1785 LayerDef {
1786 size: 27,
1787 activation: Activation::Tanh,
1788 },
1789 LayerDef {
1790 size: 27,
1791 activation: Activation::Tanh,
1792 },
1793 LayerDef {
1794 size: 18,
1795 activation: Activation::Tanh,
1796 },
1797 ],
1798 ..default_config()
1799 };
1800 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
1801 let result = actor.infer(&[0.5; 9]);
1802 for &v in &result.y_conv {
1803 assert!(v.is_finite());
1804 }
1805 assert_eq!(result.hidden_states.len(), 3);
1806 assert_eq!(result.latent_concat.len(), 27 + 27 + 18);
1807 }
1808
1809 #[test]
1810 fn test_residual_same_size_hidden_layers_accepted() {
1811 let mut rng = make_rng();
1812 let config = PcActorConfig {
1813 residual: true,
1814 hidden_layers: vec![
1815 LayerDef {
1816 size: 27,
1817 activation: Activation::Tanh,
1818 },
1819 LayerDef {
1820 size: 27,
1821 activation: Activation::Tanh,
1822 },
1823 ],
1824 ..default_config()
1825 };
1826 let result: Result<PcActor, _> = PcActor::new(config, &mut rng);
1827 assert!(result.is_ok());
1828 }
1829
1830 fn residual_two_hidden_config() -> PcActorConfig {
1831 PcActorConfig {
1832 residual: true,
1833 hidden_layers: vec![
1834 LayerDef {
1835 size: 27,
1836 activation: Activation::Tanh,
1837 },
1838 LayerDef {
1839 size: 27,
1840 activation: Activation::Tanh,
1841 },
1842 ],
1843 ..default_config()
1844 }
1845 }
1846
1847 #[test]
1848 fn test_non_residual_actor_empty_rezero_alpha() {
1849 let mut rng = make_rng();
1850 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
1851 assert!(actor.rezero_alpha.is_empty());
1852 }
1853
1854 #[test]
1855 fn test_residual_two_hidden_one_rezero_alpha() {
1856 let mut rng = make_rng();
1857 let actor: PcActor = PcActor::new(residual_two_hidden_config(), &mut rng).unwrap();
1858 assert_eq!(actor.rezero_alpha.len(), 1);
1859 }
1860
1861 #[test]
1862 fn test_residual_three_hidden_two_rezero_alpha() {
1863 let mut rng = make_rng();
1864 let config = PcActorConfig {
1865 residual: true,
1866 hidden_layers: vec![
1867 LayerDef {
1868 size: 27,
1869 activation: Activation::Tanh,
1870 },
1871 LayerDef {
1872 size: 27,
1873 activation: Activation::Tanh,
1874 },
1875 LayerDef {
1876 size: 27,
1877 activation: Activation::Tanh,
1878 },
1879 ],
1880 ..default_config()
1881 };
1882 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
1883 assert_eq!(actor.rezero_alpha.len(), 2);
1884 }
1885
1886 #[test]
1887 fn test_rezero_alpha_initialized_to_rezero_init() {
1888 let mut rng = make_rng();
1889 let config = PcActorConfig {
1890 rezero_init: 0.005,
1891 ..residual_two_hidden_config()
1892 };
1893 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
1894 assert!((actor.rezero_alpha[0] - 0.005).abs() < 1e-12);
1895 }
1896
1897 #[test]
1898 fn test_residual_single_hidden_zero_rezero_alpha() {
1899 let mut rng = make_rng();
1900 let config = PcActorConfig {
1901 residual: true,
1902 ..default_config()
1903 };
1904 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
1905 assert!(actor.rezero_alpha.is_empty());
1906 }
1907
1908 #[test]
1909 fn test_residual_single_hidden_accepted() {
1910 let mut rng = make_rng();
1911 let config = PcActorConfig {
1912 residual: true,
1913 ..default_config()
1914 };
1915 let result: Result<PcActor, _> = PcActor::new(config, &mut rng);
1916 assert!(result.is_ok());
1917 }
1918
1919 #[test]
1924 fn test_residual_false_identical_to_non_residual() {
1925 let input = vec![1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5];
1926 let mut rng1 = make_rng();
1927 let actor1: PcActor = PcActor::new(two_hidden_config(), &mut rng1).unwrap();
1928 let result1 = actor1.infer(&input);
1929
1930 let mut rng2 = make_rng();
1931 let config2 = PcActorConfig {
1932 residual: false,
1933 ..two_hidden_config()
1934 };
1935 let actor2: PcActor = PcActor::new(config2, &mut rng2).unwrap();
1936 let result2 = actor2.infer(&input);
1937
1938 for (a, b) in result1.y_conv.iter().zip(result2.y_conv.iter()) {
1939 assert!((a - b).abs() < 1e-12);
1940 }
1941 }
1942
1943 #[test]
1944 fn test_residual_rezero_zero_second_hidden_near_identity() {
1945 let mut rng = make_rng();
1946 let config = PcActorConfig {
1947 rezero_init: 0.0,
1948 alpha: 0.0,
1949 ..residual_two_hidden_config()
1950 };
1951 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
1952 let result = actor.infer(&[0.5; 9]);
1953 let h0 = &result.hidden_states[0];
1954 let h1 = &result.hidden_states[1];
1955 for (a, b) in h0.iter().zip(h1.iter()) {
1956 assert!(
1957 (a - b).abs() < 1e-12,
1958 "With rezero_init=0, h[1] should equal h[0]"
1959 );
1960 }
1961 }
1962
1963 #[test]
1964 fn test_residual_infer_all_outputs_finite() {
1965 let mut rng = make_rng();
1966 let actor: PcActor = PcActor::new(residual_two_hidden_config(), &mut rng).unwrap();
1967 let result = actor.infer(&[0.5; 9]);
1968 for &v in &result.y_conv {
1969 assert!(v.is_finite());
1970 }
1971 for &v in &result.latent_concat {
1972 assert!(v.is_finite());
1973 }
1974 assert!(result.surprise_score.is_finite());
1975 }
1976
1977 #[test]
1978 fn test_residual_latent_concat_size() {
1979 let mut rng = make_rng();
1980 let actor: PcActor = PcActor::new(residual_two_hidden_config(), &mut rng).unwrap();
1981 let result = actor.infer(&[0.5; 9]);
1982 assert_eq!(result.latent_concat.len(), 54); }
1984
1985 #[test]
1986 fn test_residual_pc_loop_completes() {
1987 let mut rng = make_rng();
1988 let config = PcActorConfig {
1989 alpha: 0.03,
1990 max_steps: 5,
1991 ..residual_two_hidden_config()
1992 };
1993 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
1994 let result = actor.infer(&[0.5; 9]);
1995 assert!(result.steps_used > 0);
1996 assert!(result.steps_used <= 5);
1997 }
1998
1999 #[test]
2000 fn test_residual_hidden_states_count() {
2001 let mut rng = make_rng();
2002 let actor: PcActor = PcActor::new(residual_two_hidden_config(), &mut rng).unwrap();
2003 let result = actor.infer(&[0.5; 9]);
2004 assert_eq!(result.hidden_states.len(), 2);
2005 }
2006
2007 #[test]
2008 fn test_residual_infer_does_not_modify_weights() {
2009 let mut rng = make_rng();
2010 let actor: PcActor = PcActor::new(residual_two_hidden_config(), &mut rng).unwrap();
2011 let weights_before: Vec<Vec<f64>> = actor
2012 .layers
2013 .iter()
2014 .map(|l| l.weights.data.clone())
2015 .collect();
2016 let alpha_before = actor.rezero_alpha.clone();
2017 let _ = actor.infer(&[0.5; 9]);
2018 for (i, layer) in actor.layers.iter().enumerate() {
2019 assert_eq!(layer.weights.data, weights_before[i]);
2020 }
2021 assert_eq!(actor.rezero_alpha, alpha_before);
2022 }
2023
2024 #[test]
2025 fn test_residual_three_hidden_infer_finite() {
2026 let mut rng = make_rng();
2027 let config = PcActorConfig {
2028 residual: true,
2029 hidden_layers: vec![
2030 LayerDef {
2031 size: 27,
2032 activation: Activation::Tanh,
2033 },
2034 LayerDef {
2035 size: 27,
2036 activation: Activation::Tanh,
2037 },
2038 LayerDef {
2039 size: 27,
2040 activation: Activation::Tanh,
2041 },
2042 ],
2043 ..default_config()
2044 };
2045 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
2046 let result = actor.infer(&[0.5; 9]);
2047 for &v in &result.y_conv {
2048 assert!(v.is_finite());
2049 }
2050 }
2051
2052 #[test]
2053 fn test_residual_tanh_components_populated() {
2054 let mut rng = make_rng();
2055 let actor: PcActor = PcActor::new(residual_two_hidden_config(), &mut rng).unwrap();
2056 let result = actor.infer(&[0.5; 9]);
2057 assert_eq!(result.tanh_components.len(), 2);
2058 assert!(result.tanh_components[0].is_none()); assert!(result.tanh_components[1].is_some()); assert_eq!(result.tanh_components[1].as_ref().unwrap().len(), 27);
2061 }
2062
2063 #[test]
2064 fn test_residual_pc_prediction_uses_tanh_component_not_full_state() {
2065 let mut rng = make_rng();
2071 let config = PcActorConfig {
2072 rezero_init: 1.0,
2073 alpha: 0.1,
2074 max_steps: 20,
2075 tol: 0.001,
2076 min_steps: 1,
2077 ..residual_two_hidden_config()
2078 };
2079 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
2080 let result = actor.infer(&[1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5]);
2081 assert!(result.surprise_score.is_finite());
2083 assert!(result.surprise_score >= 0.0);
2084 for errors in &result.prediction_errors {
2086 for &e in errors {
2087 assert!(e.is_finite(), "PC prediction error not finite: {e}");
2088 }
2089 }
2090 }
2091
2092 #[test]
2095 fn test_residual_false_update_identical_to_non_residual() {
2096 let input = vec![1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5];
2097 let delta = vec![0.1; 9];
2098
2099 let mut rng1 = make_rng();
2100 let mut actor1: PcActor = PcActor::new(two_hidden_config(), &mut rng1).unwrap();
2101 let infer1 = actor1.infer(&input);
2102 actor1.update_weights(&delta, &infer1, &input, 1.0);
2103
2104 let mut rng2 = make_rng();
2105 let config2 = PcActorConfig {
2106 residual: false,
2107 ..two_hidden_config()
2108 };
2109 let mut actor2: PcActor = PcActor::new(config2, &mut rng2).unwrap();
2110 let infer2 = actor2.infer(&input);
2111 actor2.update_weights(&delta, &infer2, &input, 1.0);
2112
2113 for i in 0..actor1.layers.len() {
2114 assert_eq!(actor1.layers[i].weights.data, actor2.layers[i].weights.data);
2115 }
2116 }
2117
2118 #[test]
2119 fn test_residual_update_changes_all_layer_weights() {
2120 let mut rng = make_rng();
2121 let mut actor: PcActor = PcActor::new(residual_two_hidden_config(), &mut rng).unwrap();
2122 let input = vec![0.5; 9];
2123 let infer_result = actor.infer(&input);
2124 let w0 = actor.layers[0].weights.data.clone();
2125 let w1 = actor.layers[1].weights.data.clone();
2126 let w2 = actor.layers[2].weights.data.clone();
2127 actor.update_weights(&[0.1; 9], &infer_result, &input, 1.0);
2128 assert_ne!(actor.layers[0].weights.data, w0, "Layer 0 should change");
2129 assert_ne!(actor.layers[1].weights.data, w1, "Layer 1 should change");
2130 assert_ne!(
2131 actor.layers[2].weights.data, w2,
2132 "Output layer should change"
2133 );
2134 }
2135
2136 #[test]
2137 fn test_residual_update_changes_rezero_alpha() {
2138 let mut rng = make_rng();
2139 let mut actor: PcActor = PcActor::new(residual_two_hidden_config(), &mut rng).unwrap();
2140 let input = vec![0.5; 9];
2141 let infer_result = actor.infer(&input);
2142 let alpha_before = actor.rezero_alpha.clone();
2143 actor.update_weights(&[0.1; 9], &infer_result, &input, 1.0);
2144 assert_ne!(
2145 actor.rezero_alpha, alpha_before,
2146 "rezero_alpha should be updated by backprop"
2147 );
2148 }
2149
2150 #[test]
2151 fn test_residual_update_clips_weights() {
2152 let mut rng = make_rng();
2153 let mut actor: PcActor = PcActor::new(residual_two_hidden_config(), &mut rng).unwrap();
2154 let input = vec![1.0; 9];
2155 let infer_result = actor.infer(&input);
2156 actor.update_weights(&[1e6; 9], &infer_result, &input, 1.0);
2157 for layer in &actor.layers {
2158 for &w in &layer.weights.data {
2159 assert!(
2160 w.abs() <= WEIGHT_CLIP + 1e-12,
2161 "Weight {w} exceeds WEIGHT_CLIP"
2162 );
2163 }
2164 }
2165 }
2166
2167 #[test]
2168 fn test_residual_gradient_stronger_than_non_residual() {
2169 let input = vec![1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5];
2170 let delta = vec![0.1; 9];
2171
2172 let mut rng1 = make_rng();
2174 let config1 = PcActorConfig {
2175 hidden_layers: vec![
2176 LayerDef {
2177 size: 27,
2178 activation: Activation::Tanh,
2179 },
2180 LayerDef {
2181 size: 27,
2182 activation: Activation::Tanh,
2183 },
2184 ],
2185 ..default_config()
2186 };
2187 let mut actor1: PcActor = PcActor::new(config1, &mut rng1).unwrap();
2188 let w0_before1 = actor1.layers[0].weights.data.clone();
2189 let infer1 = actor1.infer(&input);
2190 actor1.update_weights(&delta, &infer1, &input, 1.0);
2191 let change1: f64 = actor1.layers[0]
2192 .weights
2193 .data
2194 .iter()
2195 .zip(w0_before1.iter())
2196 .map(|(a, b)| (a - b).abs())
2197 .sum();
2198
2199 let mut rng2 = make_rng();
2201 let config2 = PcActorConfig {
2202 rezero_init: 1.0,
2203 ..residual_two_hidden_config()
2204 };
2205 let mut actor2: PcActor = PcActor::new(config2, &mut rng2).unwrap();
2206 let w0_before2 = actor2.layers[0].weights.data.clone();
2207 let infer2 = actor2.infer(&input);
2208 actor2.update_weights(&delta, &infer2, &input, 1.0);
2209 let change2: f64 = actor2.layers[0]
2210 .weights
2211 .data
2212 .iter()
2213 .zip(w0_before2.iter())
2214 .map(|(a, b)| (a - b).abs())
2215 .sum();
2216
2217 assert!(
2218 change2 > change1,
2219 "Residual should propagate stronger gradient to layer 0: residual={change2:.6}, non-residual={change1:.6}"
2220 );
2221 }
2222
2223 #[test]
2224 fn test_residual_hybrid_lambda_works() {
2225 let mut rng = make_rng();
2226 let config = PcActorConfig {
2227 local_lambda: 0.99,
2228 ..residual_two_hidden_config()
2229 };
2230 let mut actor: PcActor = PcActor::new(config, &mut rng).unwrap();
2231 let input = vec![0.5; 9];
2232 let infer_result = actor.infer(&input);
2233 let w0_before = actor.layers[0].weights.data.clone();
2234 actor.update_weights(&[0.1; 9], &infer_result, &input, 1.0);
2235 assert_ne!(actor.layers[0].weights.data, w0_before);
2236 }
2237
2238 fn local_learning_config() -> PcActorConfig {
2239 PcActorConfig {
2240 local_lambda: 0.0,
2241 ..default_config()
2242 }
2243 }
2244
2245 #[test]
2246 fn test_infer_prediction_errors_count_matches_hidden_layers() {
2247 let mut rng = make_rng();
2248 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
2249 let result = actor.infer(&[0.0; 9]);
2250 assert_eq!(result.prediction_errors.len(), 1);
2251 }
2252
2253 #[test]
2254 fn test_infer_prediction_errors_two_hidden() {
2255 let mut rng = make_rng();
2256 let actor: PcActor = PcActor::new(two_hidden_config(), &mut rng).unwrap();
2257 let result = actor.infer(&[0.0; 9]);
2258 assert_eq!(result.prediction_errors.len(), 2);
2259 }
2260
2261 #[test]
2262 fn test_infer_prediction_errors_zero_hidden_is_empty() {
2263 let mut rng = make_rng();
2264 let config = PcActorConfig {
2265 hidden_layers: vec![],
2266 ..default_config()
2267 };
2268 let actor: PcActor = PcActor::new(config, &mut rng).unwrap();
2269 let result = actor.infer(&[0.5; 9]);
2270 assert!(result.prediction_errors.is_empty());
2271 }
2272
2273 #[test]
2274 fn test_infer_prediction_errors_all_finite() {
2275 let mut rng = make_rng();
2276 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
2277 let result = actor.infer(&[1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5]);
2278 for errors in &result.prediction_errors {
2279 for &e in errors {
2280 assert!(e.is_finite(), "prediction error not finite: {e}");
2281 }
2282 }
2283 }
2284
2285 #[test]
2286 fn test_infer_prediction_errors_size_matches_hidden_layer_size() {
2287 let mut rng = make_rng();
2288 let actor: PcActor = PcActor::new(default_config(), &mut rng).unwrap();
2289 let result = actor.infer(&[0.0; 9]);
2290 assert_eq!(result.prediction_errors[0].len(), 18);
2292 }
2293
2294 #[test]
2295 fn test_local_learning_config_accepted() {
2296 let mut rng = make_rng();
2297 let config = local_learning_config();
2298 assert!((config.local_lambda).abs() < f64::EPSILON);
2299 let actor: Result<PcActor, _> = PcActor::new(config, &mut rng);
2300 assert!(actor.is_ok());
2301 }
2302
2303 #[test]
2304 fn test_local_learning_update_changes_weights() {
2305 let mut rng = make_rng();
2306 let mut actor: PcActor = PcActor::new(local_learning_config(), &mut rng).unwrap();
2307 let input = vec![1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5];
2308 let infer_result = actor.infer(&input);
2309 let weights_before = actor.layers[0].weights.data.clone();
2310 let delta = vec![0.1; 9];
2311 actor.update_weights(&delta, &infer_result, &input, 1.0);
2312 assert_ne!(actor.layers[0].weights.data, weights_before);
2313 }
2314
2315 #[test]
2316 fn test_local_learning_clips_weights() {
2317 let mut rng = make_rng();
2318 let mut actor: PcActor = PcActor::new(local_learning_config(), &mut rng).unwrap();
2319 let input = vec![1.0; 9];
2320 let infer_result = actor.infer(&input);
2321 let delta = vec![1e6; 9];
2322 actor.update_weights(&delta, &infer_result, &input, 1.0);
2323 for layer in &actor.layers {
2324 for &w in &layer.weights.data {
2325 assert!(
2326 w.abs() <= WEIGHT_CLIP + 1e-12,
2327 "Weight {w} exceeds WEIGHT_CLIP"
2328 );
2329 }
2330 }
2331 }
2332
2333 #[test]
2334 fn test_local_learning_two_hidden_changes_both() {
2335 let mut rng = make_rng();
2336 let config = PcActorConfig {
2337 local_lambda: 0.0,
2338 ..two_hidden_config()
2339 };
2340 let mut actor: PcActor = PcActor::new(config, &mut rng).unwrap();
2341 let input = vec![0.5; 9];
2342 let infer_result = actor.infer(&input);
2343 let w0_before = actor.layers[0].weights.data.clone();
2344 let w1_before = actor.layers[1].weights.data.clone();
2345 let delta = vec![0.1; 9];
2346 actor.update_weights(&delta, &infer_result, &input, 1.0);
2347 assert_ne!(
2348 actor.layers[0].weights.data, w0_before,
2349 "Layer 0 should change"
2350 );
2351 assert_ne!(
2352 actor.layers[1].weights.data, w1_before,
2353 "Layer 1 should change"
2354 );
2355 }
2356
2357 #[test]
2358 fn test_local_learning_differs_from_backprop() {
2359 let input = vec![1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5];
2360 let delta = vec![0.1; 9];
2361
2362 let mut rng1 = make_rng();
2364 let mut bp_actor: PcActor = PcActor::new(default_config(), &mut rng1).unwrap();
2365 let bp_infer = bp_actor.infer(&input);
2366 bp_actor.update_weights(&delta, &bp_infer, &input, 1.0);
2367
2368 let mut rng2 = make_rng();
2370 let mut ll_actor: PcActor = PcActor::new(local_learning_config(), &mut rng2).unwrap();
2371 let ll_infer = ll_actor.infer(&input);
2372 ll_actor.update_weights(&delta, &ll_infer, &input, 1.0);
2373
2374 assert_ne!(
2376 bp_actor.layers[0].weights.data, ll_actor.layers[0].weights.data,
2377 "Local learning should produce different weight updates than backprop"
2378 );
2379 }
2380
2381 fn hybrid_config(lambda: f64) -> PcActorConfig {
2384 PcActorConfig {
2385 local_lambda: lambda,
2386 ..default_config()
2387 }
2388 }
2389
2390 #[test]
2391 fn test_local_lambda_one_equals_backprop() {
2392 let input = vec![1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5];
2393 let delta = vec![0.1; 9];
2394
2395 let mut rng1 = make_rng();
2397 let mut bp_actor: PcActor = PcActor::new(default_config(), &mut rng1).unwrap();
2398 let bp_infer = bp_actor.infer(&input);
2399 bp_actor.update_weights(&delta, &bp_infer, &input, 1.0);
2400
2401 let mut rng2 = make_rng();
2403 let mut lam_actor: PcActor = PcActor::new(hybrid_config(1.0), &mut rng2).unwrap();
2404 let lam_infer = lam_actor.infer(&input);
2405 lam_actor.update_weights(&delta, &lam_infer, &input, 1.0);
2406
2407 assert_eq!(
2408 bp_actor.layers[0].weights.data, lam_actor.layers[0].weights.data,
2409 "lambda=1.0 should produce identical weights to pure backprop"
2410 );
2411 }
2412
2413 #[test]
2414 fn test_local_lambda_zero_equals_local_learning() {
2415 let input = vec![1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5];
2416 let delta = vec![0.1; 9];
2417
2418 let mut rng1 = make_rng();
2420 let mut ll_actor: PcActor = PcActor::new(local_learning_config(), &mut rng1).unwrap();
2421 let ll_infer = ll_actor.infer(&input);
2422 ll_actor.update_weights(&delta, &ll_infer, &input, 1.0);
2423
2424 let mut rng2 = make_rng();
2426 let mut lam_actor: PcActor = PcActor::new(hybrid_config(0.0), &mut rng2).unwrap();
2427 let lam_infer = lam_actor.infer(&input);
2428 lam_actor.update_weights(&delta, &lam_infer, &input, 1.0);
2429
2430 assert_eq!(
2431 ll_actor.layers[0].weights.data, lam_actor.layers[0].weights.data,
2432 "lambda=0.0 should produce identical weights to pure local learning"
2433 );
2434 }
2435
2436 #[test]
2437 fn test_local_lambda_half_differs_from_both_pure_modes() {
2438 let input = vec![1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5];
2439 let delta = vec![0.1; 9];
2440
2441 let mut rng1 = make_rng();
2443 let mut bp_actor: PcActor = PcActor::new(default_config(), &mut rng1).unwrap();
2444 let bp_infer = bp_actor.infer(&input);
2445 bp_actor.update_weights(&delta, &bp_infer, &input, 1.0);
2446
2447 let mut rng2 = make_rng();
2449 let mut ll_actor: PcActor = PcActor::new(local_learning_config(), &mut rng2).unwrap();
2450 let ll_infer = ll_actor.infer(&input);
2451 ll_actor.update_weights(&delta, &ll_infer, &input, 1.0);
2452
2453 let mut rng3 = make_rng();
2455 let mut hy_actor: PcActor = PcActor::new(hybrid_config(0.5), &mut rng3).unwrap();
2456 let hy_infer = hy_actor.infer(&input);
2457 hy_actor.update_weights(&delta, &hy_infer, &input, 1.0);
2458
2459 assert_ne!(
2460 hy_actor.layers[0].weights.data, bp_actor.layers[0].weights.data,
2461 "lambda=0.5 should differ from pure backprop"
2462 );
2463 assert_ne!(
2464 hy_actor.layers[0].weights.data, ll_actor.layers[0].weights.data,
2465 "lambda=0.5 should differ from pure local"
2466 );
2467 }
2468
2469 #[test]
2470 fn test_local_lambda_changes_weights() {
2471 let mut rng = make_rng();
2472 let mut actor: PcActor = PcActor::new(hybrid_config(0.5), &mut rng).unwrap();
2473 let input = vec![1.0, -1.0, 0.5, -0.5, 0.0, 1.0, -1.0, 0.5, -0.5];
2474 let infer_result = actor.infer(&input);
2475 let weights_before = actor.layers[0].weights.data.clone();
2476 let delta = vec![0.1; 9];
2477 actor.update_weights(&delta, &infer_result, &input, 1.0);
2478 assert_ne!(actor.layers[0].weights.data, weights_before);
2479 }
2480
2481 #[test]
2482 fn test_local_lambda_clips_weights() {
2483 let mut rng = make_rng();
2484 let mut actor: PcActor = PcActor::new(hybrid_config(0.5), &mut rng).unwrap();
2485 let input = vec![1.0; 9];
2486 let infer_result = actor.infer(&input);
2487 let delta = vec![1e6; 9];
2488 actor.update_weights(&delta, &infer_result, &input, 1.0);
2489 for layer in &actor.layers {
2490 for &w in &layer.weights.data {
2491 assert!(
2492 w.abs() <= WEIGHT_CLIP + 1e-12,
2493 "Weight {w} exceeds WEIGHT_CLIP"
2494 );
2495 }
2496 }
2497 }
2498
2499 #[test]
2500 fn test_local_lambda_negative_returns_error() {
2501 let mut rng = make_rng();
2502 let config = hybrid_config(-0.1);
2503 let result: Result<PcActor, _> = PcActor::new(config, &mut rng);
2504 assert!(result.is_err());
2505 }
2506
2507 #[test]
2508 fn test_local_lambda_above_one_returns_error() {
2509 let mut rng = make_rng();
2510 let config = hybrid_config(1.1);
2511 let result: Result<PcActor, _> = PcActor::new(config, &mut rng);
2512 assert!(result.is_err());
2513 }
2514
2515 fn crossover_config_27() -> PcActorConfig {
2518 PcActorConfig {
2519 input_size: 9,
2520 hidden_layers: vec![LayerDef {
2521 size: 27,
2522 activation: Activation::Tanh,
2523 }],
2524 output_size: 9,
2525 output_activation: Activation::Linear,
2526 alpha: 0.03,
2527 tol: 0.01,
2528 min_steps: 1,
2529 max_steps: 5,
2530 lr_weights: 0.005,
2531 synchronous: true,
2532 temperature: 1.0,
2533 local_lambda: 0.99,
2534 residual: false,
2535 rezero_init: 0.001,
2536 }
2537 }
2538
2539 fn make_caches_for_actor(actor: &PcActor, batch_size: usize) -> Vec<Vec<Vec<f64>>> {
2540 let num_hidden = actor.config.hidden_layers.len();
2541 let mut layers: Vec<Vec<Vec<f64>>> = (0..num_hidden).map(|_| Vec::new()).collect();
2542 for i in 0..batch_size {
2543 let input: Vec<f64> = (0..actor.config.input_size)
2544 .map(|j| ((i * actor.config.input_size + j) as f64 * 0.01).sin())
2545 .collect();
2546 let result = actor.infer(&input);
2547 for (layer_idx, state) in result.hidden_states.iter().enumerate() {
2548 layers[layer_idx].push(state.clone());
2549 }
2550 }
2551 layers
2552 }
2553
2554 fn build_cache_matrix(
2555 cache_layers: &[Vec<Vec<f64>>],
2556 layer_idx: usize,
2557 ) -> crate::matrix::Matrix {
2558 use crate::linalg::LinAlg;
2559 let samples = &cache_layers[layer_idx];
2560 let batch_size = samples.len();
2561 let n_neurons = samples[0].len();
2562 let mut mat = CpuLinAlg::zeros_mat(batch_size, n_neurons);
2563 for (r, sample) in samples.iter().enumerate() {
2564 for (c, &val) in sample.iter().enumerate() {
2565 CpuLinAlg::mat_set(&mut mat, r, c, val);
2566 }
2567 }
2568 mat
2569 }
2570
2571 #[test]
2572 fn test_crossover_same_topology_produces_valid_actor() {
2573 let mut rng_a = StdRng::seed_from_u64(42);
2574 let mut rng_b = StdRng::seed_from_u64(123);
2575 let config = crossover_config_27();
2576 let actor_a: PcActor = PcActor::new(config.clone(), &mut rng_a).unwrap();
2577 let actor_b: PcActor = PcActor::new(config.clone(), &mut rng_b).unwrap();
2578
2579 let caches_a = make_caches_for_actor(&actor_a, 50);
2580 let caches_b = make_caches_for_actor(&actor_b, 50);
2581 let cache_mats_a: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_a, i)).collect();
2582 let cache_mats_b: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_b, i)).collect();
2583
2584 let mut rng_child = StdRng::seed_from_u64(99);
2585 let child: PcActor = PcActor::crossover(
2586 &actor_a,
2587 &actor_b,
2588 &cache_mats_a,
2589 &cache_mats_b,
2590 0.5,
2591 config,
2592 &mut rng_child,
2593 )
2594 .unwrap();
2595
2596 assert_eq!(child.layers.len(), actor_a.layers.len());
2598 for (i, layer) in child.layers.iter().enumerate() {
2599 assert_eq!(
2600 CpuLinAlg::mat_rows(&layer.weights),
2601 CpuLinAlg::mat_rows(&actor_a.layers[i].weights)
2602 );
2603 assert_eq!(
2604 CpuLinAlg::mat_cols(&layer.weights),
2605 CpuLinAlg::mat_cols(&actor_a.layers[i].weights)
2606 );
2607 }
2608 }
2609
2610 #[test]
2611 fn test_crossover_same_topology_child_differs_from_parents() {
2612 let mut rng_a = StdRng::seed_from_u64(42);
2613 let mut rng_b = StdRng::seed_from_u64(123);
2614 let config = crossover_config_27();
2615 let actor_a: PcActor = PcActor::new(config.clone(), &mut rng_a).unwrap();
2616 let actor_b: PcActor = PcActor::new(config.clone(), &mut rng_b).unwrap();
2617
2618 let caches_a = make_caches_for_actor(&actor_a, 50);
2619 let caches_b = make_caches_for_actor(&actor_b, 50);
2620 let cache_mats_a: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_a, i)).collect();
2621 let cache_mats_b: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_b, i)).collect();
2622
2623 let mut rng_child = StdRng::seed_from_u64(99);
2624 let child: PcActor = PcActor::crossover(
2625 &actor_a,
2626 &actor_b,
2627 &cache_mats_a,
2628 &cache_mats_b,
2629 0.5,
2630 config,
2631 &mut rng_child,
2632 )
2633 .unwrap();
2634
2635 assert_ne!(child.layers[0].weights.data, actor_a.layers[0].weights.data);
2637 assert_ne!(child.layers[0].weights.data, actor_b.layers[0].weights.data);
2638 }
2639
2640 #[test]
2641 fn test_crossover_alpha_one_approximates_parent_a() {
2642 let mut rng_a = StdRng::seed_from_u64(42);
2643 let mut rng_b = StdRng::seed_from_u64(123);
2644 let config = crossover_config_27();
2645 let actor_a: PcActor = PcActor::new(config.clone(), &mut rng_a).unwrap();
2646 let actor_b: PcActor = PcActor::new(config.clone(), &mut rng_b).unwrap();
2647
2648 let caches_a = make_caches_for_actor(&actor_a, 50);
2649 let caches_b = make_caches_for_actor(&actor_b, 50);
2650 let cache_mats_a: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_a, i)).collect();
2651 let cache_mats_b: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_b, i)).collect();
2652
2653 let mut rng_child = StdRng::seed_from_u64(99);
2654 let child: PcActor = PcActor::crossover(
2655 &actor_a,
2656 &actor_b,
2657 &cache_mats_a,
2658 &cache_mats_b,
2659 1.0, config,
2661 &mut rng_child,
2662 )
2663 .unwrap();
2664
2665 let a_w = &actor_a.layers[0].weights.data;
2667 let child_w = &child.layers[0].weights.data;
2668 let max_diff: f64 = a_w
2669 .iter()
2670 .zip(child_w.iter())
2671 .map(|(a, c)| (a - c).abs())
2672 .fold(0.0_f64, f64::max);
2673 assert!(
2674 max_diff < 1e-10,
2675 "alpha=1.0: input layer max diff from parent A = {max_diff}"
2676 );
2677 }
2678
2679 #[test]
2680 fn test_crossover_child_weights_finite() {
2681 let mut rng_a = StdRng::seed_from_u64(42);
2682 let mut rng_b = StdRng::seed_from_u64(123);
2683 let config = crossover_config_27();
2684 let actor_a: PcActor = PcActor::new(config.clone(), &mut rng_a).unwrap();
2685 let actor_b: PcActor = PcActor::new(config.clone(), &mut rng_b).unwrap();
2686
2687 let caches_a = make_caches_for_actor(&actor_a, 50);
2688 let caches_b = make_caches_for_actor(&actor_b, 50);
2689 let cache_mats_a: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_a, i)).collect();
2690 let cache_mats_b: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_b, i)).collect();
2691
2692 let mut rng_child = StdRng::seed_from_u64(99);
2693 let child: PcActor = PcActor::crossover(
2694 &actor_a,
2695 &actor_b,
2696 &cache_mats_a,
2697 &cache_mats_b,
2698 0.5,
2699 config,
2700 &mut rng_child,
2701 )
2702 .unwrap();
2703
2704 for (i, layer) in child.layers.iter().enumerate() {
2705 for &w in &layer.weights.data {
2706 assert!(w.is_finite(), "NaN/Inf in layer {i} weights");
2707 }
2708 for b in CpuLinAlg::vec_to_vec(&layer.bias) {
2709 assert!(b.is_finite(), "NaN/Inf in layer {i} biases");
2710 }
2711 }
2712 }
2713
2714 #[test]
2717 fn test_crossover_child_smaller() {
2718 let mut rng_a = StdRng::seed_from_u64(42);
2719 let mut rng_b = StdRng::seed_from_u64(123);
2720 let config_27 = PcActorConfig {
2721 hidden_layers: vec![
2722 LayerDef {
2723 size: 27,
2724 activation: Activation::Tanh,
2725 },
2726 LayerDef {
2727 size: 27,
2728 activation: Activation::Tanh,
2729 },
2730 ],
2731 ..crossover_config_27()
2732 };
2733 let actor_a: PcActor = PcActor::new(config_27.clone(), &mut rng_a).unwrap();
2734 let actor_b: PcActor = PcActor::new(config_27, &mut rng_b).unwrap();
2735
2736 let caches_a = make_caches_for_actor(&actor_a, 50);
2737 let caches_b = make_caches_for_actor(&actor_b, 50);
2738 let cache_mats_a: Vec<_> = (0..2).map(|i| build_cache_matrix(&caches_a, i)).collect();
2739 let cache_mats_b: Vec<_> = (0..2).map(|i| build_cache_matrix(&caches_b, i)).collect();
2740
2741 let child_config = PcActorConfig {
2742 hidden_layers: vec![
2743 LayerDef {
2744 size: 18,
2745 activation: Activation::Tanh,
2746 },
2747 LayerDef {
2748 size: 18,
2749 activation: Activation::Tanh,
2750 },
2751 ],
2752 ..crossover_config_27()
2753 };
2754
2755 let mut rng_child = StdRng::seed_from_u64(99);
2756 let child: PcActor = PcActor::crossover(
2757 &actor_a,
2758 &actor_b,
2759 &cache_mats_a,
2760 &cache_mats_b,
2761 0.5,
2762 child_config,
2763 &mut rng_child,
2764 )
2765 .unwrap();
2766
2767 use crate::linalg::LinAlg;
2769 assert_eq!(CpuLinAlg::mat_rows(&child.layers[0].weights), 18);
2770 assert_eq!(CpuLinAlg::mat_rows(&child.layers[1].weights), 18);
2771 }
2772
2773 #[test]
2776 fn test_crossover_parents_different_sizes() {
2777 let mut rng_a = StdRng::seed_from_u64(42);
2778 let mut rng_b = StdRng::seed_from_u64(123);
2779 let config_a = crossover_config_27(); let config_b = PcActorConfig {
2781 hidden_layers: vec![LayerDef {
2782 size: 18,
2783 activation: Activation::Tanh,
2784 }],
2785 ..crossover_config_27()
2786 }; let actor_a: PcActor = PcActor::new(config_a, &mut rng_a).unwrap();
2789 let actor_b: PcActor = PcActor::new(config_b, &mut rng_b).unwrap();
2790
2791 let caches_a = make_caches_for_actor(&actor_a, 50);
2792 let caches_b = make_caches_for_actor(&actor_b, 50);
2793 let cache_mats_a: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_a, i)).collect();
2794 let cache_mats_b: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_b, i)).collect();
2795
2796 let child_config = crossover_config_27();
2798 let mut rng_child = StdRng::seed_from_u64(99);
2799 let child: PcActor = PcActor::crossover(
2800 &actor_a,
2801 &actor_b,
2802 &cache_mats_a,
2803 &cache_mats_b,
2804 0.5,
2805 child_config,
2806 &mut rng_child,
2807 )
2808 .unwrap();
2809
2810 use crate::linalg::LinAlg;
2811 assert_eq!(CpuLinAlg::mat_rows(&child.layers[0].weights), 27);
2813 for &w in &child.layers[0].weights.data {
2815 assert!(w.is_finite());
2816 }
2817 }
2818
2819 #[test]
2822 fn test_crossover_child_larger() {
2823 let mut rng_a = StdRng::seed_from_u64(42);
2824 let mut rng_b = StdRng::seed_from_u64(123);
2825 let config_18 = PcActorConfig {
2826 hidden_layers: vec![LayerDef {
2827 size: 18,
2828 activation: Activation::Tanh,
2829 }],
2830 ..crossover_config_27()
2831 };
2832 let actor_a: PcActor = PcActor::new(config_18.clone(), &mut rng_a).unwrap();
2833 let actor_b: PcActor = PcActor::new(config_18, &mut rng_b).unwrap();
2834
2835 let caches_a = make_caches_for_actor(&actor_a, 50);
2836 let caches_b = make_caches_for_actor(&actor_b, 50);
2837 let cache_mats_a: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_a, i)).collect();
2838 let cache_mats_b: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_b, i)).collect();
2839
2840 let child_config = crossover_config_27();
2842 let mut rng_child = StdRng::seed_from_u64(99);
2843 let child: PcActor = PcActor::crossover(
2844 &actor_a,
2845 &actor_b,
2846 &cache_mats_a,
2847 &cache_mats_b,
2848 0.5,
2849 child_config,
2850 &mut rng_child,
2851 )
2852 .unwrap();
2853
2854 use crate::linalg::LinAlg;
2855 assert_eq!(CpuLinAlg::mat_rows(&child.layers[0].weights), 27);
2856 for &w in &child.layers[0].weights.data {
2858 assert!(w.is_finite());
2859 }
2860 let xavier_zone_nonzero = (18..27).any(|r| {
2862 (0..CpuLinAlg::mat_cols(&child.layers[0].weights))
2863 .any(|c| CpuLinAlg::mat_get(&child.layers[0].weights, r, c).abs() > 1e-15)
2864 });
2865 assert!(
2866 xavier_zone_nonzero,
2867 "Xavier zone [18..27) should have non-zero weights"
2868 );
2869 }
2870
2871 #[test]
2874 fn test_crossover_child_more_layers() {
2875 let mut rng_a = StdRng::seed_from_u64(42);
2876 let mut rng_b = StdRng::seed_from_u64(123);
2877 let config_2l = PcActorConfig {
2878 hidden_layers: vec![
2879 LayerDef {
2880 size: 27,
2881 activation: Activation::Tanh,
2882 },
2883 LayerDef {
2884 size: 27,
2885 activation: Activation::Tanh,
2886 },
2887 ],
2888 ..crossover_config_27()
2889 };
2890 let actor_a: PcActor = PcActor::new(config_2l.clone(), &mut rng_a).unwrap();
2891 let actor_b: PcActor = PcActor::new(config_2l, &mut rng_b).unwrap();
2892
2893 let caches_a = make_caches_for_actor(&actor_a, 50);
2894 let caches_b = make_caches_for_actor(&actor_b, 50);
2895 let cache_mats_a: Vec<_> = (0..2).map(|i| build_cache_matrix(&caches_a, i)).collect();
2896 let cache_mats_b: Vec<_> = (0..2).map(|i| build_cache_matrix(&caches_b, i)).collect();
2897
2898 let child_config = PcActorConfig {
2900 hidden_layers: vec![
2901 LayerDef {
2902 size: 27,
2903 activation: Activation::Tanh,
2904 },
2905 LayerDef {
2906 size: 27,
2907 activation: Activation::Tanh,
2908 },
2909 LayerDef {
2910 size: 18,
2911 activation: Activation::Tanh,
2912 },
2913 ],
2914 ..crossover_config_27()
2915 };
2916
2917 let mut rng_child = StdRng::seed_from_u64(99);
2918 let child: PcActor = PcActor::crossover(
2919 &actor_a,
2920 &actor_b,
2921 &cache_mats_a,
2922 &cache_mats_b,
2923 0.5,
2924 child_config,
2925 &mut rng_child,
2926 )
2927 .unwrap();
2928
2929 use crate::linalg::LinAlg;
2930 assert_eq!(child.layers.len(), 4);
2932 assert_eq!(CpuLinAlg::mat_rows(&child.layers[2].weights), 18);
2934 for (i, layer) in child.layers.iter().enumerate() {
2936 for &w in &layer.weights.data {
2937 assert!(w.is_finite(), "NaN/Inf in layer {i}");
2938 }
2939 }
2940 }
2941
2942 #[test]
2943 fn test_crossover_child_fewer_layers() {
2944 let mut rng_a = StdRng::seed_from_u64(42);
2945 let mut rng_b = StdRng::seed_from_u64(123);
2946 let config_3l = PcActorConfig {
2947 hidden_layers: vec![
2948 LayerDef {
2949 size: 27,
2950 activation: Activation::Tanh,
2951 },
2952 LayerDef {
2953 size: 27,
2954 activation: Activation::Tanh,
2955 },
2956 LayerDef {
2957 size: 18,
2958 activation: Activation::Tanh,
2959 },
2960 ],
2961 ..crossover_config_27()
2962 };
2963 let actor_a: PcActor = PcActor::new(config_3l.clone(), &mut rng_a).unwrap();
2964 let actor_b: PcActor = PcActor::new(config_3l, &mut rng_b).unwrap();
2965
2966 let caches_a = make_caches_for_actor(&actor_a, 50);
2967 let caches_b = make_caches_for_actor(&actor_b, 50);
2968 let cache_mats_a: Vec<_> = (0..3).map(|i| build_cache_matrix(&caches_a, i)).collect();
2969 let cache_mats_b: Vec<_> = (0..3).map(|i| build_cache_matrix(&caches_b, i)).collect();
2970
2971 let child_config = PcActorConfig {
2973 hidden_layers: vec![
2974 LayerDef {
2975 size: 27,
2976 activation: Activation::Tanh,
2977 },
2978 LayerDef {
2979 size: 27,
2980 activation: Activation::Tanh,
2981 },
2982 ],
2983 ..crossover_config_27()
2984 };
2985
2986 let mut rng_child = StdRng::seed_from_u64(99);
2987 let child: PcActor = PcActor::crossover(
2988 &actor_a,
2989 &actor_b,
2990 &cache_mats_a,
2991 &cache_mats_b,
2992 0.5,
2993 child_config,
2994 &mut rng_child,
2995 )
2996 .unwrap();
2997
2998 use crate::linalg::LinAlg;
2999 assert_eq!(child.layers.len(), 3);
3001 assert_eq!(CpuLinAlg::mat_cols(&child.layers[2].weights), 27);
3003 }
3004
3005 #[test]
3008 fn test_crossover_residual_rezero_blended() {
3009 let mut rng_a = StdRng::seed_from_u64(42);
3010 let mut rng_b = StdRng::seed_from_u64(123);
3011 let config = PcActorConfig {
3012 hidden_layers: vec![
3013 LayerDef {
3014 size: 27,
3015 activation: Activation::Softsign,
3016 },
3017 LayerDef {
3018 size: 27,
3019 activation: Activation::Softsign,
3020 },
3021 ],
3022 residual: true,
3023 rezero_init: 0.1,
3024 ..crossover_config_27()
3025 };
3026 let actor_a: PcActor = PcActor::new(config.clone(), &mut rng_a).unwrap();
3027 let actor_b: PcActor = PcActor::new(config.clone(), &mut rng_b).unwrap();
3028
3029 let caches_a = make_caches_for_actor(&actor_a, 50);
3030 let caches_b = make_caches_for_actor(&actor_b, 50);
3031 let cache_mats_a: Vec<_> = (0..2).map(|i| build_cache_matrix(&caches_a, i)).collect();
3032 let cache_mats_b: Vec<_> = (0..2).map(|i| build_cache_matrix(&caches_b, i)).collect();
3033
3034 let mut rng_child = StdRng::seed_from_u64(99);
3035 let child: PcActor = PcActor::crossover(
3036 &actor_a,
3037 &actor_b,
3038 &cache_mats_a,
3039 &cache_mats_b,
3040 0.5,
3041 config,
3042 &mut rng_child,
3043 )
3044 .unwrap();
3045
3046 assert!(!child.rezero_alpha.is_empty());
3048 for &rz in &child.rezero_alpha {
3051 assert!(rz.is_finite(), "rezero_alpha is not finite");
3052 }
3053 }
3054
3055 #[test]
3056 fn test_crossover_residual_skip_projections_blended() {
3057 let mut rng_a = StdRng::seed_from_u64(42);
3058 let mut rng_b = StdRng::seed_from_u64(123);
3059 let config = PcActorConfig {
3060 hidden_layers: vec![
3061 LayerDef {
3062 size: 27,
3063 activation: Activation::Softsign,
3064 },
3065 LayerDef {
3066 size: 18,
3067 activation: Activation::Softsign,
3068 },
3069 ],
3070 residual: true,
3071 rezero_init: 0.1,
3072 ..crossover_config_27()
3073 };
3074 let actor_a: PcActor = PcActor::new(config.clone(), &mut rng_a).unwrap();
3075 let actor_b: PcActor = PcActor::new(config.clone(), &mut rng_b).unwrap();
3076
3077 let caches_a = make_caches_for_actor(&actor_a, 50);
3078 let caches_b = make_caches_for_actor(&actor_b, 50);
3079 let cache_mats_a: Vec<_> = (0..2).map(|i| build_cache_matrix(&caches_a, i)).collect();
3080 let cache_mats_b: Vec<_> = (0..2).map(|i| build_cache_matrix(&caches_b, i)).collect();
3081
3082 let mut rng_child = StdRng::seed_from_u64(99);
3083 let child: PcActor = PcActor::crossover(
3084 &actor_a,
3085 &actor_b,
3086 &cache_mats_a,
3087 &cache_mats_b,
3088 0.5,
3089 config,
3090 &mut rng_child,
3091 )
3092 .unwrap();
3093
3094 assert!(!child.skip_projections.is_empty());
3096 let has_projection = child.skip_projections.iter().any(|p| p.is_some());
3098 assert!(has_projection, "Expected at least one skip projection");
3099
3100 for mat in child.skip_projections.iter().flatten() {
3102 for &w in &mat.data {
3103 assert!(w.is_finite(), "NaN/Inf in skip projection");
3104 }
3105 }
3106 }
3107
3108 #[test]
3111 fn test_crossover_multilayer_column_permutation_consistency() {
3112 use crate::linalg::LinAlg;
3125 let mut rng_a = StdRng::seed_from_u64(42);
3126 let mut rng_b = StdRng::seed_from_u64(123);
3127 let config = PcActorConfig {
3128 hidden_layers: vec![
3129 LayerDef {
3130 size: 8,
3131 activation: Activation::Tanh,
3132 },
3133 LayerDef {
3134 size: 8,
3135 activation: Activation::Tanh,
3136 },
3137 ],
3138 input_size: 4,
3139 output_size: 4,
3140 ..crossover_config_27()
3141 };
3142 let actor_a: PcActor = PcActor::new(config.clone(), &mut rng_a).unwrap();
3143 let actor_b: PcActor = PcActor::new(config.clone(), &mut rng_b).unwrap();
3144
3145 let caches_a = make_caches_for_actor(&actor_a, 100);
3146 let caches_b = make_caches_for_actor(&actor_b, 100);
3147 let cache_mats_a: Vec<_> = (0..2).map(|i| build_cache_matrix(&caches_a, i)).collect();
3148 let cache_mats_b: Vec<_> = (0..2).map(|i| build_cache_matrix(&caches_b, i)).collect();
3149
3150 let perm0 =
3152 crate::matrix::cca_neuron_alignment::<CpuLinAlg>(&cache_mats_a[0], &cache_mats_b[0])
3153 .unwrap();
3154 let is_nontrivial = perm0.iter().enumerate().any(|(i, &p)| i != p);
3155
3156 if !is_nontrivial {
3158 return;
3160 }
3161
3162 let mut rng_child = StdRng::seed_from_u64(99);
3164 let child: PcActor = PcActor::crossover(
3165 &actor_a,
3166 &actor_b,
3167 &cache_mats_a,
3168 &cache_mats_b,
3169 0.5,
3170 config.clone(),
3171 &mut rng_child,
3172 )
3173 .unwrap();
3174
3175 let b_layer1 = &actor_b.layers[1];
3188 let b_cols = CpuLinAlg::mat_cols(&b_layer1.weights);
3189
3190 let a_layer1 = &actor_a.layers[1];
3194 let child_layer1 = &child.layers[1];
3195 let n_rows = CpuLinAlg::mat_rows(&child_layer1.weights);
3196
3197 let mut has_col_permutation = false;
3198 for (c, &src_col) in perm0.iter().enumerate().take(b_cols.min(perm0.len())) {
3199 if src_col == c {
3200 continue; }
3202 for r in 0..n_rows {
3205 let a_val = CpuLinAlg::mat_get(&a_layer1.weights, r, c);
3206 let b_val_permuted = CpuLinAlg::mat_get(&b_layer1.weights, r, src_col);
3207 let b_val_unpermuted = CpuLinAlg::mat_get(&b_layer1.weights, r, c);
3208 let child_val = CpuLinAlg::mat_get(&child_layer1.weights, r, c);
3209
3210 let expected_permuted = 0.5 * a_val + 0.5 * b_val_permuted;
3211 let expected_unpermuted = 0.5 * a_val + 0.5 * b_val_unpermuted;
3212
3213 if (child_val - expected_permuted).abs() < 1e-10
3215 && (child_val - expected_unpermuted).abs() > 1e-10
3216 {
3217 has_col_permutation = true;
3218 }
3219 }
3220 }
3221
3222 assert!(
3223 has_col_permutation,
3224 "Layer 1 columns should be permuted to match layer 0's CCA \
3225 permutation of parent B. perm0={perm0:?}"
3226 );
3227 }
3228
3229 #[test]
3232 fn test_crossover_empty_hidden_layers_returns_error() {
3233 let mut rng_a = StdRng::seed_from_u64(42);
3234 let mut rng_b = StdRng::seed_from_u64(123);
3235 let config = crossover_config_27();
3236 let actor_a: PcActor = PcActor::new(config.clone(), &mut rng_a).unwrap();
3237 let actor_b: PcActor = PcActor::new(config, &mut rng_b).unwrap();
3238
3239 let caches_a = make_caches_for_actor(&actor_a, 50);
3240 let caches_b = make_caches_for_actor(&actor_b, 50);
3241 let cache_mats_a: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_a, i)).collect();
3242 let cache_mats_b: Vec<_> = (0..1).map(|i| build_cache_matrix(&caches_b, i)).collect();
3243
3244 let empty_config = PcActorConfig {
3246 hidden_layers: vec![],
3247 ..crossover_config_27()
3248 };
3249
3250 let mut rng_child = StdRng::seed_from_u64(99);
3251 let result = PcActor::crossover(
3252 &actor_a,
3253 &actor_b,
3254 &cache_mats_a,
3255 &cache_mats_b,
3256 0.5,
3257 empty_config,
3258 &mut rng_child,
3259 );
3260 assert!(
3261 result.is_err(),
3262 "Crossover with empty hidden_layers should return error"
3263 );
3264 }
3265
3266 fn valid_weights_for(config: &PcActorConfig) -> crate::serializer::PcActorWeights {
3271 let mut rng = make_rng();
3272 let actor = PcActor::<CpuLinAlg>::new(config.clone(), &mut rng).unwrap();
3273 actor.to_weights()
3274 }
3275
3276 #[test]
3277 fn test_from_weights_valid_returns_ok() {
3278 let config = default_config();
3279 let weights = valid_weights_for(&config);
3280 let result = PcActor::<CpuLinAlg>::from_weights(config, weights);
3281 assert!(result.is_ok());
3282 }
3283
3284 #[test]
3285 fn test_from_weights_wrong_weight_rows_returns_err() {
3286 let config = default_config(); let mut weights = valid_weights_for(&config);
3288 weights.layers[0].weights = crate::matrix::Matrix::zeros(10, 9);
3290 weights.layers[0].bias = vec![0.0; 10];
3291 let result = PcActor::<CpuLinAlg>::from_weights(config, weights);
3292 assert!(result.is_err());
3293 let err = result.unwrap_err();
3294 assert!(
3295 matches!(err, PcError::DimensionMismatch { .. }),
3296 "Expected DimensionMismatch, got: {err}"
3297 );
3298 }
3299
3300 #[test]
3301 fn test_from_weights_wrong_weight_cols_returns_err() {
3302 let config = default_config(); let mut weights = valid_weights_for(&config);
3304 weights.layers[0].weights = crate::matrix::Matrix::zeros(18, 5);
3306 let result = PcActor::<CpuLinAlg>::from_weights(config, weights);
3307 assert!(result.is_err());
3308 let err = result.unwrap_err();
3309 assert!(
3310 matches!(err, PcError::DimensionMismatch { .. }),
3311 "Expected DimensionMismatch, got: {err}"
3312 );
3313 }
3314
3315 #[test]
3316 fn test_from_weights_wrong_bias_length_returns_err() {
3317 let config = default_config(); let mut weights = valid_weights_for(&config);
3319 weights.layers[0].bias = vec![0.0; 5]; let result = PcActor::<CpuLinAlg>::from_weights(config, weights);
3321 assert!(result.is_err());
3322 let err = result.unwrap_err();
3323 assert!(
3324 matches!(err, PcError::DimensionMismatch { .. }),
3325 "Expected DimensionMismatch, got: {err}"
3326 );
3327 }
3328
3329 #[test]
3330 fn test_from_weights_wrong_output_layer_dims_returns_err() {
3331 let config = default_config(); let mut weights = valid_weights_for(&config);
3333 let last = weights.layers.len() - 1;
3334 weights.layers[last].weights = crate::matrix::Matrix::zeros(9, 10); let result = PcActor::<CpuLinAlg>::from_weights(config, weights);
3336 assert!(result.is_err());
3337 }
3338
3339 #[test]
3340 fn test_from_weights_wrong_rezero_alpha_count_returns_err() {
3341 let mut config = default_config();
3342 config.hidden_layers = vec![
3343 LayerDef {
3344 size: 18,
3345 activation: Activation::Tanh,
3346 },
3347 LayerDef {
3348 size: 18,
3349 activation: Activation::Tanh,
3350 },
3351 ];
3352 config.residual = true;
3353 let mut weights = valid_weights_for(&config);
3354 weights.rezero_alpha = vec![];
3356 let result = PcActor::<CpuLinAlg>::from_weights(config, weights);
3357 assert!(result.is_err());
3358 let err = result.unwrap_err();
3359 assert!(
3360 matches!(err, PcError::DimensionMismatch { .. }),
3361 "Expected DimensionMismatch, got: {err}"
3362 );
3363 }
3364
3365 #[test]
3366 fn test_from_weights_wrong_skip_projections_count_returns_err() {
3367 let mut config = default_config();
3368 config.hidden_layers = vec![
3369 LayerDef {
3370 size: 18,
3371 activation: Activation::Tanh,
3372 },
3373 LayerDef {
3374 size: 18,
3375 activation: Activation::Tanh,
3376 },
3377 ];
3378 config.residual = true;
3379 let mut weights = valid_weights_for(&config);
3380 weights.skip_projections = vec![None, None, None];
3382 let result = PcActor::<CpuLinAlg>::from_weights(config, weights);
3383 assert!(result.is_err());
3384 let err = result.unwrap_err();
3385 assert!(
3386 matches!(err, PcError::DimensionMismatch { .. }),
3387 "Expected DimensionMismatch, got: {err}"
3388 );
3389 }
3390}