rlevo_evolution/genome.rs
1//! Genome category trait and its zero-sized marker types.
2//!
3//! [`GenomeKind`] tags genome representations at the type level so operators
4//! can specialize on the element semantics (real-valued, binary, integer,
5//! or tree). Strategies take a marker type as a const generic to pick the
6//! right operator set.
7//!
8//! The markers themselves carry no data — they exist purely to discriminate
9//! trait impls.
10
11use std::fmt::Debug;
12
13use burn::tensor::{Int, Tensor, backend::Backend};
14
15/// Shape-erased genome kind.
16///
17/// `GenomeKind` is a zero-sized marker that strategies parameterize on to
18/// pick operators. Concrete kinds (`Real`, `Binary`, `Integer`, `Tree`,
19/// `Permutation`) live below; new kinds can be added by implementing this
20/// trait on a fresh marker type.
21///
22/// Genome width is a runtime property (`Population::genome_dim`), not a
23/// type-level one: every shipping kind is either runtime-dimensioned
24/// (`Real`/`Binary`/`Integer`) or variable-length (`Tree`/`Permutation`). A
25/// structurally-fixed-width kind could add a compile-time length constant when
26/// one is introduced — an associated const with a default is a non-breaking
27/// addition.
28pub trait GenomeKind: Debug + Copy + Send + Sync + 'static {
29 /// Element type of the genome (typically `f32`, `i32`, or `bool`).
30 type Element: Copy + Debug + Send + Sync + 'static;
31}
32
33/// Real-valued genome (each gene is an `f32`).
34///
35/// Populations are stored as `Tensor<B, 2>` of shape `(pop_size, dim)`.
36/// All classical ES variants, real-coded GA, EP, and DE use this kind.
37#[derive(Debug, Clone, Copy, Default)]
38pub struct Real;
39
40impl GenomeKind for Real {
41 type Element = f32;
42}
43
44/// Binary genome (each gene is a bit, stored as `i32` 0/1 on device).
45///
46/// Populations are stored as `Tensor<B, 2, Int>` of shape
47/// `(pop_size, dim)`. Binary-coded GA uses this kind.
48#[derive(Debug, Clone, Copy, Default)]
49pub struct Binary;
50
51impl GenomeKind for Binary {
52 type Element = i32;
53}
54
55/// Integer-valued genome (each gene is a non-negative integer index).
56///
57/// Populations are stored as `Tensor<B, 2, Int>` of shape
58/// `(pop_size, dim)`. Cartesian GP (node indices), discrete parameter
59/// search, and other problems where genes are bounded integer values use
60/// this kind. For ordered-sequence problems (TSP, QAP) use [`Permutation`]
61/// instead.
62#[derive(Debug, Clone, Copy, Default)]
63pub struct Integer;
64
65/// Tree-based genome (variable-length AST, stored host-side).
66///
67/// Reserved for classical Koza-style GP in a future release. Tree
68/// genomes cannot be batched on a GPU and therefore have no tensor
69/// representation in this crate. The associated `Element` type is `i32`
70/// as a placeholder (structural node IDs); the actual in-memory
71/// representation will be defined when this kind is fully implemented.
72#[derive(Debug, Clone, Copy, Default)]
73pub struct Tree;
74
75/// Permutation genome (each row is a permutation of `0..n_nodes`).
76///
77/// Rectangular and device-resident: populations are stored as
78/// `Tensor<B, 2, Int>` of shape `(pop_size, n_nodes)`, so this kind
79/// implements [`TensorGenome`] and `Population<B, Permutation>` type-checks.
80/// The permutation invariant (every row is a bijection of `0..n_nodes`) is
81/// **not** enforced at construction — [`new_permutation`] validates only
82/// shape, matching how `Binary`/`Integer` leave element values unchecked.
83///
84/// Used by Ant Colony Optimization over combinatorial domains (TSP, QAP, …);
85/// only a stubbed consumer ships in this release — the operators are planned
86/// for a future release.
87///
88/// [`new_permutation`]: crate::population::Population::new_permutation
89#[derive(Debug, Clone, Copy, Default)]
90pub struct Permutation;
91
92impl GenomeKind for Integer {
93 type Element = i32;
94}
95
96impl GenomeKind for Tree {
97 type Element = i32;
98}
99
100impl GenomeKind for Permutation {
101 type Element = i32;
102}
103
104/// Genome kinds with a rectangular, device-resident tensor representation.
105///
106/// This is the subset of [`GenomeKind`]s that a
107/// [`Population`](crate::population::Population) can store on-device. The
108/// associated [`Tensor`](TensorGenome::Tensor) type names *which* tensor flavour
109/// backs the kind, tying the storage type to the marker at compile time: `Real`
110/// maps to `Tensor<B, 2>`; `Binary`, `Integer`, and `Permutation` map to
111/// `Tensor<B, 2, Int>`.
112///
113/// Because the storage type is chosen by the trait impl, `Population<B, K>` needs
114/// only one field and no run-time tag — the wrong-tensor-for-this-kind state is
115/// simply unrepresentable. Variable-length kinds such as [`Tree`] have no
116/// rectangular form and deliberately do not implement this trait, so
117/// `Population<B, Tree>` is not a valid type.
118pub trait TensorGenome: GenomeKind {
119 /// Device tensor type storing a whole population of this kind, shape
120 /// `(pop_size, genome_dim)`.
121 ///
122 /// The `Send + Sync` bound keeps `Population<B, K>` thread-safe under the
123 /// crate-wide [`Strategy: Send + Sync`](crate::strategy::Strategy) contract:
124 /// without it, generic code over `K: TensorGenome` could not prove the
125 /// stored tensor — and therefore the population — is shareable across
126 /// threads, even though every concrete Burn tensor is.
127 type Tensor<B: Backend>: Clone + Debug + Send + Sync;
128}
129
130impl TensorGenome for Real {
131 type Tensor<B: Backend> = Tensor<B, 2>;
132}
133
134impl TensorGenome for Binary {
135 type Tensor<B: Backend> = Tensor<B, 2, Int>;
136}
137
138impl TensorGenome for Integer {
139 type Tensor<B: Backend> = Tensor<B, 2, Int>;
140}
141
142impl TensorGenome for Permutation {
143 type Tensor<B: Backend> = Tensor<B, 2, Int>;
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn real_has_f32_element() {
152 let _: <Real as GenomeKind>::Element = 0.0_f32;
153 }
154
155 #[test]
156 fn binary_has_i32_element() {
157 let _: <Binary as GenomeKind>::Element = 1_i32;
158 }
159
160 #[test]
161 fn integer_has_i32_element() {
162 let _: <Integer as GenomeKind>::Element = 5_i32;
163 }
164
165 #[test]
166 fn permutation_has_i32_element() {
167 let _: <Permutation as GenomeKind>::Element = 7_i32;
168 }
169
170 #[test]
171 fn markers_are_debug() {
172 let _ = format!("{Real:?} {Binary:?} {Integer:?} {Tree:?} {Permutation:?}");
173 }
174
175 #[test]
176 fn permutation_impls_tensor_genome() {
177 use burn::backend::Flex;
178
179 // Compiles iff `Permutation: TensorGenome` with its tensor flavour
180 // unifying with `Tensor<Flex, 2, Int>`.
181 fn takes_perm_tensor(
182 t: <Permutation as TensorGenome>::Tensor<Flex>,
183 ) -> Tensor<Flex, 2, Int> {
184 t
185 }
186 let _ = takes_perm_tensor;
187 }
188
189 #[test]
190 fn tensor_genome_storage_is_send_sync() {
191 use burn::backend::Flex;
192
193 // Regression guard for the `Send + Sync` GAT bound: instantiating this
194 // for each kind fails to compile if the bound is ever dropped.
195 fn assert_send_sync<T: Send + Sync>() {}
196 assert_send_sync::<<Real as TensorGenome>::Tensor<Flex>>();
197 assert_send_sync::<<Binary as TensorGenome>::Tensor<Flex>>();
198 assert_send_sync::<<Integer as TensorGenome>::Tensor<Flex>>();
199 assert_send_sync::<<Permutation as TensorGenome>::Tensor<Flex>>();
200 }
201}