Skip to main content

rlevo_evolution/
population.rs

1//! Population containers.
2//!
3//! [`Population<B, K>`] is a thin wrapper around a device tensor plus the
4//! shape metadata strategies need. For real-valued kinds it holds a
5//! `Tensor<B, 2>`; binary and integer kinds use `Tensor<B, 2, Int>`.
6//!
7//! The wrapper exists so operators and strategies have a single shape
8//! contract to validate against (they check `pop_size` and `genome_dim`
9//! rather than repeatedly interrogating `tensor.shape().dims`).
10
11use std::marker::PhantomData;
12
13use burn::tensor::{backend::Backend, Int, Tensor};
14
15use crate::genome::{Binary, Integer, Real};
16
17/// Population stored on a Burn backend device.
18///
19/// The concrete tensor type depends on the genome kind `K`. Most
20/// consumers interact with [`Population<B, Real>`] via [`tensor`](Population::tensor),
21/// but strategies parameterized on the kind can keep the `K` generic and
22/// reach for the right tensor flavor through the inherent impls below.
23///
24/// Invariant: for every `Population<B, K>` produced by the public
25/// constructors, exactly one of `tensor_real` / `tensor_int` is `Some`,
26/// determined by `K`. `Real` populates `tensor_real`; `Binary`,
27/// `Integer`, and `Permutation` populate `tensor_int`. The inherent
28/// `tensor(&self)` accessors `.expect()` on the matching field because
29/// the constructor contract pins the invariant — a mismatch would be a
30/// bug in this module.
31#[derive(Debug, Clone)]
32pub struct Population<B: Backend, K> {
33    pop_size: usize,
34    genome_dim: usize,
35    _kind: PhantomData<K>,
36    tensor_real: Option<Tensor<B, 2>>,
37    tensor_int: Option<Tensor<B, 2, Int>>,
38}
39
40impl<B: Backend, K> Population<B, K> {
41    /// Returns the number of individuals (rows) in the population.
42    #[must_use]
43    pub fn pop_size(&self) -> usize {
44        self.pop_size
45    }
46
47    /// Returns the genome dimensionality (columns).
48    #[must_use]
49    pub fn genome_dim(&self) -> usize {
50        self.genome_dim
51    }
52}
53
54impl<B: Backend> Population<B, Real> {
55    /// Constructs a real-valued population from a `Tensor<B, 2>`.
56    ///
57    /// # Panics
58    ///
59    /// Panics if the tensor is not rank 2.
60    #[must_use]
61    pub fn new_real(tensor: Tensor<B, 2>) -> Self {
62        let dims = tensor.shape().dims;
63        assert_eq!(dims.len(), 2, "population tensor must be rank 2");
64        Self {
65            pop_size: dims[0],
66            genome_dim: dims[1],
67            _kind: PhantomData,
68            tensor_real: Some(tensor),
69            tensor_int: None,
70        }
71    }
72
73    /// Borrows the backing real-valued tensor.
74    ///
75    /// # Panics
76    ///
77    /// Never panics for a correctly constructed `Population<B, Real>`.
78    #[must_use]
79    pub fn tensor(&self) -> &Tensor<B, 2> {
80        self.tensor_real
81            .as_ref()
82            .expect("real population always has a tensor_real")
83    }
84
85    /// Consumes the wrapper and returns the owned tensor.
86    ///
87    /// # Panics
88    ///
89    /// Never panics for a correctly constructed `Population<B, Real>`.
90    #[must_use]
91    pub fn into_tensor(self) -> Tensor<B, 2> {
92        self.tensor_real
93            .expect("real population always has a tensor_real")
94    }
95}
96
97impl<B: Backend> Population<B, Binary> {
98    /// Constructs a binary population from a `Tensor<B, 2, Int>`.
99    ///
100    /// # Panics
101    ///
102    /// Panics if the tensor is not rank 2.
103    #[must_use]
104    pub fn new_binary(tensor: Tensor<B, 2, Int>) -> Self {
105        let dims = tensor.shape().dims;
106        assert_eq!(dims.len(), 2, "population tensor must be rank 2");
107        Self {
108            pop_size: dims[0],
109            genome_dim: dims[1],
110            _kind: PhantomData,
111            tensor_real: None,
112            tensor_int: Some(tensor),
113        }
114    }
115
116    /// Borrows the backing integer tensor holding 0/1 values.
117    ///
118    /// # Panics
119    ///
120    /// Never panics for a correctly constructed `Population<B, Binary>`.
121    #[must_use]
122    pub fn tensor(&self) -> &Tensor<B, 2, Int> {
123        self.tensor_int
124            .as_ref()
125            .expect("binary population always has a tensor_int")
126    }
127}
128
129impl<B: Backend> Population<B, Integer> {
130    /// Constructs an integer population from a `Tensor<B, 2, Int>`.
131    ///
132    /// # Panics
133    ///
134    /// Panics if the tensor is not rank 2.
135    #[must_use]
136    pub fn new_integer(tensor: Tensor<B, 2, Int>) -> Self {
137        let dims = tensor.shape().dims;
138        assert_eq!(dims.len(), 2, "population tensor must be rank 2");
139        Self {
140            pop_size: dims[0],
141            genome_dim: dims[1],
142            _kind: PhantomData,
143            tensor_real: None,
144            tensor_int: Some(tensor),
145        }
146    }
147
148    /// Borrows the backing integer tensor.
149    ///
150    /// # Panics
151    ///
152    /// Never panics for a correctly constructed `Population<B, Integer>`.
153    #[must_use]
154    pub fn tensor(&self) -> &Tensor<B, 2, Int> {
155        self.tensor_int
156            .as_ref()
157            .expect("integer population always has a tensor_int")
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use burn::backend::NdArray;
165    use burn::tensor::TensorData;
166    type TestBackend = NdArray;
167
168    #[test]
169    fn real_population_reports_shape() {
170        let device = Default::default();
171        let data = TensorData::new(vec![1.0f32, 2.0, 3.0, 4.0], [2, 2]);
172        let tensor = Tensor::<TestBackend, 2>::from_data(data, &device);
173        let pop = Population::<TestBackend, Real>::new_real(tensor);
174        assert_eq!(pop.pop_size(), 2);
175        assert_eq!(pop.genome_dim(), 2);
176        assert_eq!(pop.tensor().shape().dims, vec![2, 2]);
177    }
178
179    #[test]
180    fn binary_population_uses_int_tensor() {
181        let device = Default::default();
182        let data = TensorData::new(vec![0i64, 1, 1, 0, 1, 0], [2, 3]);
183        let tensor = Tensor::<TestBackend, 2, Int>::from_data(data, &device);
184        let pop = Population::<TestBackend, Binary>::new_binary(tensor);
185        assert_eq!(pop.pop_size(), 2);
186        assert_eq!(pop.genome_dim(), 3);
187    }
188}