use burn::tensor::{Tensor, TensorData, backend::Backend};
use rand::Rng;
use rand_distr::{Distribution as _, Normal};
use crate::probability_model::ProbabilityModel;
#[derive(Debug, Clone)]
pub struct DependencyChainParams {
pub genome_dim: usize,
pub init_mean: f32,
pub init_std: f32,
pub min_variance: f32,
}
impl DependencyChainParams {
#[must_use]
pub fn default_for(genome_dim: usize) -> Self {
Self {
genome_dim,
init_mean: 0.0,
init_std: 2.0,
min_variance: 1e-6,
}
}
}
#[derive(Debug, Clone)]
pub struct DependencyChainState {
pub chain: Vec<usize>,
pub mean: Vec<f32>,
pub std: Vec<f32>,
pub link_corr: Vec<f32>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DependencyChain;
impl<B: Backend> ProbabilityModel<B> for DependencyChain {
type Params = DependencyChainParams;
type State = DependencyChainState;
#[allow(clippy::too_many_lines)]
fn fit(
&self,
params: &Self::Params,
prev: Option<&Self::State>,
population: Tensor<B, 2>,
fitness: Tensor<B, 1>,
device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> Self::State {
let _ = device;
let _ = fitness;
let Some(_prev) = prev else {
let d = params.genome_dim;
return DependencyChainState {
chain: (0..d).collect(),
mean: vec![params.init_mean; d],
std: vec![params.init_std; d],
link_corr: vec![0.0; d],
};
};
let [k, d] = population.dims();
if k < 2 {
return DependencyChainState {
chain: (0..d).collect(),
mean: vec![params.init_mean; d],
std: vec![params.init_std; d],
link_corr: vec![0.0; d],
};
}
let rows = population
.into_data()
.into_vec::<f32>()
.expect("population tensor must be readable as f32");
#[allow(clippy::cast_precision_loss)]
let kf = k as f32;
let mut mean = vec![0.0_f32; d];
for i in 0..k {
for j in 0..d {
mean[j] += rows[i * d + j];
}
}
for m in &mut mean {
*m /= kf;
}
let mut raw_var = vec![0.0_f32; d];
for i in 0..k {
for j in 0..d {
let diff = rows[i * d + j] - mean[j];
raw_var[j] += diff * diff;
}
}
for v in &mut raw_var {
*v /= kf;
}
let std: Vec<f32> = raw_var
.iter()
.map(|&v| v.max(params.min_variance).sqrt())
.collect();
let mut cov = vec![0.0_f32; d * d];
for i in 0..k {
for a in 0..d {
let da = rows[i * d + a] - mean[a];
for b in 0..d {
let db = rows[i * d + b] - mean[b];
cov[a * d + b] += da * db;
}
}
}
for c in &mut cov {
*c /= kf;
}
let significance = 2.0 / kf.sqrt();
let mut corr = vec![0.0_f32; d * d];
let mut mi = vec![0.0_f32; d * d];
for a in 0..d {
for b in 0..d {
let r = if raw_var[a] < params.min_variance || raw_var[b] < params.min_variance {
0.0
} else {
let raw = cov[a * d + b] / (raw_var[a].sqrt() * raw_var[b].sqrt());
if raw.abs() < significance {
0.0
} else {
raw.clamp(-0.9999, 0.9999)
}
};
corr[a * d + b] = r;
mi[a * d + b] = -0.5 * (1.0 - r * r).ln();
}
}
let mut root = 0_usize;
let mut root_std = f32::INFINITY;
for (j, &sj) in std.iter().enumerate() {
if sj < root_std {
root_std = sj;
root = j;
}
}
let mut visited = vec![false; d];
let mut chain = Vec::with_capacity(d);
chain.push(root);
visited[root] = true;
for _ in 1..d {
let last = *chain.last().unwrap();
let mut best_j = usize::MAX;
let mut best_mi = f32::NEG_INFINITY;
for j in 0..d {
if visited[j] {
continue;
}
if mi[last * d + j] > best_mi {
best_mi = mi[last * d + j];
best_j = j;
}
}
chain.push(best_j);
visited[best_j] = true;
}
let mut link_corr = vec![0.0_f32; d];
for t in 1..chain.len() {
let parent = chain[t - 1];
let cur = chain[t];
link_corr[cur] = corr[parent * d + cur];
}
DependencyChainState {
chain,
mean,
std,
link_corr,
}
}
fn sample(
&self,
state: &Self::State,
n: usize,
rng: &mut dyn Rng,
device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> Tensor<B, 2> {
let d = state.mean.len();
let mut rows = vec![0.0_f32; n * d];
for i in 0..n {
let base = i * d;
let root = state.chain[0];
let root_normal = Normal::new(state.mean[root], state.std[root])
.expect("floored std is positive and finite");
rows[base + root] = root_normal.sample(rng);
for t in 1..state.chain.len() {
let parent = state.chain[t - 1];
let cur = state.chain[t];
let r = state.link_corr[cur];
let mu_c = state.mean[cur];
let mu_p = state.mean[parent];
let sigma_c = state.std[cur];
let sigma_p = state.std[parent]; let cond_mean = mu_c + r * (sigma_c / sigma_p) * (rows[base + parent] - mu_p);
let cond_std = (sigma_c * sigma_c * (1.0 - r * r)).sqrt();
rows[base + cur] =
if cond_mean.is_finite() && cond_std.is_finite() && cond_std > 0.0 {
Normal::new(cond_mean, cond_std)
.expect("guarded: conditional std positive and finite")
.sample(rng)
} else {
Normal::new(mu_c, sigma_c)
.expect("floored marginal std is positive and finite")
.sample(rng)
};
}
}
Tensor::<B, 2>::from_data(TensorData::new(rows, [n, d]), device)
}
}
#[cfg(test)]
mod tests {
use super::*;
use burn::backend::Flex;
use rand::SeedableRng;
use rand::rngs::StdRng;
type TestBackend = Flex;
fn pop(rows: Vec<f32>, n: usize, d: usize) -> Tensor<TestBackend, 2> {
let device = Default::default();
Tensor::<TestBackend, 2>::from_data(TensorData::new(rows, [n, d]), &device)
}
fn fitness(values: Vec<f32>) -> Tensor<TestBackend, 1> {
let device = Default::default();
let n = values.len();
Tensor::<TestBackend, 1>::from_data(TensorData::new(values, [n]), &device)
}
fn fit_prior(p: &DependencyChainParams) -> DependencyChainState {
let device = Default::default();
<DependencyChain as ProbabilityModel<TestBackend>>::fit(
&DependencyChain,
p,
None,
pop(vec![], 0, 0),
fitness(vec![]),
&device,
)
}
fn refit(
p: &DependencyChainParams,
rows: Vec<f32>,
n: usize,
d: usize,
) -> DependencyChainState {
let device = Default::default();
let prior = fit_prior(p);
#[allow(clippy::cast_precision_loss)]
let fit_values: Vec<f32> = (0..n).map(|i| i as f32).collect();
<DependencyChain as ProbabilityModel<TestBackend>>::fit(
&DependencyChain,
p,
Some(&prior),
pop(rows, n, d),
fitness(fit_values),
&device,
)
}
#[test]
fn prior_is_natural_order_independent() {
let p = DependencyChainParams::default_for(3);
let state = fit_prior(&p);
assert_eq!(state.chain, vec![0, 1, 2]);
assert_eq!(state.mean, vec![0.0, 0.0, 0.0]);
assert_eq!(state.std, vec![2.0, 2.0, 2.0]);
assert_eq!(state.link_corr, vec![0.0, 0.0, 0.0]);
}
#[test]
fn chain_links_correlated_dimensions_adjacently() {
let p = DependencyChainParams::default_for(3);
let rows = vec![
-2.0, -2.01, 5.0, -1.0, -0.99, -3.0, 0.0, 0.01, 1.0, 1.0, 1.02, -4.0, 2.0, 1.98, 0.5, ];
let state = refit(&p, rows, 5, 3);
let pos0 = state.chain.iter().position(|&x| x == 0).unwrap();
let pos1 = state.chain.iter().position(|&x| x == 1).unwrap();
assert_eq!(
pos0.abs_diff(pos1),
1,
"dims 0 and 1 should be adjacent in chain {:?}",
state.chain
);
let child = usize::from(pos0 <= pos1);
assert!(
state.link_corr[child].abs() > 0.99,
"expected strong link corr, got {}",
state.link_corr[child]
);
}
#[test]
fn zero_variance_column_yields_zero_correlation() {
let p = DependencyChainParams::default_for(2);
let rows = vec![0.0, 5.0, 1.0, 5.0, 2.0, 5.0, 3.0, 5.0];
let state = refit(&p, rows, 4, 2);
for &r in &state.link_corr {
approx::assert_relative_eq!(r, 0.0, epsilon = 1e-6);
}
}
#[test]
fn perfect_correlation_is_clamped() {
let p = DependencyChainParams::default_for(2);
let rows = vec![0.0, 0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0];
let state = refit(&p, rows, 4, 2);
let child = *state.chain.last().unwrap();
assert!(state.link_corr[child].abs() <= 0.9999 + 1e-6);
assert!(state.link_corr[child].abs() > 0.99);
}
#[test]
fn link_corr_matches_expected_pearson() {
let p = DependencyChainParams::default_for(2);
let rows = vec![-2.0, -4.0, -1.0, -2.0, 0.0, 0.0, 1.0, 2.0, 2.0, 4.0];
let state = refit(&p, rows, 5, 2);
let child = *state.chain.last().unwrap();
approx::assert_relative_eq!(state.link_corr[child], 0.9999, epsilon = 1e-3);
}
#[test]
fn sampling_respects_chain_correlation() {
let p = DependencyChainParams::default_for(2);
let rows = vec![-2.0, -2.0, -1.0, -1.0, 0.0, 0.0, 1.0, 1.0, 2.0, 2.0];
let state = refit(&p, rows, 5, 2);
let device = Default::default();
let mut rng = StdRng::seed_from_u64(99);
let n = 5_000;
let samples = <DependencyChain as ProbabilityModel<TestBackend>>::sample(
&DependencyChain,
&state,
n,
&mut rng,
&device,
);
let data = samples
.into_data()
.into_vec::<f32>()
.expect("samples host-read of a tensor this test just built");
let mut s0 = 0.0_f64;
let mut s1 = 0.0_f64;
for i in 0..n {
s0 += f64::from(data[i * 2]);
s1 += f64::from(data[i * 2 + 1]);
}
#[allow(clippy::cast_precision_loss)]
let nf = n as f64;
let m0 = s0 / nf;
let m1 = s1 / nf;
let (mut cov, mut v0, mut v1) = (0.0_f64, 0.0_f64, 0.0_f64);
for i in 0..n {
let a = f64::from(data[i * 2]) - m0;
let b = f64::from(data[i * 2 + 1]) - m1;
cov += a * b;
v0 += a * a;
v1 += b * b;
}
let corr = cov / (v0.sqrt() * v1.sqrt());
assert!(corr > 0.9, "sampled correlation too low: {corr}");
}
#[test]
fn two_fits_same_data_identical_state() {
let p = DependencyChainParams::default_for(3);
let rows = vec![
-2.0, 1.0, 0.5, -1.0, 2.0, -0.5, 0.0, 0.0, 1.0, 1.0, -1.0, -1.0, ];
let a = refit(&p, rows.clone(), 4, 3);
let b = refit(&p, rows, 4, 3);
assert_eq!(a.chain, b.chain);
assert_eq!(a.mean, b.mean);
assert_eq!(a.std, b.std);
assert_eq!(a.link_corr, b.link_corr);
}
#[test]
fn fit_k_less_than_two_returns_prior() {
let p = DependencyChainParams::default_for(2);
let state = refit(&p, vec![1.0, 2.0], 1, 2);
assert_eq!(state.chain, vec![0, 1]);
assert_eq!(state.mean, vec![p.init_mean, p.init_mean]);
assert_eq!(state.std, vec![p.init_std, p.init_std]);
assert_eq!(state.link_corr, vec![0.0, 0.0]);
}
#[test]
fn sample_with_degenerate_link_stays_finite() {
let device = Default::default();
let state = DependencyChainState {
chain: vec![0, 1],
mean: vec![0.0, 0.0],
std: vec![1e-30, 1e30],
link_corr: vec![0.0, 0.9999],
};
let mut rng = StdRng::seed_from_u64(7);
let samples = <DependencyChain as ProbabilityModel<TestBackend>>::sample(
&DependencyChain,
&state,
16,
&mut rng,
&device,
);
for v in samples
.into_data()
.into_vec::<f32>()
.expect("samples host-read of a tensor this test just built")
{
assert!(
v.is_finite(),
"degenerate link must yield finite samples, got {v}"
);
}
}
#[test]
fn single_dimension_sample_stays_finite() {
let p = DependencyChainParams::default_for(1);
let state = refit(&p, vec![1.0, 2.0, 3.0, 4.0], 4, 1);
assert_eq!(state.chain, vec![0], "single-dim chain is the root only");
let device = Default::default();
let mut rng = StdRng::seed_from_u64(3);
let samples = <DependencyChain as ProbabilityModel<TestBackend>>::sample(
&DependencyChain,
&state,
256,
&mut rng,
&device,
);
for v in samples
.into_data()
.into_vec::<f32>()
.expect("samples host-read of a tensor this test just built")
{
assert!(v.is_finite(), "single-dim sample must be finite, got {v}");
}
}
#[test]
fn sample_is_deterministic_for_seed_and_state() {
let p = DependencyChainParams::default_for(3);
let rows = vec![
-2.0, 1.0, 0.5, -1.0, 2.0, -0.5, 0.0, 0.0, 1.0, 1.0, -1.0, -1.0, ];
let state = refit(&p, rows, 4, 3);
let device = Default::default();
let mut rng_a = StdRng::seed_from_u64(555);
let mut rng_b = StdRng::seed_from_u64(555);
let a = <DependencyChain as ProbabilityModel<TestBackend>>::sample(
&DependencyChain,
&state,
300,
&mut rng_a,
&device,
);
let b = <DependencyChain as ProbabilityModel<TestBackend>>::sample(
&DependencyChain,
&state,
300,
&mut rng_b,
&device,
);
let data_a = a
.into_data()
.into_vec::<f32>()
.expect("samples host-read of a tensor this test just built");
let data_b = b
.into_data()
.into_vec::<f32>()
.expect("samples host-read of a tensor this test just built");
assert_eq!(
data_a, data_b,
"same seed + state must produce identical output"
);
}
}