1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! The [`ProbabilityModel`] trait shared by estimation-of-distribution
//! algorithms (EDAs).
//!
//! An EDA replaces an explicit recombination operator with an explicit
//! probabilistic model of the promising region of search space. Each
//! generation fits a model to the selected individuals and then samples a
//! fresh population from it. This module defines the seam between the
//! generic [`crate::algorithms::eda::EdaStrategy`] driver and the concrete
//! model implementations (univariate Gaussian, Bernoulli, compact-GA, and a
//! dependency-chain MIMIC model).
use Debug;
use ;
use Rng;
/// A probabilistic model of a promising region of search space.
///
/// EDAs run a `fit` → `sample` loop: each generation, [`fit`](Self::fit)
/// estimates model parameters from the (already truncation-selected)
/// population, then [`sample`](Self::sample) draws the next candidate
/// population from the fitted model. The model fully replaces the crossover
/// and mutation operators of a classical GA.
///
/// The two associated types separate *static* per-model configuration
/// ([`Params`](Self::Params), e.g. learning rates, initial means) from the
/// *evolving* fitted statistics ([`State`](Self::State), e.g. per-dimension
/// means and variances). [`State`](Self::State) is deliberately `Sync` (a
/// stronger bound than the `Send`-only [`crate::Strategy::State`]) so a future
/// covariance-carrying CMA-ES state can be shared across threads; the same
/// `fit`/`sample` shape is intended to host that model unchanged.
///
/// The `fitness` tensor is passed to [`fit`](Self::fit) so models that weight
/// or rank the selected individuals (rank-μ updates, weighted MLE) can use it.
/// The univariate models shipped here perform an unweighted maximum-likelihood
/// fit and ignore it.
///
/// # Invariants
///
/// - **Prior path.** When `prev = None`, the model builds its prior *purely*
/// from [`params`](Self::Params). On this path the `population` and
/// `fitness` tensors are ignored; [`EdaStrategy`](crate::algorithms::eda::EdaStrategy)'s
/// `init` passes them as a `0 × 0` population and a length-`0` fitness tensor,
/// so a model must never read their contents when `prev` is `None`.
/// - **Host RNG only.** All randomness in [`sample`](Self::sample) must come
/// from the supplied `rng`. Implementations must never call `Tensor::random`
/// or `B::seed`: Burn's GPU PRNG kernels are seeded through process-global
/// state, which interleaves across parallel strategy calls and breaks the
/// per-stream determinism the crate guarantees. Sample on the host and upload
/// with `Tensor::from_data`.
/// - **Selection order.** The population rows handed to [`fit`](Self::fit) by
/// [`EdaStrategy`](crate::algorithms::eda::EdaStrategy)'s `tell` arrive in
/// *ascending-fitness*
/// order (best first), deterministically. Models that need the best or worst
/// row (e.g. PBIL, cGA) must still compute argmin/argmax themselves rather
/// than assume a fixed index — the ascending order is a convenience, not a
/// contract a model may hard-code against.
/// - **`Sync` state.** [`State`](Self::State) requires `Sync`, deliberately
/// exceeding [`crate::Strategy::State`]'s `Send`-only bound, to leave room for
/// a future thread-shared CMA-ES state.
/// - **Sanitized input assumed.** [`fit`](Self::fit) and
/// [`sample`](Self::sample) are infallible by design. The
/// [`EdaStrategy`](crate::algorithms::eda::EdaStrategy) driver sanitizes
/// fitness (`NaN` → `-inf`) and clamps the selected-row count to `≥ 2` before
/// calling `fit`, so the supported call path never supplies empty populations
/// or non-finite fitness. Callers that invoke `fit`/`sample` directly
/// (bypassing the driver) must uphold the same preconditions; otherwise
/// behaviour is unspecified — implementations may emit `NaN` statistics or
/// panic in `sample`.
///
/// # Examples
///
/// ```
/// use burn::backend::Flex;
/// use burn::tensor::{Tensor, TensorData};
/// use rand::{rngs::StdRng, SeedableRng};
/// use rlevo_evolution::ProbabilityModel;
/// use rlevo_evolution::algorithms::eda::{UnivariateGaussian, UnivariateGaussianParams};
///
/// let device = Default::default();
/// let model = UnivariateGaussian;
/// let params = UnivariateGaussianParams::default_for(2);
///
/// // Fit to a tiny two-row, two-column selected population.
/// let pop = Tensor::<Flex, 2>::from_data(
/// TensorData::new(vec![0.0_f32, 0.0, 2.0, 2.0], [2, 2]),
/// &device,
/// );
/// let fitness = Tensor::<Flex, 1>::from_data(TensorData::new(vec![0.0_f32, 8.0], [2]), &device);
/// let state =
/// ProbabilityModel::<Flex>::fit(&model, ¶ms, None, pop.clone(), fitness.clone(), &device);
/// let next = ProbabilityModel::<Flex>::fit(&model, ¶ms, Some(&state), pop, fitness, &device);
///
/// // Sample a fresh population from the fitted model.
/// let mut rng = StdRng::seed_from_u64(0);
/// let drawn: Tensor<Flex, 2> = model.sample(&next, 4, &mut rng, &device);
/// assert_eq!(drawn.dims(), [4, 2]);
/// ```