#![allow(clippy::needless_range_loop)]
#![allow(dead_code)]
use std::f64::consts::{PI, TAU};
fn log_normal_pdf(x: f64, mean: f64, var: f64) -> f64 {
-0.5 * ((x - mean).powi(2) / var + var.ln() + (TAU).ln())
}
fn normal_pdf(x: f64, mean: f64, var: f64) -> f64 {
(-(x - mean).powi(2) / (2.0 * var)).exp() / (TAU * var).sqrt()
}
fn log_sum_exp(values: &[f64]) -> f64 {
let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
if max.is_infinite() {
return f64::NEG_INFINITY;
}
let sum: f64 = values.iter().map(|&v| (v - max).exp()).sum();
max + sum.ln()
}
fn softmax(logits: &[f64]) -> Vec<f64> {
let max = logits.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let exp: Vec<f64> = logits.iter().map(|&x| (x - max).exp()).collect();
let sum: f64 = exp.iter().sum::<f64>().max(1e-300);
exp.iter().map(|&e| e / sum).collect()
}
fn mvn_log_pdf_diag(x: &[f64], mean: &[f64], var: &[f64]) -> f64 {
let d = x.len() as f64;
let log_det: f64 = var.iter().map(|v| v.max(1e-300).ln()).sum();
let maha: f64 = x
.iter()
.zip(mean.iter())
.zip(var.iter())
.map(|((&xi, &mi), &vi)| (xi - mi).powi(2) / vi.max(1e-300))
.sum();
-0.5 * (d * TAU.ln() + log_det + maha)
}
#[derive(Debug, Clone)]
pub struct BnNode {
pub name: String,
pub n_states: usize,
pub parents: Vec<usize>,
pub cpt: Vec<f64>,
}
impl BnNode {
pub fn new(
name: impl Into<String>,
n_states: usize,
parents: Vec<usize>,
cpt: Vec<f64>,
) -> Self {
Self {
name: name.into(),
n_states,
parents,
cpt,
}
}
pub fn cpt_value(&self, state: usize, parent_config: usize) -> f64 {
let offset = parent_config * self.n_states;
self.cpt[offset + state]
}
}
#[derive(Debug, Clone)]
pub struct BayesianNetwork {
pub nodes: Vec<BnNode>,
}
impl BayesianNetwork {
pub fn new() -> Self {
Self { nodes: Vec::new() }
}
pub fn add_node(&mut self, node: BnNode) -> usize {
let idx = self.nodes.len();
self.nodes.push(node);
idx
}
pub fn joint_probability(&self, assignment: &[usize]) -> f64 {
let mut prob = 1.0f64;
for (i, node) in self.nodes.iter().enumerate() {
let parent_config = self.parent_config_index(i, assignment);
prob *= node.cpt_value(assignment[i], parent_config);
}
prob
}
fn parent_config_index(&self, node_idx: usize, assignment: &[usize]) -> usize {
let node = &self.nodes[node_idx];
let mut config = 0usize;
for &p in &node.parents {
let p_states = self.nodes[p].n_states;
config = config * p_states + assignment[p];
}
config
}
pub fn marginal(&self, target: usize, target_state: usize) -> f64 {
let n = self.nodes.len();
let n_states: Vec<usize> = self.nodes.iter().map(|nd| nd.n_states).collect();
let total: usize = n_states.iter().product();
let mut prob = 0.0f64;
let mut assignment = vec![0usize; n];
for _ in 0..total {
if assignment[target] == target_state {
prob += self.joint_probability(&assignment);
}
let mut carry = 1;
for i in (0..n).rev() {
let next = assignment[i] + carry;
assignment[i] = next % n_states[i];
carry = next / n_states[i];
if carry == 0 {
break;
}
}
}
prob
}
pub fn marginal_all(&self, target: usize) -> Vec<f64> {
let n_states = self.nodes[target].n_states;
(0..n_states).map(|s| self.marginal(target, s)).collect()
}
pub fn conditional(
&self,
target: usize,
target_state: usize,
evidence: &[(usize, usize)],
) -> f64 {
let n = self.nodes.len();
let n_states: Vec<usize> = self.nodes.iter().map(|nd| nd.n_states).collect();
let total: usize = n_states.iter().product();
let mut num = 0.0f64;
let mut denom = 0.0f64;
let mut assignment = vec![0usize; n];
for _ in 0..total {
let consistent = evidence.iter().all(|&(ni, s)| assignment[ni] == s);
if consistent {
let p = self.joint_probability(&assignment);
denom += p;
if assignment[target] == target_state {
num += p;
}
}
let mut carry = 1;
for i in (0..n).rev() {
let next = assignment[i] + carry;
assignment[i] = next % n_states[i];
carry = next / n_states[i];
if carry == 0 {
break;
}
}
}
if denom < 1e-300 { 0.0 } else { num / denom }
}
pub fn validate(&self) -> bool {
for node in &self.nodes {
let n_configs = if node.parents.is_empty() {
1
} else {
node.cpt.len() / node.n_states
};
for cfg in 0..n_configs {
let sum: f64 = (0..node.n_states).map(|s| node.cpt_value(s, cfg)).sum();
if (sum - 1.0).abs() > 1e-6 {
return false;
}
}
}
true
}
}
impl Default for BayesianNetwork {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct HiddenMarkovModel {
pub n_states: usize,
pub initial: Vec<f64>,
pub transition: Vec<Vec<f64>>,
pub emission_mean: Vec<f64>,
pub emission_var: Vec<f64>,
}
impl HiddenMarkovModel {
pub fn new(
n_states: usize,
initial: Vec<f64>,
transition: Vec<Vec<f64>>,
emission_mean: Vec<f64>,
emission_var: Vec<f64>,
) -> Self {
Self {
n_states,
initial,
transition,
emission_mean,
emission_var,
}
}
pub fn uniform(n_states: usize) -> Self {
let p = 1.0 / n_states as f64;
let initial = vec![p; n_states];
let transition = vec![vec![p; n_states]; n_states];
let emission_mean: Vec<f64> = (0..n_states).map(|i| i as f64).collect();
let emission_var = vec![1.0; n_states];
Self::new(n_states, initial, transition, emission_mean, emission_var)
}
fn log_emit(&self, s: usize, obs: f64) -> f64 {
log_normal_pdf(obs, self.emission_mean[s], self.emission_var[s])
}
pub fn forward(&self, observations: &[f64]) -> f64 {
let t_len = observations.len();
if t_len == 0 {
return 0.0;
}
let k = self.n_states;
let mut alpha = vec![0.0f64; k];
for s in 0..k {
alpha[s] = self.initial[s].ln() + self.log_emit(s, observations[0]);
}
for t in 1..t_len {
let mut alpha_new = vec![f64::NEG_INFINITY; k];
for j in 0..k {
let log_emit_j = self.log_emit(j, observations[t]);
let terms: Vec<f64> = (0..k)
.map(|i| alpha[i] + self.transition[i][j].max(1e-300).ln())
.collect();
alpha_new[j] = log_sum_exp(&terms) + log_emit_j;
}
alpha = alpha_new;
}
log_sum_exp(&alpha)
}
pub fn viterbi(&self, observations: &[f64]) -> Vec<usize> {
let t_len = observations.len();
if t_len == 0 {
return Vec::new();
}
let k = self.n_states;
let mut delta = vec![vec![0.0f64; k]; t_len];
let mut psi = vec![vec![0usize; k]; t_len];
for s in 0..k {
delta[0][s] = self.initial[s].max(1e-300).ln() + self.log_emit(s, observations[0]);
}
for t in 1..t_len {
for j in 0..k {
let (best_s, best_val) = (0..k)
.map(|i| {
let v = delta[t - 1][i] + self.transition[i][j].max(1e-300).ln();
(i, v)
})
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.expect("states iterator is non-empty");
delta[t][j] = best_val + self.log_emit(j, observations[t]);
psi[t][j] = best_s;
}
}
let mut path = vec![0usize; t_len];
path[t_len - 1] = (0..k)
.max_by(|&a, &b| {
delta[t_len - 1][a]
.partial_cmp(&delta[t_len - 1][b])
.unwrap_or(std::cmp::Ordering::Equal)
})
.expect("k states is non-empty");
for t in (0..t_len - 1).rev() {
path[t] = psi[t + 1][path[t + 1]];
}
path
}
pub fn baum_welch(&mut self, observations: &[f64], n_iter: usize) -> Vec<f64> {
let t_len = observations.len();
let k = self.n_states;
let mut ll_history = Vec::new();
for _iter in 0..n_iter {
let mut log_alpha = vec![vec![0.0f64; k]; t_len];
for s in 0..k {
log_alpha[0][s] =
self.initial[s].max(1e-300).ln() + self.log_emit(s, observations[0]);
}
for t in 1..t_len {
for j in 0..k {
let terms: Vec<f64> = (0..k)
.map(|i| log_alpha[t - 1][i] + self.transition[i][j].max(1e-300).ln())
.collect();
log_alpha[t][j] = log_sum_exp(&terms) + self.log_emit(j, observations[t]);
}
}
let log_ll = log_sum_exp(&log_alpha[t_len - 1]);
ll_history.push(log_ll);
let mut log_beta = vec![vec![0.0f64; k]; t_len];
for t in (0..t_len - 1).rev() {
for i in 0..k {
let terms: Vec<f64> = (0..k)
.map(|j| {
self.transition[i][j].max(1e-300).ln()
+ self.log_emit(j, observations[t + 1])
+ log_beta[t + 1][j]
})
.collect();
log_beta[t][i] = log_sum_exp(&terms);
}
}
let mut gamma = vec![vec![0.0f64; k]; t_len];
for t in 0..t_len {
let log_probs: Vec<f64> =
(0..k).map(|s| log_alpha[t][s] + log_beta[t][s]).collect();
let norm = log_sum_exp(&log_probs);
for s in 0..k {
gamma[t][s] = (log_probs[s] - norm).exp();
}
}
let mut xi = vec![vec![vec![0.0f64; k]; k]; t_len.saturating_sub(1)];
for t in 0..t_len.saturating_sub(1) {
let mut xi_t = vec![vec![0.0f64; k]; k];
let mut log_xi_t = vec![vec![0.0f64; k]; k];
for i in 0..k {
for j in 0..k {
log_xi_t[i][j] = log_alpha[t][i]
+ self.transition[i][j].max(1e-300).ln()
+ self.log_emit(j, observations[t + 1])
+ log_beta[t + 1][j];
}
}
let flat: Vec<f64> = log_xi_t.iter().flat_map(|r| r.iter().copied()).collect();
let norm = log_sum_exp(&flat);
for i in 0..k {
for j in 0..k {
xi_t[i][j] = (log_xi_t[i][j] - norm).exp();
}
}
xi[t] = xi_t;
}
for s in 0..k {
self.initial[s] = gamma[0][s].max(1e-300);
}
let init_sum: f64 = self.initial.iter().sum::<f64>().max(1e-300);
for s in 0..k {
self.initial[s] /= init_sum;
}
for i in 0..k {
let denom: f64 = (0..t_len.saturating_sub(1))
.map(|t| gamma[t][i])
.sum::<f64>()
.max(1e-300);
for j in 0..k {
let num: f64 = (0..t_len.saturating_sub(1)).map(|t| xi[t][i][j]).sum();
self.transition[i][j] = (num / denom).max(1e-300);
}
let row_sum: f64 = self.transition[i].iter().sum::<f64>().max(1e-300);
for j in 0..k {
self.transition[i][j] /= row_sum;
}
}
for s in 0..k {
let denom: f64 = (0..t_len).map(|t| gamma[t][s]).sum::<f64>().max(1e-300);
let new_mean: f64 = (0..t_len)
.map(|t| gamma[t][s] * observations[t])
.sum::<f64>()
/ denom;
let new_var: f64 = ((0..t_len)
.map(|t| gamma[t][s] * (observations[t] - new_mean).powi(2))
.sum::<f64>()
/ denom)
.max(1e-6);
self.emission_mean[s] = new_mean;
self.emission_var[s] = new_var;
}
}
ll_history
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum KernelType {
Rbf,
Matern32,
Matern52,
Periodic,
}
#[derive(Debug, Clone)]
pub struct GaussianProcess {
pub kernel: KernelType,
pub signal_var: f64,
pub length_scale: f64,
pub period: f64,
pub noise_var: f64,
pub x_train: Vec<f64>,
pub y_train: Vec<f64>,
chol: Vec<f64>,
alpha: Vec<f64>,
}
impl GaussianProcess {
pub fn new(kernel: KernelType, signal_var: f64, length_scale: f64, noise_var: f64) -> Self {
Self {
kernel,
signal_var,
length_scale,
period: 1.0,
noise_var,
x_train: Vec::new(),
y_train: Vec::new(),
chol: Vec::new(),
alpha: Vec::new(),
}
}
pub fn with_period(mut self, period: f64) -> Self {
self.period = period;
self
}
pub fn k(&self, x1: f64, x2: f64) -> f64 {
let r = (x1 - x2).abs();
match self.kernel {
KernelType::Rbf => {
self.signal_var * (-r * r / (2.0 * self.length_scale * self.length_scale)).exp()
}
KernelType::Matern32 => {
let sq3r = 3.0f64.sqrt() * r / self.length_scale;
self.signal_var * (1.0 + sq3r) * (-sq3r).exp()
}
KernelType::Matern52 => {
let sq5r = 5.0f64.sqrt() * r / self.length_scale;
self.signal_var * (1.0 + sq5r + sq5r * sq5r / 3.0) * (-sq5r).exp()
}
KernelType::Periodic => {
let arg = PI * r / self.period;
self.signal_var
* (-2.0 * arg.sin().powi(2) / (self.length_scale * self.length_scale)).exp()
}
}
}
pub fn fit(&mut self, x_train: Vec<f64>, y_train: Vec<f64>) {
let n = x_train.len();
self.x_train = x_train;
self.y_train = y_train.clone();
let mut k_mat = vec![0.0f64; n * n];
for i in 0..n {
for j in 0..n {
k_mat[i * n + j] = self.k(self.x_train[i], self.x_train[j]);
}
k_mat[i * n + i] += self.noise_var;
}
let mut l = k_mat.clone();
for i in 0..n {
for j in 0..=i {
let mut s = l[i * n + j];
for k_idx in 0..j {
s -= l[i * n + k_idx] * l[j * n + k_idx];
}
if i == j {
l[i * n + j] = s.max(1e-12).sqrt();
} else {
l[i * n + j] = s / l[j * n + j].max(1e-12);
}
}
for j in i + 1..n {
l[i * n + j] = 0.0;
}
}
self.chol = l.clone();
let mut w = y_train;
for i in 0..n {
let mut s = w[i];
for j in 0..i {
s -= l[i * n + j] * w[j];
}
w[i] = s / l[i * n + i].max(1e-12);
}
let mut alpha = w;
for i in (0..n).rev() {
let mut s = alpha[i];
for j in i + 1..n {
s -= l[j * n + i] * alpha[j];
}
alpha[i] = s / l[i * n + i].max(1e-12);
}
self.alpha = alpha;
}
pub fn predict(&self, x_star: f64) -> (f64, f64) {
let n = self.x_train.len();
if n == 0 {
return (0.0, self.signal_var + self.noise_var);
}
let k_star: Vec<f64> = self.x_train.iter().map(|&xi| self.k(x_star, xi)).collect();
let mean: f64 = k_star
.iter()
.zip(self.alpha.iter())
.map(|(a, b)| a * b)
.sum();
let mut v = k_star.clone();
for i in 0..n {
let mut s = v[i];
for j in 0..i {
s -= self.chol[i * n + j] * v[j];
}
v[i] = s / self.chol[i * n + i].max(1e-12);
}
let var = (self.k(x_star, x_star) - v.iter().map(|vi| vi * vi).sum::<f64>()).max(1e-12);
(mean, var)
}
pub fn log_marginal_likelihood(&self) -> f64 {
let n = self.x_train.len();
if n == 0 {
return 0.0;
}
let data_fit: f64 = self
.y_train
.iter()
.zip(self.alpha.iter())
.map(|(y, a)| y * a)
.sum::<f64>();
let log_det: f64 = (0..n)
.map(|i| self.chol[i * n + i].max(1e-300).ln())
.sum::<f64>();
-0.5 * data_fit - log_det - 0.5 * n as f64 * TAU.ln()
}
}
#[derive(Debug, Clone)]
pub struct DirichletProcess {
pub alpha: f64,
pub assignments: Vec<usize>,
pub cluster_counts: Vec<usize>,
pub cluster_means: Vec<f64>,
pub cluster_ss: Vec<f64>,
pub n_assigned: usize,
}
impl DirichletProcess {
pub fn new(alpha: f64) -> Self {
Self {
alpha,
assignments: Vec::new(),
cluster_counts: Vec::new(),
cluster_means: Vec::new(),
cluster_ss: Vec::new(),
n_assigned: 0,
}
}
pub fn n_clusters(&self) -> usize {
self.cluster_counts.len()
}
pub fn crp_assign(&mut self, x: f64) -> usize {
let n = self.n_assigned as f64;
let k = self.cluster_counts.len();
let mut probs: Vec<f64> = self
.cluster_counts
.iter()
.map(|&cnt| cnt as f64 / (n + self.alpha))
.collect();
probs.push(self.alpha / (n + self.alpha));
let chosen = probs
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(k);
if chosen == k {
self.cluster_counts.push(1);
self.cluster_means.push(x);
self.cluster_ss.push(0.0);
} else {
let cnt = self.cluster_counts[chosen] as f64;
let old_mean = self.cluster_means[chosen];
self.cluster_counts[chosen] += 1;
let new_mean = old_mean + (x - old_mean) / (cnt + 1.0);
self.cluster_ss[chosen] += (x - old_mean) * (x - new_mean);
self.cluster_means[chosen] = new_mean;
}
self.assignments.push(chosen);
self.n_assigned += 1;
chosen
}
pub fn stick_breaking_weights(&self, k: usize) -> Vec<f64> {
let mut weights = Vec::with_capacity(k);
let mut remaining = 1.0f64;
for i in 0..k {
let mean_beta = 1.0 / (1.0 + self.alpha);
let v = mean_beta * (1.0 - 0.1 * i as f64 / (k as f64 + 1.0));
let v = v.clamp(1e-6, 1.0 - 1e-6);
let w = remaining * v;
weights.push(w);
remaining *= 1.0 - v;
}
if let Some(last) = weights.last_mut() {
*last += remaining;
}
let total: f64 = weights.iter().sum::<f64>().max(1e-300);
weights.iter_mut().for_each(|w| *w /= total);
weights
}
pub fn cluster_variances(&self) -> Vec<f64> {
self.cluster_counts
.iter()
.zip(self.cluster_ss.iter())
.map(
|(&cnt, &ss)| {
if cnt > 1 { ss / (cnt - 1) as f64 } else { 1.0 }
},
)
.collect()
}
pub fn expected_clusters(alpha: f64, n: usize) -> f64 {
alpha * (1.0 + n as f64 / alpha).ln()
}
}
#[derive(Debug, Clone)]
pub struct VariationalInference {
pub n_components: usize,
pub log_weights: Vec<f64>,
pub var_mean: Vec<f64>,
pub var_var: Vec<f64>,
pub prior_mean: f64,
pub prior_var: f64,
pub obs_var: f64,
pub elbo_history: Vec<f64>,
}
impl VariationalInference {
pub fn new(n_components: usize, prior_mean: f64, prior_var: f64, obs_var: f64) -> Self {
let log_weights = vec![-(n_components as f64).ln(); n_components];
let var_mean: Vec<f64> = (0..n_components).map(|i| i as f64).collect();
let var_var = vec![1.0f64; n_components];
Self {
n_components,
log_weights,
var_mean,
var_var,
prior_mean,
prior_var,
obs_var,
elbo_history: Vec::new(),
}
}
pub fn elbo(&self, observations: &[f64]) -> f64 {
let weights = softmax(&self.log_weights);
let mut elbo = 0.0f64;
for &x in observations {
let ll_terms: Vec<f64> = (0..self.n_components)
.map(|k| {
weights[k].max(1e-300).ln()
+ log_normal_pdf(x, self.var_mean[k], self.obs_var + self.var_var[k])
})
.collect();
elbo += log_sum_exp(&ll_terms);
}
for k in 0..self.n_components {
let kl = 0.5
* (self.prior_var / self.var_var[k].max(1e-12)
+ (self.var_mean[k] - self.prior_mean).powi(2) / self.prior_var
- 1.0
+ (self.var_var[k] / self.prior_var).ln());
elbo -= weights[k] * kl;
}
elbo
}
pub fn cavi_step(&mut self, observations: &[f64]) -> f64 {
let n = observations.len() as f64;
for k in 0..self.n_components {
let weights = softmax(&self.log_weights);
let r_k: Vec<f64> = observations
.iter()
.map(|&x| weights[k] * normal_pdf(x, self.var_mean[k], self.obs_var))
.collect();
let r_sum: f64 = r_k.iter().sum::<f64>().max(1e-300);
let prior_prec = 1.0 / self.prior_var.max(1e-12);
let lik_prec = r_sum / self.obs_var.max(1e-12);
let post_prec = prior_prec + lik_prec;
let post_var = 1.0 / post_prec.max(1e-12);
let data_sum: f64 = r_k
.iter()
.zip(observations.iter())
.map(|(r, x)| r * x)
.sum();
let post_mean =
post_var * (prior_prec * self.prior_mean + data_sum / self.obs_var.max(1e-12));
self.var_mean[k] = post_mean;
self.var_var[k] = post_var;
self.log_weights[k] = r_sum.max(1e-300).ln();
}
let lse = log_sum_exp(&self.log_weights.clone());
for k in 0..self.n_components {
self.log_weights[k] -= lse;
}
let _ = n;
let elbo_val = self.elbo(observations);
self.elbo_history.push(elbo_val);
elbo_val
}
pub fn fit(&mut self, observations: &[f64], n_iter: usize) -> f64 {
for _ in 0..n_iter {
self.cavi_step(observations);
}
*self.elbo_history.last().unwrap_or(&f64::NEG_INFINITY)
}
pub fn reparameterize(&self, k: usize, eps: f64) -> f64 {
self.var_mean[k] + self.var_var[k].sqrt() * eps
}
pub fn predictive_density(&self, x: f64) -> f64 {
let weights = softmax(&self.log_weights);
(0..self.n_components)
.map(|k| weights[k] * normal_pdf(x, self.var_mean[k], self.obs_var + self.var_var[k]))
.sum()
}
}
#[derive(Debug, Clone)]
pub struct GmmComponent {
pub weight: f64,
pub mean: f64,
pub var: f64,
}
impl GmmComponent {
pub fn new(weight: f64, mean: f64, var: f64) -> Self {
Self { weight, mean, var }
}
}
#[derive(Debug, Clone)]
pub struct ExpectationMaximization {
pub n_components: usize,
pub components: Vec<GmmComponent>,
pub ll_history: Vec<f64>,
pub tol: f64,
}
impl ExpectationMaximization {
pub fn new(n_components: usize) -> Self {
let components = (0..n_components)
.map(|i| GmmComponent::new(1.0 / n_components as f64, i as f64, 1.0))
.collect();
Self {
n_components,
components,
ll_history: Vec::new(),
tol: 1e-6,
}
}
pub fn with_tol(mut self, tol: f64) -> Self {
self.tol = tol;
self
}
pub fn kmeans_init(&mut self, data: &[f64]) {
if data.is_empty() {
return;
}
let mut sorted = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let k = self.n_components;
for i in 0..k {
let idx = (sorted.len() * (2 * i + 1)) / (2 * k);
self.components[i].mean = sorted[idx.min(sorted.len() - 1)];
self.components[i].var = 1.0;
self.components[i].weight = 1.0 / k as f64;
}
}
pub fn log_likelihood(&self, data: &[f64]) -> f64 {
data.iter()
.map(|&x| {
let terms: Vec<f64> = self
.components
.iter()
.map(|c| c.weight.max(1e-300).ln() + log_normal_pdf(x, c.mean, c.var))
.collect();
log_sum_exp(&terms)
})
.sum()
}
pub fn bic(&self, data: &[f64]) -> f64 {
let n = data.len() as f64;
let ll = self.log_likelihood(data);
let n_params = (3 * self.n_components - 1) as f64;
n_params * n.ln() - 2.0 * ll
}
fn e_step(&self, data: &[f64]) -> Vec<Vec<f64>> {
data.iter()
.map(|&x| {
let log_probs: Vec<f64> = self
.components
.iter()
.map(|c| c.weight.max(1e-300).ln() + log_normal_pdf(x, c.mean, c.var))
.collect();
softmax(&log_probs)
})
.collect()
}
fn m_step(&mut self, data: &[f64], responsibilities: &[Vec<f64>]) {
let n = data.len() as f64;
for k in 0..self.n_components {
let r_sum: f64 = responsibilities
.iter()
.map(|r| r[k])
.sum::<f64>()
.max(1e-300);
let new_weight = r_sum / n;
let new_mean: f64 = responsibilities
.iter()
.zip(data.iter())
.map(|(r, &x)| r[k] * x)
.sum::<f64>()
/ r_sum;
let new_var: f64 = (responsibilities
.iter()
.zip(data.iter())
.map(|(r, &x)| r[k] * (x - new_mean).powi(2))
.sum::<f64>()
/ r_sum)
.max(1e-6);
self.components[k].weight = new_weight;
self.components[k].mean = new_mean;
self.components[k].var = new_var;
}
}
pub fn fit(&mut self, data: &[f64], max_iter: usize) -> f64 {
self.ll_history.clear();
let mut prev_ll = f64::NEG_INFINITY;
for _ in 0..max_iter {
let resp = self.e_step(data);
self.m_step(data, &resp);
let ll = self.log_likelihood(data);
self.ll_history.push(ll);
if (ll - prev_ll).abs() < self.tol {
break;
}
prev_ll = ll;
}
*self.ll_history.last().unwrap_or(&f64::NEG_INFINITY)
}
pub fn predict(&self, x: f64) -> usize {
self.components
.iter()
.enumerate()
.map(|(k, c)| {
let ll = c.weight.max(1e-300).ln() + log_normal_pdf(x, c.mean, c.var);
(k, ll)
})
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(k, _)| k)
.unwrap_or(0)
}
pub fn normalized_weights(&self) -> Vec<f64> {
let sum: f64 = self
.components
.iter()
.map(|c| c.weight)
.sum::<f64>()
.max(1e-300);
self.components.iter().map(|c| c.weight / sum).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_simple_bn() -> BayesianNetwork {
let mut bn = BayesianNetwork::new();
bn.add_node(BnNode::new("Rain", 2, vec![], vec![0.3, 0.7]));
bn.add_node(BnNode::new(
"Sprinkler",
2,
vec![0],
vec![0.1, 0.9, 0.5, 0.5], ));
bn
}
#[test]
fn test_bn_validate() {
let bn = make_simple_bn();
assert!(bn.validate());
}
#[test]
fn test_bn_joint_probability_sums_to_one() {
let mut bn = BayesianNetwork::new();
bn.add_node(BnNode::new("A", 2, vec![], vec![0.4, 0.6]));
bn.add_node(BnNode::new("B", 2, vec![0], vec![0.7, 0.3, 0.2, 0.8]));
let total: f64 = (0..4)
.map(|i| {
let a = i / 2;
let b_val = i % 2;
bn.joint_probability(&[a, b_val])
})
.sum();
assert!((total - 1.0).abs() < 1e-10);
}
#[test]
fn test_bn_marginal_sums_to_one() {
let bn = make_simple_bn();
let m0 = bn.marginal(0, 0);
let m1 = bn.marginal(0, 1);
assert!((m0 + m1 - 1.0).abs() < 1e-6);
}
#[test]
fn test_bn_marginal_root_equals_prior() {
let bn = make_simple_bn();
let m0 = bn.marginal(0, 0);
assert!((m0 - 0.3).abs() < 1e-8);
}
#[test]
fn test_bn_conditional_valid() {
let bn = make_simple_bn();
let p = bn.conditional(1, 0, &[(0, 0)]);
assert!((0.0..=1.0).contains(&p));
}
#[test]
fn test_bn_marginal_all_sums_to_one() {
let bn = make_simple_bn();
let m = bn.marginal_all(0);
let s: f64 = m.iter().sum();
assert!((s - 1.0).abs() < 1e-8);
}
#[test]
fn test_bn_cpt_value() {
let node = BnNode::new("X", 2, vec![], vec![0.4, 0.6]);
assert!((node.cpt_value(0, 0) - 0.4).abs() < 1e-10);
assert!((node.cpt_value(1, 0) - 0.6).abs() < 1e-10);
}
#[test]
fn test_bn_single_node() {
let mut bn = BayesianNetwork::new();
bn.add_node(BnNode::new("X", 3, vec![], vec![0.2, 0.5, 0.3]));
let p = bn.joint_probability(&[1]);
assert!((p - 0.5).abs() < 1e-10);
}
fn make_hmm() -> HiddenMarkovModel {
HiddenMarkovModel::new(
2,
vec![0.6, 0.4],
vec![vec![0.7, 0.3], vec![0.4, 0.6]],
vec![0.0, 3.0],
vec![1.0, 1.0],
)
}
#[test]
fn test_hmm_forward_returns_finite() {
let hmm = make_hmm();
let obs = vec![0.1, 0.2, 0.3, 2.8, 3.1];
let ll = hmm.forward(&obs);
assert!(ll.is_finite());
}
#[test]
fn test_hmm_forward_empty() {
let hmm = make_hmm();
assert_eq!(hmm.forward(&[]), 0.0);
}
#[test]
fn test_hmm_viterbi_length() {
let hmm = make_hmm();
let obs = vec![0.1, 0.2, 0.3, 2.8, 3.1];
let path = hmm.viterbi(&obs);
assert_eq!(path.len(), obs.len());
}
#[test]
fn test_hmm_viterbi_valid_states() {
let hmm = make_hmm();
let obs = vec![0.1, 2.9, 0.2, 3.0];
let path = hmm.viterbi(&obs);
assert!(path.iter().all(|&s| s < 2));
}
#[test]
fn test_hmm_viterbi_empty() {
let hmm = make_hmm();
assert_eq!(hmm.viterbi(&[]).len(), 0);
}
#[test]
fn test_hmm_baum_welch_ll_increases() {
let mut hmm = make_hmm();
let obs: Vec<f64> = (0..20)
.map(|i| if i % 3 == 0 { 0.1 } else { 2.9 })
.collect();
let ll_hist = hmm.baum_welch(&obs, 5);
for i in 1..ll_hist.len() {
assert!(ll_hist[i] >= ll_hist[i - 1] - 1e-4);
}
}
#[test]
fn test_hmm_uniform_creation() {
let hmm = HiddenMarkovModel::uniform(3);
assert_eq!(hmm.n_states, 3);
let row_sum: f64 = hmm.transition[0].iter().sum();
assert!((row_sum - 1.0).abs() < 1e-10);
}
#[test]
fn test_gp_rbf_kernel_diagonal() {
let gp = GaussianProcess::new(KernelType::Rbf, 1.0, 1.0, 1e-3);
assert!((gp.k(0.0, 0.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_gp_rbf_kernel_decays() {
let gp = GaussianProcess::new(KernelType::Rbf, 1.0, 1.0, 1e-3);
assert!(gp.k(0.0, 10.0) < gp.k(0.0, 1.0));
}
#[test]
fn test_gp_matern32_diagonal() {
let gp = GaussianProcess::new(KernelType::Matern32, 2.0, 1.0, 1e-3);
assert!((gp.k(0.0, 0.0) - 2.0).abs() < 1e-10);
}
#[test]
fn test_gp_matern52_diagonal() {
let gp = GaussianProcess::new(KernelType::Matern52, 1.5, 1.0, 1e-3);
assert!((gp.k(0.0, 0.0) - 1.5).abs() < 1e-10);
}
#[test]
fn test_gp_periodic_diagonal() {
let gp = GaussianProcess::new(KernelType::Periodic, 1.0, 1.0, 1e-3);
assert!((gp.k(0.0, 0.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_gp_fit_predict_mean() {
let mut gp = GaussianProcess::new(KernelType::Rbf, 1.0, 1.0, 1e-4);
let x = vec![0.0, 1.0, 2.0, 3.0];
let y = vec![0.0, 1.0, 4.0, 9.0];
gp.fit(x, y);
let (mean, _var) = gp.predict(1.0);
assert!((mean - 1.0).abs() < 0.5); }
#[test]
fn test_gp_predict_variance_positive() {
let mut gp = GaussianProcess::new(KernelType::Rbf, 1.0, 1.0, 1e-4);
gp.fit(vec![0.0, 1.0], vec![0.0, 1.0]);
let (_mean, var) = gp.predict(5.0); assert!(var > 0.0);
}
#[test]
fn test_gp_predict_empty() {
let gp = GaussianProcess::new(KernelType::Rbf, 1.0, 1.0, 1e-3);
let (mean, var) = gp.predict(0.5);
assert_eq!(mean, 0.0);
assert!(var > 0.0);
}
#[test]
fn test_gp_log_marginal_likelihood() {
let mut gp = GaussianProcess::new(KernelType::Rbf, 1.0, 1.0, 0.1);
gp.fit(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0]);
let lml = gp.log_marginal_likelihood();
assert!(lml.is_finite());
}
#[test]
fn test_dp_initial_state() {
let dp = DirichletProcess::new(1.0);
assert_eq!(dp.n_clusters(), 0);
assert_eq!(dp.n_assigned, 0);
}
#[test]
fn test_dp_crp_first_point() {
let mut dp = DirichletProcess::new(1.0);
let c = dp.crp_assign(0.0);
assert_eq!(c, 0);
assert_eq!(dp.n_clusters(), 1);
}
#[test]
fn test_dp_crp_multiple_points() {
let mut dp = DirichletProcess::new(0.1); for i in 0..10 {
dp.crp_assign(i as f64 * 0.01);
}
assert!(dp.n_clusters() <= 5);
}
#[test]
fn test_dp_stick_breaking_sums_to_one() {
let dp = DirichletProcess::new(2.0);
let w = dp.stick_breaking_weights(10);
let sum: f64 = w.iter().sum();
assert!((sum - 1.0).abs() < 1e-6);
}
#[test]
fn test_dp_stick_breaking_positive() {
let dp = DirichletProcess::new(1.0);
let w = dp.stick_breaking_weights(5);
assert!(w.iter().all(|&wi| wi > 0.0));
}
#[test]
fn test_dp_expected_clusters() {
let e = DirichletProcess::expected_clusters(1.0, 100);
assert!(e > 3.0 && e < 10.0);
}
#[test]
fn test_dp_cluster_variances() {
let mut dp = DirichletProcess::new(0.5);
for i in 0..5 {
dp.crp_assign(i as f64);
}
let vars = dp.cluster_variances();
assert!(vars.iter().all(|&v| v >= 0.0));
}
#[test]
fn test_vi_elbo_finite() {
let vi = VariationalInference::new(2, 0.0, 1.0, 1.0);
let obs = vec![0.0, 1.0, -1.0, 2.0];
let elbo = vi.elbo(&obs);
assert!(elbo.is_finite());
}
#[test]
fn test_vi_cavi_step_updates_params() {
let mut vi = VariationalInference::new(2, 0.0, 1.0, 1.0);
let old_mean = vi.var_mean[0];
let obs = vec![3.0, 3.1, 3.2, -3.0, -3.1, -3.2];
vi.cavi_step(&obs);
assert!((vi.var_mean[0] - old_mean).abs() > 0.0);
}
#[test]
fn test_vi_fit_returns_finite() {
let mut vi = VariationalInference::new(2, 0.0, 2.0, 1.0);
let obs: Vec<f64> = (0..20)
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
.collect();
let elbo = vi.fit(&obs, 10);
assert!(elbo.is_finite());
}
#[test]
fn test_vi_reparameterize() {
let vi = VariationalInference::new(2, 0.0, 1.0, 1.0);
let sample = vi.reparameterize(0, 1.0);
let expected = vi.var_mean[0] + vi.var_var[0].sqrt();
assert!((sample - expected).abs() < 1e-10);
}
#[test]
fn test_vi_predictive_density_positive() {
let vi = VariationalInference::new(2, 0.0, 1.0, 1.0);
let p = vi.predictive_density(0.0);
assert!(p > 0.0);
}
#[test]
fn test_vi_elbo_history_grows() {
let mut vi = VariationalInference::new(2, 0.0, 1.0, 1.0);
let obs = vec![1.0, -1.0, 2.0];
vi.fit(&obs, 5);
assert_eq!(vi.elbo_history.len(), 5);
}
#[test]
fn test_em_initial_weights_sum_to_one() {
let em = ExpectationMaximization::new(3);
let sum: f64 = em.normalized_weights().iter().sum();
assert!((sum - 1.0).abs() < 1e-10);
}
#[test]
fn test_em_kmeans_init() {
let mut em = ExpectationMaximization::new(2);
let data = vec![0.0, 0.1, 0.2, 5.0, 5.1, 5.2];
em.kmeans_init(&data);
let means: Vec<f64> = em.components.iter().map(|c| c.mean).collect();
assert!(means.iter().any(|&m| m < 1.0));
assert!(means.iter().any(|&m| m > 4.0));
}
#[test]
fn test_em_log_likelihood_finite() {
let em = ExpectationMaximization::new(2);
let data = vec![0.0, 1.0, 2.0];
assert!(em.log_likelihood(&data).is_finite());
}
#[test]
fn test_em_fit_ll_increases() {
let mut em = ExpectationMaximization::new(2);
let data: Vec<f64> = (0..30)
.map(|i| {
if i < 15 {
i as f64 * 0.1
} else {
5.0 + i as f64 * 0.1
}
})
.collect();
em.kmeans_init(&data);
em.fit(&data, 20);
let ll = &em.ll_history;
for i in 1..ll.len() {
assert!(ll[i] >= ll[i - 1] - 1e-4);
}
}
#[test]
fn test_em_predict_valid_component() {
let em = ExpectationMaximization::new(3);
let pred = em.predict(0.5);
assert!(pred < 3);
}
#[test]
fn test_em_bic_finite() {
let em = ExpectationMaximization::new(2);
let data = vec![0.0, 1.0, 5.0, 6.0];
let bic = em.bic(&data);
assert!(bic.is_finite());
}
#[test]
fn test_em_fit_separates_clusters() {
let mut em = ExpectationMaximization::new(2);
let mut data: Vec<f64> = (0..20).map(|i| i as f64 * 0.05).collect(); let data2: Vec<f64> = (0..20).map(|i| 10.0 + i as f64 * 0.05).collect(); data.extend(data2);
em.kmeans_init(&data);
em.fit(&data, 50);
let means: Vec<f64> = em.components.iter().map(|c| c.mean).collect();
assert!(means.iter().any(|&m| m < 3.0));
assert!(means.iter().any(|&m| m > 7.0));
}
#[test]
fn test_em_n_components() {
let em = ExpectationMaximization::new(4);
assert_eq!(em.n_components, 4);
assert_eq!(em.components.len(), 4);
}
#[test]
fn test_log_sum_exp_empty() {
assert_eq!(log_sum_exp(&[]), f64::NEG_INFINITY);
}
#[test]
fn test_log_sum_exp_single() {
assert!((log_sum_exp(&[2.0]) - 2.0).abs() < 1e-10);
}
#[test]
fn test_softmax_sums_to_one() {
let s = softmax(&[1.0, 2.0, 3.0]);
assert!((s.iter().sum::<f64>() - 1.0).abs() < 1e-10);
}
#[test]
fn test_normal_pdf_peak() {
let p = normal_pdf(0.0, 0.0, 1.0);
assert!((p - 1.0 / (TAU).sqrt()).abs() < 1e-10);
}
#[test]
fn test_mvn_log_pdf_diag() {
let x = vec![0.0, 0.0];
let mean = vec![0.0, 0.0];
let var = vec![1.0, 1.0];
let lp = mvn_log_pdf_diag(&x, &mean, &var);
assert!(lp.is_finite());
}
}