#[cfg(not(feature = "riemannian"))]
fn main() {
eprintln!("This example requires `--features riemannian`.");
}
#[cfg(feature = "riemannian")]
fn main() {
use flowmatch::ode::OdeMethod;
use flowmatch::riemannian::{train_riemannian_fm, ManifoldVectorField};
use flowmatch::riemannian_ode::integrate_fixed_manifold;
use flowmatch::sd_fm::SdFmTrainConfig;
use infogeom::FisherRaoSimplex;
use ndarray::{Array1, Array2, ArrayView1};
let manifold = FisherRaoSimplex::default();
const DIM: usize = 3;
let targets: Vec<[f32; DIM]> = vec![[0.90, 0.05, 0.05], [0.05, 0.90, 0.05], [0.05, 0.05, 0.90]];
let n_targets = targets.len();
let mut x1_data = Array2::<f32>::zeros((n_targets, DIM));
for (i, t) in targets.iter().enumerate() {
for j in 0..DIM {
x1_data[[i, j]] = t[j];
}
}
const HIDDEN: usize = 32;
struct MlpTangentField {
dim: usize,
hidden: usize,
w1: Array2<f32>,
b1: Array1<f32>,
w2: Array2<f32>,
b2: Array1<f32>,
}
impl MlpTangentField {
fn new(dim: usize, hidden: usize, seed: u64) -> Self {
let input_size = dim + 1;
let mut state = seed;
let scale1 = ((2.0 / (input_size + hidden) as f64).sqrt()) as f32;
let scale2 = ((2.0 / (hidden + dim) as f64).sqrt()) as f32;
let xorshift = |s: &mut u64| -> f32 {
*s ^= *s << 13;
*s ^= *s >> 7;
*s ^= *s << 17;
((*s as f32) / (u64::MAX as f32)) * 2.0 - 1.0
};
let w1 = Array2::from_shape_fn((hidden, input_size), |_| xorshift(&mut state) * scale1);
let b1 = Array1::zeros(hidden);
let w2 = Array2::from_shape_fn((dim, hidden), |_| xorshift(&mut state) * scale2);
let b2 = Array1::zeros(dim);
Self {
dim,
hidden,
w1,
b1,
w2,
b2,
}
}
fn forward(&self, x: &ArrayView1<f32>, t: f32) -> (Array1<f32>, Array1<f32>, Array1<f32>) {
let input_size = self.dim + 1;
let mut input = Array1::<f32>::zeros(input_size);
for j in 0..self.dim {
input[j] = x[j];
}
input[self.dim] = t;
let pre = self.w1.dot(&input) + &self.b1;
let h = pre.mapv(f32::tanh);
let out = self.w2.dot(&h) + &self.b2;
(pre, h, out)
}
}
impl ManifoldVectorField for MlpTangentField {
fn eval(&self, x: &ArrayView1<f32>, t: f32) -> Array1<f32> {
let (_pre, _h, out) = self.forward(x, t);
out
}
fn sgd_step(&mut self, x: &ArrayView1<f32>, t: f32, target: &ArrayView1<f32>, lr: f32) {
let (_pre, h, out) = self.forward(x, t);
let input_size = self.dim + 1;
let mut input = Array1::<f32>::zeros(input_size);
for j in 0..self.dim {
input[j] = x[j];
}
input[self.dim] = t;
let d_out = &out - target;
let d_h = self.w2.t().dot(&d_out);
for i in 0..self.dim {
for j in 0..self.hidden {
self.w2[[i, j]] -= lr * d_out[i] * h[j];
}
self.b2[i] -= lr * d_out[i];
}
let d_pre = &d_h * &(1.0 - &h * &h);
for i in 0..self.hidden {
for j in 0..input_size {
self.w1[[i, j]] -= lr * d_pre[i] * input[j];
}
self.b1[i] -= lr * d_pre[i];
}
}
}
let mut field = MlpTangentField::new(DIM, HIDDEN, 42);
let cfg = SdFmTrainConfig {
lr: 0.01,
steps: 4_000,
batch_size: 16,
sample_steps: 0,
seed: 42,
..Default::default()
};
let sample_x0 = |rng: &mut rand_chacha::ChaCha8Rng| -> Array1<f32> {
use rand::Rng;
let noise = 0.08_f32;
let raw: Vec<f32> = (0..DIM)
.map(|_| (1.0 / DIM as f32) + (rng.random::<f32>() * 2.0 * noise - noise))
.collect();
let clamped: Vec<f32> = raw.iter().map(|&v| v.max(1e-3)).collect();
let s: f32 = clamped.iter().sum();
Array1::from_vec(clamped.iter().map(|&v| v / s).collect())
};
train_riemannian_fm(&manifold, &mut field, &x1_data.view(), &cfg, sample_x0)
.expect("training failed");
println!(
"Trained Fisher Flow Matching on the {DIM}-category simplex ({} steps, batch {}).",
cfg.steps, cfg.batch_size
);
let n_samples = 12;
let ode_steps = 50_usize;
let dt = 1.0_f64 / (ode_steps as f64);
let mut rng = {
use rand::SeedableRng;
rand_chacha::ChaCha8Rng::seed_from_u64(99)
};
let starts: Vec<Array1<f64>> = (0..n_samples)
.map(|_| {
use rand::Rng;
let noise = 0.08_f64;
let raw: Vec<f64> = (0..DIM)
.map(|_| (1.0 / DIM as f64) + (rng.random::<f64>() * 2.0 * noise - noise))
.collect();
let clamped: Vec<f64> = raw.iter().map(|&v| v.max(1e-3)).collect();
let s: f64 = clamped.iter().sum();
Array1::from_vec(clamped.iter().map(|&v| v / s).collect())
})
.collect();
let final_points: Vec<Array1<f64>> = starts
.iter()
.map(|x0| {
integrate_fixed_manifold(
OdeMethod::Heun,
&manifold,
x0,
0.0,
dt,
ode_steps,
|x: &ArrayView1<f64>, t: f64| -> Array1<f64> {
let x32 = x.mapv(|v| v as f32);
let v32 = field.eval(&x32.view(), t as f32);
v32.mapv(|v| v as f64)
},
)
.expect("integration failed")
})
.collect();
let targets_f64: Vec<Array1<f64>> = targets
.iter()
.map(|t| Array1::from_vec(t.iter().map(|&v| v as f64).collect()))
.collect();
let nearest_mode_dist = |p: &Array1<f64>| -> f64 {
targets_f64
.iter()
.map(|t| {
let d = p - t;
d.dot(&d).sqrt()
})
.fold(f64::INFINITY, f64::min)
};
let is_simplex = |p: &Array1<f64>| -> bool {
let sum: f64 = p.sum();
(sum - 1.0).abs() < 1e-6 && p.iter().all(|&v| v >= -1e-9)
};
let base_avg: f64 = starts.iter().map(nearest_mode_dist).sum::<f64>() / n_samples as f64;
let final_avg: f64 = final_points.iter().map(nearest_mode_dist).sum::<f64>() / n_samples as f64;
let on_simplex = final_points.iter().filter(|p| is_simplex(p)).count();
println!(" base avg dist to nearest mode: {base_avg:.6}");
println!(" final avg dist to nearest mode: {final_avg:.6}");
println!(" final points on simplex: {on_simplex}/{n_samples}");
for (i, p) in final_points.iter().enumerate() {
println!(" sample {i:2}: [{:.4}, {:.4}, {:.4}]", p[0], p[1], p[2]);
}
assert_eq!(
on_simplex, n_samples,
"some integrated points left the simplex ({on_simplex}/{n_samples})"
);
assert!(
final_avg < base_avg,
"training did not move samples toward the modes: base={base_avg:.6}, final={final_avg:.6}"
);
}