use core::f64;
use std::cell::RefCell;
use ndarray::prelude::*;
use ndarray_stats::QuantileExt;
use rand::{
Rng, RngExt, SeedableRng,
distr::{Distribution, weighted::WeightedIndex},
};
use rand_distr::Exp;
use rayon::prelude::*;
use crate::{
datasets::{CatSample, CatTable, CatTrj, CatType, GaussTable},
models::{
BN, CIM, CPD, CTBN, CatBN, CatCTBN, GaussBN, HasLabels, MixedBN, MixedCPD, MixedSample,
MixedSupport, MixedTable,
},
samplers::{BNSampler, CTBNSampler, ParBNSampler, ParCTBNSampler},
set,
types::{EPSILON, Error, Map, Result, Set},
};
#[derive(Debug)]
pub struct ForwardSampler<'a, R, M> {
rng: RefCell<&'a mut R>,
model: &'a M,
}
impl<'a, R, M> ForwardSampler<'a, R, M> {
#[inline]
pub fn new(rng: &'a mut R, model: &'a M) -> Result<Self> {
let rng = RefCell::new(rng);
Ok(Self { rng, model })
}
}
impl<R: Rng> BNSampler<CatBN> for ForwardSampler<'_, R, CatBN> {
type Sample = <CatBN as BN>::Sample;
type Samples = <CatBN as BN>::Samples;
fn sample(&self) -> Result<Self::Sample> {
let mut rng = self.rng.borrow_mut();
let mut sample = Array::zeros(self.model.labels().len());
for &i in self.model.topological_order() {
let cpd_i = &self.model.cpds()[i];
let pa_i = self.model.graph().parents(&set![i])?;
let pa_i = pa_i.iter().map(|&z| sample[z]).collect();
sample[i] = cpd_i.sample(&mut rng, &pa_i)?[0];
}
Ok(sample)
}
fn sample_n(&self, n: usize) -> Result<Self::Samples> {
let mut dataset = Array::zeros((n, self.model.labels().len()));
dataset
.rows_mut()
.into_iter()
.try_for_each(|mut row| -> Result<_> {
row.assign(&self.sample()?);
Ok(())
})?;
CatTable::new(self.model.support().clone(), dataset)
}
}
impl<R: Rng + SeedableRng> ParBNSampler<CatBN> for ForwardSampler<'_, R, CatBN> {
type Samples = <CatBN as BN>::Samples;
fn par_sample_n(&self, n: usize) -> Result<Self::Samples> {
let rng = self.rng.borrow_mut();
let seeds: Vec<_> = rng.random_iter().take(n).collect();
let mut samples = Array::zeros((n, self.model.labels().len()));
seeds
.into_par_iter()
.zip(samples.axis_iter_mut(Axis(0)))
.try_for_each(|(seed, mut row)| -> Result<()> {
let mut rng = R::seed_from_u64(seed);
let sampler = ForwardSampler::new(&mut rng, self.model)?;
row.assign(&sampler.sample()?);
Ok(())
})?;
CatTable::new(self.model.support().clone(), samples)
}
}
impl<R: Rng> BNSampler<GaussBN> for ForwardSampler<'_, R, GaussBN> {
type Sample = <GaussBN as BN>::Sample;
type Samples = <GaussBN as BN>::Samples;
fn sample(&self) -> Result<Self::Sample> {
let mut rng = self.rng.borrow_mut();
let mut sample = Array::zeros(self.model.labels().len());
for &i in self.model.topological_order() {
let cpd_i = &self.model.cpds()[i];
let pa_i = self.model.graph().parents(&set![i])?;
let pa_i = pa_i.iter().map(|&z| sample[z]).collect();
sample[i] = cpd_i.sample(&mut rng, &pa_i)?[0];
}
Ok(sample)
}
fn sample_n(&self, n: usize) -> Result<Self::Samples> {
let mut samples = Array::zeros((n, self.model.labels().len()));
samples
.rows_mut()
.into_iter()
.try_for_each(|mut row| -> Result<_> {
row.assign(&self.sample()?);
Ok(())
})?;
GaussTable::new(self.model.labels().clone(), samples)
}
}
impl<R: Rng + SeedableRng> ParBNSampler<GaussBN> for ForwardSampler<'_, R, GaussBN> {
type Samples = <GaussBN as BN>::Samples;
fn par_sample_n(&self, n: usize) -> Result<Self::Samples> {
let rng = self.rng.borrow_mut();
let seeds: Vec<_> = rng.random_iter().take(n).collect();
let mut samples = Array::zeros((n, self.model.labels().len()));
seeds
.into_par_iter()
.zip(samples.axis_iter_mut(Axis(0)))
.try_for_each(|(seed, mut row)| -> Result<()> {
let mut rng = R::seed_from_u64(seed);
let sampler = ForwardSampler::new(&mut rng, self.model)?;
row.assign(&sampler.sample()?);
Ok(())
})?;
GaussTable::new(self.model.labels().clone(), samples)
}
}
macro_rules! mixed_pure_dispatch {
($self:expr, $cat:block, $gauss:block) => {{
let ok = $self
.model
.cpds()
.values()
.all(|distribution| matches!(distribution, MixedCPD::Categorical(_)));
if ok {
$cat
} else {
let ok = $self
.model
.cpds()
.values()
.all(|distribution| matches!(distribution, MixedCPD::Gaussian(_)));
if ok {
$gauss
} else {
return Err(Error::InvalidParameter(
"model",
"mixed CPD types not yet supported for forward sampling",
));
}
}
}};
}
fn extract_cat_support(
support: &crate::types::Map<String, MixedSupport>,
) -> Map<String, Set<String>> {
support
.iter()
.map(|(label, mixed_supp)| match mixed_supp {
MixedSupport::Categorical(cpd_support) => {
let states = cpd_support[label].clone();
(label.clone(), states)
}
_ => unreachable!(), })
.collect()
}
impl<R: Rng> BNSampler<MixedBN> for ForwardSampler<'_, R, MixedBN> {
type Sample = MixedSample;
type Samples = MixedTable;
fn sample(&self) -> Result<Self::Sample> {
let mut rng = self.rng.borrow_mut();
let n_vars = self.model.labels().len();
mixed_pure_dispatch!(
self,
{
let mut sample = Array1::<CatType>::zeros(n_vars);
for &i in self.model.topological_order() {
let cpd_i = &self.model.cpds()[i];
let pa_i = self.model.graph().parents(&set![i])?;
let pa_vals: Array1<CatType> = pa_i.iter().map(|&z| sample[z]).collect();
let pa_sample = MixedSample::Categorical(pa_vals);
let result = cpd_i.sample(&mut rng, &pa_sample)?;
if let MixedSample::Categorical(v) = result {
sample[i] = v[0];
}
}
Ok(MixedSample::Categorical(sample))
},
{
let mut sample = Array1::<f64>::zeros(n_vars);
for &i in self.model.topological_order() {
let cpd_i = &self.model.cpds()[i];
let pa_i = self.model.graph().parents(&set![i])?;
let pa_vals: Array1<f64> = pa_i.iter().map(|&z| sample[z]).collect();
let pa_sample = MixedSample::Gaussian(pa_vals);
let result = cpd_i.sample(&mut rng, &pa_sample)?;
if let MixedSample::Gaussian(v) = result {
sample[i] = v[0];
}
}
Ok(MixedSample::Gaussian(sample))
}
)
}
fn sample_n(&self, n: usize) -> Result<Self::Samples> {
mixed_pure_dispatch!(
self,
{
let mut dataset = Array::zeros((n, self.model.labels().len()));
dataset
.rows_mut()
.into_iter()
.try_for_each(|mut row| -> Result<_> {
if let MixedSample::Categorical(stats) = self.sample()? {
row.assign(&stats);
}
Ok(())
})?;
let cat_support = extract_cat_support(&self.model.support());
CatTable::new(cat_support, dataset).map(MixedTable::Categorical)
},
{
let mut dataset = Array::zeros((n, self.model.labels().len()));
dataset
.rows_mut()
.into_iter()
.try_for_each(|mut row| -> Result<_> {
if let MixedSample::Gaussian(stats) = self.sample()? {
row.assign(&stats);
}
Ok(())
})?;
GaussTable::new(self.model.labels().clone(), dataset).map(MixedTable::Gaussian)
}
)
}
}
impl<R: Rng + SeedableRng> ParBNSampler<MixedBN> for ForwardSampler<'_, R, MixedBN> {
type Samples = MixedTable;
fn par_sample_n(&self, n: usize) -> Result<Self::Samples> {
let rng = self.rng.borrow_mut();
let seeds: Vec<_> = rng.random_iter().take(n).collect();
mixed_pure_dispatch!(
self,
{
let mut samples = Array::zeros((n, self.model.labels().len()));
seeds
.into_par_iter()
.zip(samples.axis_iter_mut(Axis(0)))
.try_for_each(|(seed, mut row)| -> Result<()> {
let mut rng = R::seed_from_u64(seed);
let sampler = ForwardSampler::new(&mut rng, self.model)?;
if let MixedSample::Categorical(stats) = sampler.sample()? {
row.assign(&stats);
}
Ok(())
})?;
let cat_support = extract_cat_support(&self.model.support());
CatTable::new(cat_support, samples).map(MixedTable::Categorical)
},
{
let mut samples = Array::zeros((n, self.model.labels().len()));
seeds
.into_par_iter()
.zip(samples.axis_iter_mut(Axis(0)))
.try_for_each(|(seed, mut row)| -> Result<()> {
let mut rng = R::seed_from_u64(seed);
let sampler = ForwardSampler::new(&mut rng, self.model)?;
if let MixedSample::Gaussian(stats) = sampler.sample()? {
row.assign(&stats);
}
Ok(())
})?;
GaussTable::new(self.model.labels().clone(), samples).map(MixedTable::Gaussian)
}
)
}
}
impl<R: Rng> ForwardSampler<'_, R, CatCTBN> {
fn sample_time(&self, event: &CatSample, i: usize) -> Result<f64> {
let x = event[i] as usize;
let cim_i = &self.model.cims()[i];
let pa_i = self.model.graph().parents(&set![i])?;
let pa_i = pa_i.iter().map(|&z| event[z] as usize);
let pa_i = cim_i.conditioning_multi_index().ravel(pa_i);
let q_i_x = -cim_i.parameters()[[pa_i, x, x]];
let exp_i_x = Exp::new(q_i_x)
.map_err(|evidence| Error::RandDistr(&format!("Invalid lambda: {}", evidence)))?;
Ok(exp_i_x.sample(&mut self.rng.borrow_mut()))
}
}
impl<R: Rng> CTBNSampler<CatCTBN> for ForwardSampler<'_, R, CatCTBN> {
type Sample = <CatCTBN as CTBN>::Trajectory;
type Samples = <CatCTBN as CTBN>::Trajectories;
#[inline]
fn sample_by_length(&self, max_length: usize) -> Result<Self::Sample> {
self.sample_by_length_or_time(max_length, f64::MAX)
}
#[inline]
fn sample_by_time(&self, max_time: f64) -> Result<Self::Sample> {
self.sample_by_length_or_time(usize::MAX, max_time)
}
fn sample_by_length_or_time(&self, max_length: usize, max_time: f64) -> Result<Self::Sample> {
if max_length == 0 {
return Err(Error::InvalidParameter(
"max_length",
"The maximum length of the trajectory must be strictly positive.",
));
}
if max_time <= 0. {
return Err(Error::InvalidParameter("max_time", "must be positive"));
}
let mut sample_events = Vec::new();
let mut sample_times = Vec::new();
let mut event = {
let mut rng = self.rng.borrow_mut();
let initial = self.model.initial_distribution();
let initial = ForwardSampler::new(&mut rng, initial)?;
initial.sample()?
};
sample_events.push(event.clone());
sample_times.push(0.);
let mut times: Array1<_> = (0..event.len())
.map(|i| self.sample_time(&event, i))
.collect::<Result<_>>()?;
let mut i = times
.argmin()
.map_err(|evidence| Error::Stats(&format!("Failed to find min time: {}", evidence)))?;
let mut time = times[i];
while sample_events.len() < max_length && time < max_time {
let x = event[i] as usize;
let cim_i = &self.model.cims()[i];
let pa_i = self.model.graph().parents(&set![i])?;
let pa_i = pa_i.iter().map(|&z| event[z] as usize);
let pa_i = cim_i.conditioning_multi_index().ravel(pa_i);
let mut q_i_zx = cim_i.parameters().slice(s![pa_i, x, ..]).to_owned();
q_i_zx[x] = 0.;
q_i_zx /= q_i_zx.sum();
let s_i_zx = WeightedIndex::new(&q_i_zx).map_err(|evidence| {
Error::RandDistr(&format!("Invalid probabilities: {}", evidence))
})?;
event[i] = s_i_zx.sample(&mut self.rng.borrow_mut()) as CatType;
sample_events.push(event.clone());
sample_times.push(time);
for j in std::iter::once(i).chain(self.model.graph().children(&set![i])?) {
times[j] = time + self.sample_time(&event, j)?;
}
times += EPSILON;
i = times.argmin().map_err(|evidence| {
Error::Stats(&format!("Failed to find min time: {}", evidence))
})?;
time = times[i];
}
let support = self.model.support().clone();
let shape = (sample_events.len(), sample_events[0].len());
let sample_events = Array::from_iter(sample_events.into_iter().flatten())
.into_shape_with_order(shape)
.map_err(|evidence| Error::Shape(&evidence.to_string()))?;
let sample_times = Array::from_iter(sample_times);
CatTrj::new(support, sample_events, sample_times)
}
#[inline]
fn sample_n_by_length(&self, max_length: usize, n: usize) -> Result<Self::Samples> {
(0..n).map(|_| self.sample_by_length(max_length)).collect()
}
#[inline]
fn sample_n_by_time(&self, max_time: f64, n: usize) -> Result<Self::Samples> {
(0..n).map(|_| self.sample_by_time(max_time)).collect()
}
#[inline]
fn sample_n_by_length_or_time(
&self,
max_length: usize,
max_time: f64,
n: usize,
) -> Result<Self::Samples> {
(0..n)
.map(|_| self.sample_by_length_or_time(max_length, max_time))
.collect()
}
}
impl<R: Rng + SeedableRng> ParCTBNSampler<CatCTBN> for ForwardSampler<'_, R, CatCTBN> {
type Samples = <CatCTBN as CTBN>::Trajectories;
#[inline]
fn par_sample_n_by_length(&self, max_length: usize, n: usize) -> Result<Self::Samples> {
self.par_sample_n_by_length_or_time(max_length, f64::MAX, n)
}
#[inline]
fn par_sample_n_by_time(&self, max_time: f64, n: usize) -> Result<Self::Samples> {
self.par_sample_n_by_length_or_time(usize::MAX, max_time, n)
}
fn par_sample_n_by_length_or_time(
&self,
max_length: usize,
max_time: f64,
n: usize,
) -> Result<Self::Samples> {
let rng = self.rng.borrow_mut();
let seeds: Vec<_> = rng.random_iter().take(n).collect();
seeds
.into_par_iter()
.map(|seed| {
let mut rng = R::seed_from_u64(seed);
let sampler = ForwardSampler::new(&mut rng, self.model)?;
sampler.sample_by_length_or_time(max_length, max_time)
})
.collect()
}
}