1use std::collections::{BTreeMap, HashMap, VecDeque};
18use std::marker::PhantomData;
19
20use burn::tensor::backend::Backend;
21use burn::tensor::{Bool, Tensor, TensorData, activation};
22
23use super::topology::{ActivationFn, NodeGene, NodeId, NodeKind, SIGMOID_GAIN, TopologyGenome};
24
25pub trait PhenotypeBuilder<B: Backend> {
31 fn build(
34 &self,
35 genome: &TopologyGenome,
36 device: &<B as burn::tensor::backend::BackendTypes>::Device,
37 ) -> Box<dyn Phenotype<B>>;
38}
39
40pub trait Phenotype<B: Backend>: Send + Sync {
43 fn forward(&self, input: Tensor<B, 2>) -> Tensor<B, 2>;
46}
47
48#[derive(Debug, Clone, Copy, Default)]
50pub struct InterpretedBuilder;
51
52impl<B: Backend> PhenotypeBuilder<B> for InterpretedBuilder {
53 fn build(
54 &self,
55 genome: &TopologyGenome,
56 _device: &<B as burn::tensor::backend::BackendTypes>::Device,
57 ) -> Box<dyn Phenotype<B>> {
58 Box::new(InterpretedPhenotype::<B>::new(genome))
59 }
60}
61
62#[derive(Debug, Clone)]
64struct NodeEval {
65 id: NodeId,
66 incoming: Vec<(NodeId, f32)>,
68 bias: f32,
69 activation: ActivationFn,
70}
71
72#[derive(Debug, Clone)]
79pub struct InterpretedPhenotype<B: Backend> {
80 input_ids: Vec<NodeId>,
82 output_ids: Vec<NodeId>,
84 eval_order: Vec<NodeEval>,
86 _backend: PhantomData<fn() -> B>,
87}
88
89impl<B: Backend> InterpretedPhenotype<B> {
90 #[must_use]
96 pub fn new(genome: &TopologyGenome) -> Self {
97 let mut input_ids: Vec<NodeId> = filter_ids(genome, |k| matches!(k, NodeKind::Input));
98 input_ids.sort_unstable();
99
100 let mut output_ids: Vec<NodeId> = filter_ids(genome, |k| matches!(k, NodeKind::Output));
101 output_ids.sort_unstable();
102
103 let nodes_by_id: HashMap<NodeId, &NodeGene> =
107 genome.nodes.iter().map(|n| (n.id, n)).collect();
108 let mut incoming_by_target: HashMap<NodeId, Vec<(NodeId, f32)>> = HashMap::new();
109 for c in genome.connections.iter().filter(|c| c.enabled) {
110 incoming_by_target
111 .entry(c.target)
112 .or_default()
113 .push((c.source, c.weight));
114 }
115
116 let order = topological_order(genome);
117 let mut eval_order: Vec<NodeEval> = Vec::with_capacity(order.len());
118 for nid in order {
119 let Some(&node) = nodes_by_id.get(&nid) else {
120 continue;
121 };
122 if matches!(node.kind, NodeKind::Input) {
123 continue;
124 }
125 let incoming: Vec<(NodeId, f32)> = incoming_by_target.remove(&nid).unwrap_or_default();
126 eval_order.push(NodeEval {
127 id: nid,
128 incoming,
129 bias: node.bias,
130 activation: node.activation,
131 });
132 }
133
134 Self {
135 input_ids,
136 output_ids,
137 eval_order,
138 _backend: PhantomData,
139 }
140 }
141}
142
143impl<B: Backend> Phenotype<B> for InterpretedPhenotype<B> {
144 fn forward(&self, input: Tensor<B, 2>) -> Tensor<B, 2> {
145 let [batch, _num_inputs] = input.dims();
146 let device = input.device();
147
148 let mut values: HashMap<NodeId, Tensor<B, 2>> =
149 HashMap::with_capacity(self.input_ids.len());
150 if !self.input_ids.is_empty() {
151 let columns: Vec<Tensor<B, 2>> = input.chunk(self.input_ids.len(), 1);
154 for (iid, column) in self.input_ids.iter().copied().zip(columns) {
155 values.insert(iid, column);
156 }
157 }
158
159 for node in &self.eval_order {
160 let mut acc = Tensor::<B, 2>::zeros([batch, 1], &device);
161 for (src, weight) in &node.incoming {
162 if let Some(src_value) = values.get(src) {
163 acc = acc + src_value.clone().mul_scalar(*weight);
164 }
165 }
166 acc = acc.add_scalar(node.bias);
167 values.insert(node.id, apply_activation::<B>(node.activation, acc));
168 }
169
170 let columns: Vec<Tensor<B, 2>> = self
171 .output_ids
172 .iter()
173 .map(|oid| {
174 values
175 .get(oid)
176 .cloned()
177 .unwrap_or_else(|| Tensor::<B, 2>::zeros([batch, 1], &device))
178 })
179 .collect();
180 Tensor::cat(columns, 1)
181 }
182}
183
184fn apply_activation<B: Backend>(act: ActivationFn, x: Tensor<B, 2>) -> Tensor<B, 2> {
187 match act {
188 ActivationFn::Sigmoid => activation::sigmoid(x.mul_scalar(SIGMOID_GAIN)),
189 ActivationFn::Tanh => activation::tanh(x),
190 ActivationFn::Relu => activation::relu(x),
191 ActivationFn::Linear => x,
192 }
193}
194
195pub trait BatchPhenotypeEvaluator<B: Backend>: Send + Sync {
208 fn evaluate_population(
217 &self,
218 genomes: &[TopologyGenome],
219 obs: Tensor<B, 2>,
220 device: &<B as burn::tensor::backend::BackendTypes>::Device,
221 ) -> Tensor<B, 3>;
222}
223
224#[derive(Debug, Clone, Copy)]
242pub struct DensePaddedEvaluator {
243 pub max_nodes_cap: usize,
246}
247
248impl DensePaddedEvaluator {
249 #[must_use]
251 pub fn new(max_nodes_cap: usize) -> Self {
252 Self { max_nodes_cap }
253 }
254}
255
256impl Default for DensePaddedEvaluator {
257 fn default() -> Self {
260 Self { max_nodes_cap: 512 }
261 }
262}
263
264impl<B: Backend> BatchPhenotypeEvaluator<B> for DensePaddedEvaluator {
265 fn evaluate_population(
266 &self,
267 genomes: &[TopologyGenome],
268 obs: Tensor<B, 2>,
269 device: &<B as burn::tensor::backend::BackendTypes>::Device,
270 ) -> Tensor<B, 3> {
271 let pop = genomes.len();
272 let [batch, obs_dim] = obs.dims();
273 if pop == 0 {
274 return Tensor::<B, 3>::zeros([0, batch, 0], device);
275 }
276 let max_nodes = genomes.iter().map(|g| g.nodes.len()).max().unwrap_or(0);
277 assert!(
278 max_nodes <= self.max_nodes_cap,
279 "largest genome has {max_nodes} nodes, exceeding max_nodes_cap {}",
280 self.max_nodes_cap
281 );
282
283 let PaddedPopulation {
284 weights,
285 bias,
286 act_masks,
287 input_slots,
288 num_inputs,
289 num_outputs,
290 n,
291 iterations,
292 } = PaddedPopulation::<B>::compile(genomes, device);
293 assert_eq!(
294 obs_dim, num_inputs,
295 "obs feature dim {obs_dim} must equal the population's input-node count {num_inputs}"
296 );
297
298 let obs_t = obs.swap_dims(0, 1); let seed_2d = if n > num_inputs {
302 let pad = Tensor::<B, 2>::zeros([n - num_inputs, batch], device);
303 Tensor::cat(vec![obs_t, pad], 0) } else {
305 obs_t
306 };
307 let seeded = seed_2d.unsqueeze_dim::<3>(0).repeat_dim(0, pop); let bias = bias.unsqueeze_dim::<3>(2); let input_slots = input_slots.unsqueeze_dim::<3>(2).repeat_dim(2, batch);
312 let [mask_sigmoid, mask_tanh, mask_relu, mask_linear] = act_masks;
313 let mask_sigmoid = mask_sigmoid.unsqueeze_dim::<3>(2).repeat_dim(2, batch);
314 let mask_tanh = mask_tanh.unsqueeze_dim::<3>(2).repeat_dim(2, batch);
315 let mask_relu = mask_relu.unsqueeze_dim::<3>(2).repeat_dim(2, batch);
316 let mask_linear = mask_linear.unsqueeze_dim::<3>(2).repeat_dim(2, batch);
317
318 let mut values = seeded.clone(); for _ in 0..iterations {
320 let acc = weights.clone().matmul(values.clone()) + bias.clone(); let mut out = Tensor::<B, 3>::zeros([pop, n, batch], device);
324 out = out.mask_where(
325 mask_sigmoid.clone(),
326 activation::sigmoid(acc.clone().mul_scalar(SIGMOID_GAIN)),
327 );
328 out = out.mask_where(mask_tanh.clone(), activation::tanh(acc.clone()));
329 out = out.mask_where(mask_relu.clone(), activation::relu(acc.clone()));
330 out = out.mask_where(mask_linear.clone(), acc);
331 values = out.mask_where(input_slots.clone(), seeded.clone());
333 }
334
335 let result = values.slice([0..pop, num_inputs..num_inputs + num_outputs, 0..batch]);
337 result.swap_dims(1, 2) }
339}
340
341struct PaddedPopulation<B: Backend> {
351 weights: Tensor<B, 3>,
353 bias: Tensor<B, 2>,
355 act_masks: [Tensor<B, 2, Bool>; 4],
358 input_slots: Tensor<B, 2, Bool>,
360 num_inputs: usize,
361 num_outputs: usize,
362 n: usize,
363 iterations: usize,
371}
372
373impl<B: Backend> PaddedPopulation<B> {
374 fn compile(
375 genomes: &[TopologyGenome],
376 device: &<B as burn::tensor::backend::BackendTypes>::Device,
377 ) -> Self {
378 let pop = genomes.len();
379 let num_inputs = count_kind(&genomes[0], NodeKind::Input);
380 let num_outputs = count_kind(&genomes[0], NodeKind::Output);
381 let n = genomes.iter().map(|g| g.nodes.len()).max().unwrap_or(0);
382 let iterations = genomes
385 .iter()
386 .map(longest_path_edges)
387 .max()
388 .unwrap_or(0)
389 .max(1);
390
391 let mut weights = vec![0.0f32; pop * n * n];
392 let mut bias = vec![0.0f32; pop * n];
393 let mut masks: [Vec<f32>; 4] = [
394 vec![0.0f32; pop * n],
395 vec![0.0f32; pop * n],
396 vec![0.0f32; pop * n],
397 vec![0.0f32; pop * n],
398 ];
399 let mut input_slots = vec![0.0f32; pop * n];
400
401 for (p, genome) in genomes.iter().enumerate() {
402 debug_assert_eq!(
403 count_kind(genome, NodeKind::Input),
404 num_inputs,
405 "every genome must share the population input-node count"
406 );
407 debug_assert_eq!(
408 count_kind(genome, NodeKind::Output),
409 num_outputs,
410 "every genome must share the population output-node count"
411 );
412 let local = local_rows(genome);
413 for node in &genome.nodes {
414 let base = p * n + local[&node.id];
415 if matches!(node.kind, NodeKind::Input) {
416 input_slots[base] = 1.0;
417 } else {
418 bias[base] = node.bias;
419 masks[act_index(node.activation)][base] = 1.0;
420 }
421 }
422 let wbase = p * n * n;
423 for conn in &genome.connections {
424 if !conn.enabled {
425 continue;
426 }
427 let i = local[&conn.target];
428 let j = local[&conn.source];
429 weights[wbase + i * n + j] = conn.weight;
430 }
431 }
432
433 let weights = Tensor::<B, 3>::from_data(TensorData::new(weights, [pop, n, n]), device);
434 let bias = Tensor::<B, 2>::from_data(TensorData::new(bias, [pop, n]), device);
435 let act_masks = masks.map(|m| {
436 Tensor::<B, 2>::from_data(TensorData::new(m, [pop, n]), device).greater_elem(0.5)
437 });
438 let input_slots = Tensor::<B, 2>::from_data(TensorData::new(input_slots, [pop, n]), device)
439 .greater_elem(0.5);
440
441 Self {
442 weights,
443 bias,
444 act_masks,
445 input_slots,
446 num_inputs,
447 num_outputs,
448 n,
449 iterations,
450 }
451 }
452}
453
454fn count_kind(genome: &TopologyGenome, kind: NodeKind) -> usize {
456 genome.nodes.iter().filter(|n| n.kind == kind).count()
457}
458
459fn longest_path_edges(genome: &TopologyGenome) -> usize {
468 let mut out_edges: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
469 for conn in &genome.connections {
470 if conn.enabled {
471 out_edges.entry(conn.source).or_default().push(conn.target);
472 }
473 }
474 let mut depth: HashMap<NodeId, usize> = genome.nodes.iter().map(|n| (n.id, 0usize)).collect();
475 for node in topological_order(genome) {
476 let here = depth.get(&node).copied().unwrap_or(0);
477 if let Some(targets) = out_edges.get(&node) {
478 for &t in targets {
479 let slot = depth.entry(t).or_insert(0);
480 *slot = (*slot).max(here + 1);
481 }
482 }
483 }
484 depth.into_values().max().unwrap_or(0)
485}
486
487fn act_index(act: ActivationFn) -> usize {
489 match act {
490 ActivationFn::Sigmoid => 0,
491 ActivationFn::Tanh => 1,
492 ActivationFn::Relu => 2,
493 ActivationFn::Linear => 3,
494 }
495}
496
497fn local_rows(genome: &TopologyGenome) -> HashMap<NodeId, usize> {
501 let mut inputs: Vec<NodeId> = filter_ids(genome, |k| matches!(k, NodeKind::Input));
502 let mut outputs: Vec<NodeId> = filter_ids(genome, |k| matches!(k, NodeKind::Output));
503 let mut others: Vec<NodeId> =
504 filter_ids(genome, |k| !matches!(k, NodeKind::Input | NodeKind::Output));
505 inputs.sort_unstable();
506 outputs.sort_unstable();
507 others.sort_unstable();
508
509 let mut map: HashMap<NodeId, usize> = HashMap::with_capacity(genome.nodes.len());
510 for (row, id) in inputs.into_iter().chain(outputs).chain(others).enumerate() {
511 map.insert(id, row);
512 }
513 map
514}
515
516fn filter_ids(genome: &TopologyGenome, pred: impl Fn(NodeKind) -> bool) -> Vec<NodeId> {
518 genome
519 .nodes
520 .iter()
521 .filter(|n| pred(n.kind))
522 .map(|n| n.id)
523 .collect()
524}
525
526fn topological_order(genome: &TopologyGenome) -> Vec<NodeId> {
531 let mut in_degree: BTreeMap<NodeId, usize> =
532 genome.nodes.iter().map(|n| (n.id, 0usize)).collect();
533 let mut adj: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
534 for c in &genome.connections {
535 if let Some(d) = in_degree.get_mut(&c.target) {
536 *d += 1;
537 }
538 adj.entry(c.source).or_default().push(c.target);
539 }
540
541 let mut queue: VecDeque<NodeId> = in_degree
543 .iter()
544 .filter(|&(_, &d)| d == 0)
545 .map(|(&id, _)| id)
546 .collect();
547
548 let mut order: Vec<NodeId> = Vec::with_capacity(genome.nodes.len());
549 while let Some(n) = queue.pop_front() {
550 order.push(n);
551 if let Some(succ) = adj.get(&n) {
552 let mut targets = succ.clone();
553 targets.sort_unstable();
554 for t in targets {
555 if let Some(d) = in_degree.get_mut(&t) {
556 *d -= 1;
557 if *d == 0 {
558 queue.push_back(t);
559 }
560 }
561 }
562 }
563 }
564
565 if order.len() < genome.nodes.len() {
566 for n in &genome.nodes {
567 if !order.contains(&n.id) {
568 order.push(n.id);
569 }
570 }
571 }
572 order
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use crate::neuroevolution::topology::{ConnectionGene, InnovationId};
579 use burn::backend::Flex;
580
581 type TestBackend = Flex;
582
583 #[test]
589 fn test_interpreted_phenotype_reproduces_truth_table() {
590 let device = Default::default();
591 let nodes = vec![
592 NodeGene {
593 id: NodeId::new(0),
594 kind: NodeKind::Input,
595 activation: ActivationFn::Linear,
596 bias: 0.0,
597 },
598 NodeGene {
599 id: NodeId::new(1),
600 kind: NodeKind::Input,
601 activation: ActivationFn::Linear,
602 bias: 0.0,
603 },
604 NodeGene {
605 id: NodeId::new(2),
606 kind: NodeKind::Hidden,
607 activation: ActivationFn::Relu,
608 bias: 0.0,
609 },
610 NodeGene {
611 id: NodeId::new(3),
612 kind: NodeKind::Output,
613 activation: ActivationFn::Linear,
614 bias: 0.5,
615 },
616 ];
617 let conns = vec![
618 ConnectionGene {
619 innovation: InnovationId::new(0),
620 source: NodeId::new(0),
621 target: NodeId::new(2),
622 weight: 1.0,
623 enabled: true,
624 },
625 ConnectionGene {
626 innovation: InnovationId::new(1),
627 source: NodeId::new(1),
628 target: NodeId::new(2),
629 weight: 1.0,
630 enabled: true,
631 },
632 ConnectionGene {
633 innovation: InnovationId::new(2),
634 source: NodeId::new(2),
635 target: NodeId::new(3),
636 weight: 2.0,
637 enabled: true,
638 },
639 ];
640 let genome = TopologyGenome::new(nodes, conns);
641
642 let builder = InterpretedBuilder;
643 let pheno = PhenotypeBuilder::<TestBackend>::build(&builder, &genome, &device);
644
645 let input = Tensor::<TestBackend, 2>::from_data(
646 burn::tensor::TensorData::new(vec![0.0f32, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0], [4, 2]),
647 &device,
648 );
649 let out = pheno
650 .forward(input)
651 .into_data()
652 .into_vec::<f32>()
653 .expect("output host-read of a tensor this test just built");
654 let expected = [0.5f32, 2.5, 2.5, 4.5];
655 for (got, want) in out.iter().zip(expected.iter()) {
656 approx::assert_relative_eq!(*got, *want, epsilon = 1e-5);
657 }
658 }
659
660 #[test]
662 fn test_interpreted_phenotype_skips_disabled_edges() {
663 let device = Default::default();
664 let nodes = vec![
665 NodeGene {
666 id: NodeId::new(0),
667 kind: NodeKind::Input,
668 activation: ActivationFn::Linear,
669 bias: 0.0,
670 },
671 NodeGene {
672 id: NodeId::new(1),
673 kind: NodeKind::Output,
674 activation: ActivationFn::Linear,
675 bias: 1.0,
676 },
677 ];
678 let conns = vec![ConnectionGene {
680 innovation: InnovationId::new(0),
681 source: NodeId::new(0),
682 target: NodeId::new(1),
683 weight: 99.0,
684 enabled: false,
685 }];
686 let genome = TopologyGenome::new(nodes, conns);
687 let pheno = InterpretedPhenotype::<TestBackend>::new(&genome);
688 let input = Tensor::<TestBackend, 2>::from_data(
689 burn::tensor::TensorData::new(vec![5.0f32, 7.0], [2, 1]),
690 &device,
691 );
692 let out = pheno
693 .forward(input)
694 .into_data()
695 .into_vec::<f32>()
696 .expect("output host-read of a tensor this test just built");
697 approx::assert_relative_eq!(out[0], 1.0, epsilon = 1e-6);
698 approx::assert_relative_eq!(out[1], 1.0, epsilon = 1e-6);
699 }
700
701 #[test]
709 fn test_interpreted_phenotype_forward_handles_zero_inputs() {
710 let device = Default::default();
711 let nodes = vec![NodeGene {
713 id: NodeId::new(0),
714 kind: NodeKind::Output,
715 activation: ActivationFn::Linear,
716 bias: 0.75,
717 }];
718 let conns: Vec<ConnectionGene> = vec![];
719 let genome = TopologyGenome::new(nodes, conns);
720 let pheno = InterpretedPhenotype::<TestBackend>::new(&genome);
721
722 let batch: usize = 3;
724 let input = Tensor::<TestBackend, 2>::from_data(
725 burn::tensor::TensorData::new(Vec::<f32>::new(), [batch, 0]),
726 &device,
727 );
728 let out = pheno.forward(input);
729 assert_eq!(
730 out.dims(),
731 [batch, 1],
732 "zero-input forward must return [batch, num_outputs]"
733 );
734 let out_vec = out
735 .into_data()
736 .into_vec::<f32>()
737 .expect("output host-read of a tensor this test just built");
738 for got in &out_vec {
739 approx::assert_relative_eq!(*got, 0.75f32, epsilon = 1e-6);
740 }
741 }
742
743 #[test]
752 #[allow(clippy::too_many_lines)] fn test_dense_matches_interpreted_within_epsilon() {
754 let device = Default::default();
755
756 let genome_a: TopologyGenome = TopologyGenome::new(
758 vec![
759 NodeGene {
760 id: NodeId::new(0),
761 kind: NodeKind::Input,
762 activation: ActivationFn::Linear,
763 bias: 0.0,
764 },
765 NodeGene {
766 id: NodeId::new(1),
767 kind: NodeKind::Input,
768 activation: ActivationFn::Linear,
769 bias: 0.0,
770 },
771 NodeGene {
772 id: NodeId::new(2),
773 kind: NodeKind::Hidden,
774 activation: ActivationFn::Relu,
775 bias: 0.0,
776 },
777 NodeGene {
778 id: NodeId::new(3),
779 kind: NodeKind::Output,
780 activation: ActivationFn::Linear,
781 bias: 0.5,
782 },
783 ],
784 vec![
785 ConnectionGene {
786 innovation: InnovationId::new(0),
787 source: NodeId::new(0),
788 target: NodeId::new(2),
789 weight: 1.0,
790 enabled: true,
791 },
792 ConnectionGene {
793 innovation: InnovationId::new(1),
794 source: NodeId::new(1),
795 target: NodeId::new(2),
796 weight: 1.0,
797 enabled: true,
798 },
799 ConnectionGene {
800 innovation: InnovationId::new(2),
801 source: NodeId::new(2),
802 target: NodeId::new(3),
803 weight: 2.0,
804 enabled: true,
805 },
806 ],
807 );
808
809 let genome_b: TopologyGenome = TopologyGenome::new(
811 vec![
812 NodeGene {
813 id: NodeId::new(0),
814 kind: NodeKind::Input,
815 activation: ActivationFn::Linear,
816 bias: 0.0,
817 },
818 NodeGene {
819 id: NodeId::new(1),
820 kind: NodeKind::Input,
821 activation: ActivationFn::Linear,
822 bias: 0.0,
823 },
824 NodeGene {
825 id: NodeId::new(2),
826 kind: NodeKind::Output,
827 activation: ActivationFn::Sigmoid,
828 bias: 0.1,
829 },
830 ],
831 vec![
832 ConnectionGene {
833 innovation: InnovationId::new(0),
834 source: NodeId::new(0),
835 target: NodeId::new(2),
836 weight: 0.5,
837 enabled: true,
838 },
839 ConnectionGene {
840 innovation: InnovationId::new(1),
841 source: NodeId::new(1),
842 target: NodeId::new(2),
843 weight: -0.3,
844 enabled: true,
845 },
846 ],
847 );
848
849 let genome_c: TopologyGenome = TopologyGenome::new(
852 vec![
853 NodeGene {
854 id: NodeId::new(0),
855 kind: NodeKind::Input,
856 activation: ActivationFn::Linear,
857 bias: 0.0,
858 },
859 NodeGene {
860 id: NodeId::new(1),
861 kind: NodeKind::Input,
862 activation: ActivationFn::Linear,
863 bias: 0.0,
864 },
865 NodeGene {
866 id: NodeId::new(2),
867 kind: NodeKind::Hidden,
868 activation: ActivationFn::Tanh,
869 bias: 0.2,
870 },
871 NodeGene {
872 id: NodeId::new(3),
873 kind: NodeKind::Hidden,
874 activation: ActivationFn::Relu,
875 bias: -0.1,
876 },
877 NodeGene {
878 id: NodeId::new(4),
879 kind: NodeKind::Output,
880 activation: ActivationFn::Tanh,
881 bias: 0.0,
882 },
883 ],
884 vec![
885 ConnectionGene {
886 innovation: InnovationId::new(0),
887 source: NodeId::new(0),
888 target: NodeId::new(2),
889 weight: 0.7,
890 enabled: true,
891 },
892 ConnectionGene {
893 innovation: InnovationId::new(1),
894 source: NodeId::new(1),
895 target: NodeId::new(2),
896 weight: -0.5,
897 enabled: true,
898 },
899 ConnectionGene {
900 innovation: InnovationId::new(2),
901 source: NodeId::new(0),
902 target: NodeId::new(3),
903 weight: 0.4,
904 enabled: true,
905 },
906 ConnectionGene {
907 innovation: InnovationId::new(3),
908 source: NodeId::new(1),
909 target: NodeId::new(3),
910 weight: 0.9,
911 enabled: true,
912 },
913 ConnectionGene {
914 innovation: InnovationId::new(4),
915 source: NodeId::new(2),
916 target: NodeId::new(4),
917 weight: 1.2,
918 enabled: true,
919 },
920 ConnectionGene {
921 innovation: InnovationId::new(5),
922 source: NodeId::new(3),
923 target: NodeId::new(4),
924 weight: -0.8,
925 enabled: true,
926 },
927 ],
928 );
929
930 let genomes: Vec<TopologyGenome> = vec![genome_a, genome_b, genome_c];
931 let pop: usize = genomes.len();
932 let out_dim: usize = 1; let batch: usize = 4;
936 let obs_data: Vec<f32> = vec![0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.5, -0.5];
937 let obs: Tensor<TestBackend, 2> = Tensor::<TestBackend, 2>::from_data(
938 burn::tensor::TensorData::new(obs_data, [batch, 2]),
939 &device,
940 );
941
942 let dense: Tensor<TestBackend, 3> =
943 DensePaddedEvaluator::default().evaluate_population(&genomes, obs.clone(), &device);
944 assert_eq!(
945 dense.dims(),
946 [pop, batch, out_dim],
947 "dense evaluator must return a [pop, batch, action_dim] tensor"
948 );
949 let dense_vec: Vec<f32> = dense
950 .into_data()
951 .into_vec::<f32>()
952 .expect("host-read of a tensor this test just built");
953
954 for (p, genome) in genomes.iter().enumerate() {
955 let interp: Vec<f32> = InterpretedPhenotype::<TestBackend>::new(genome)
956 .forward(obs.clone())
957 .into_data()
958 .into_vec::<f32>()
959 .expect("host-read of a tensor this test just built");
960 for b in 0..batch {
961 for o in 0..out_dim {
962 let dense_val: f32 = dense_vec[p * batch * out_dim + b * out_dim + o];
963 let interp_val: f32 = interp[b * out_dim + o];
964 approx::assert_relative_eq!(
965 dense_val,
966 interp_val,
967 epsilon = 1e-4,
968 max_relative = 1e-3
969 );
970 }
971 }
972 }
973 }
974}