#[cfg(feature = "ndarray")]
use burn::backend::ndarray::NdArray;
#[cfg(any(feature = "wgpu", feature = "webgpu", feature = "vulkan"))]
use burn::backend::wgpu::Wgpu;
use burn_tensor::{Device, Tensor};
use burn_tensor::backend::Backend;
use num::traits::{Float, PrimInt};
use num::FromPrimitive;
#[cfg(feature = "cxxbridge")]
pub mod cxx_api;
pub mod math;
pub mod optimizer;
use optimizer::Algorithm;
type E = Box<dyn std::error::Error>;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum BurnBackend {
Wgpu32,
Wgpu64,
#[default]
NdArray32,
NdArray64,
}
impl std::str::FromStr for BurnBackend {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"cpu32" => Ok(BurnBackend::NdArray32),
"cpu64" => Ok(BurnBackend::NdArray64),
"gpu32" => Ok(BurnBackend::Wgpu32),
"gpu64" => Ok(BurnBackend::Wgpu64),
_ => Err(format!("'{}' is not a valid BurnBackend variant", s)),
}
}
}
impl std::fmt::Display for BurnBackend {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
BurnBackend::NdArray32 => write!(f, "cpu32"),
BurnBackend::NdArray64 => write!(f, "cpu64"),
BurnBackend::Wgpu32 => write!(f, "gpu32"),
BurnBackend::Wgpu64 => write!(f, "gpu64"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct OptimizerOpts {
pub tolerance: f64,
pub max_iters: usize,
pub device: BurnBackend,
pub algorithm: Algorithm,
}
impl Default for OptimizerOpts {
fn default() -> OptimizerOpts {
OptimizerOpts {
tolerance: 1e-7_f64,
max_iters: 5000_usize,
device: BurnBackend::NdArray32,
algorithm: Algorithm::RCG,
}
}
}
pub fn run_optimizer<B: Backend>(
logl_f: &[f32],
log_counts_f: &[f32],
alpha0_f: &[f32],
options: &OptimizerOpts,
device: &Device<B>,
) -> Result<(Vec<f32>, Vec<f32>), E> {
let n_rows = log_counts_f.len();
let n_cols = alpha0_f.len();
let logl_flat = Tensor::<B, 1>::from_data(logl_f, device);
let log_counts = Tensor::<B, 1>::from_data(log_counts_f, device);
let alpha0 = Tensor::<B, 1>::from_data(alpha0_f, device);
let logl = logl_flat.reshape([n_cols, n_rows]);
let (alpha0, logl) = optimize_tensor::<B>(logl, log_counts, alpha0, options)?;
match options.device {
#[cfg(feature = "ndarray")]
BurnBackend::NdArray32 => {
Ok((alpha0.into_data().to_vec().unwrap(), logl.into_data().to_vec().unwrap()))
},
#[cfg(feature = "ndarray")]
BurnBackend::NdArray64 => {
Ok((alpha0.into_data().to_vec().unwrap().into_iter().map(|x: f64| x as f32).collect::<Vec<f32>>(),
logl.into_data().to_vec().unwrap().into_iter().map(|x: f64| x as f32).collect::<Vec<f32>>()))
},
#[cfg(any(feature = "wgpu", feature = "webgpu", feature = "vulkan"))]
BurnBackend::Wgpu32 => {
Ok((alpha0.into_data().to_vec().unwrap(), logl.into_data().to_vec().unwrap()))
},
#[cfg(any(feature = "wgpu", feature = "webgpu", feature = "vulkan"))]
BurnBackend::Wgpu64 => {
Ok((alpha0.into_data().to_vec().unwrap().into_iter().map(|x: f64| x as f32).collect::<Vec<f32>>(),
logl.into_data().to_vec().unwrap().into_iter().map(|x: f64| x as f32).collect::<Vec<f32>>()))
},
#[cfg(not(any(feature = "wgpu", feature = "webgpu", feature = "vulkan")))]
BurnBackend::Wgpu32 | BurnBackend::Wgpu64 => panic!("mixt was not compiled with WGPU support, recompile with `--features wgpu` to enable."),
#[cfg(not(feature = "ndarray"))]
BurnBackend::NdArray32 | BurnBackend::NdArray64 => panic!("mixt was not compiled with NdArray support, recompile with `--features ndarray` to enable."),
}
}
pub fn optimize_tensor<B: Backend>(
log_likelihood: Tensor::<B, 2>,
log_counts: Tensor::<B, 1>,
alpha0: Tensor::<B, 1>,
options: &OptimizerOpts,
) -> Result<(Tensor::<B, 1>, Tensor::<B, 2>), E> {
let probs = match options.algorithm {
optimizer::Algorithm::RCG => optimizer::rcg::rcg_optl_mat(log_likelihood, log_counts.clone(), alpha0, options.tolerance, options.max_iters)?,
optimizer::Algorithm::EM => optimizer::em::em_algorithm(log_likelihood, log_counts.clone(), options.tolerance, options.max_iters)?,
};
let proportions = optimizer::mixture_components(probs.clone(), log_counts);
Ok((proportions, probs))
}
pub fn optimize_flat(
log_likelihood: &[f32],
log_counts: &[f32],
prior: &[f32],
opts: Option<OptimizerOpts>,
) -> Result<(Vec<f32>, Vec<f32>), E> {
assert_eq!(log_likelihood.len() as u64, (log_counts.len() as u64) * (prior.len() as u64));
let options = opts.unwrap_or_default();
let (proportions, probs_mat) = match options.device {
#[cfg(feature = "ndarray")]
BurnBackend::NdArray32 => {
let device = burn::backend::ndarray::NdArrayDevice::default();
type Backend = NdArray<f32>;
run_optimizer::<Backend>(log_likelihood, log_counts, prior, &options, &device)?
},
#[cfg(feature = "ndarray")]
BurnBackend::NdArray64 => {
let device = burn::backend::ndarray::NdArrayDevice::default();
type Backend = NdArray<f64>;
run_optimizer::<Backend>(log_likelihood, log_counts, prior, &options, &device)?
},
#[cfg(any(feature = "wgpu", feature = "webgpu", feature = "vulkan"))]
BurnBackend::Wgpu32 => {
let device = burn::backend::wgpu::WgpuDevice::default();
type Backend = Wgpu<f32>;
run_optimizer::<Backend>(log_likelihood, log_counts, prior, &options, &device)?
},
#[cfg(any(feature = "wgpu", feature = "webgpu", feature = "vulkan"))]
BurnBackend::Wgpu64 => {
let device = burn::backend::wgpu::WgpuDevice::default();
type Backend = Wgpu<f64>;
run_optimizer::<Backend>(log_likelihood, log_counts, prior, &options, &device)?
},
#[cfg(not(any(feature = "wgpu", feature = "webgpu", feature = "vulkan")))]
BurnBackend::Wgpu32 | BurnBackend::Wgpu64 => panic!("mixt was not compiled with WGPU support, recompile with `--features wgpu` to enable."),
#[cfg(not(feature = "ndarray"))]
BurnBackend::NdArray32 | BurnBackend::NdArray64 => panic!("mixt was not compiled with NdArray support, recompile with `--features ndarray` to enable."),
};
Ok((proportions, probs_mat))
}
pub fn optimize<F: Float + FromPrimitive, U: PrimInt>(
log_likelihood: &[Vec<F>],
counts: &[U],
prior: &[F],
opts: Option<OptimizerOpts>,
) -> Result<(Vec<f32>, Vec<f32>), E> {
let logl_flat = log_likelihood.iter().flatten().map(|x| x.to_f32().unwrap()).collect::<Vec<f32>>();
let log_counts = counts.iter().map(|x| x.to_f32().unwrap().ln()).collect::<Vec<f32>>();
let alpha0 = prior.iter().map(|x| x.to_f32().unwrap()).collect::<Vec<f32>>();
optimize_flat(&logl_flat, &log_counts, &alpha0, opts)
}
#[cfg(test)]
mod tests {
use assert_approx_eq::assert_approx_eq;
#[test]
fn optimize() {
use super::BurnBackend;
use super::OptimizerOpts;
use super::optimize;
use super::optimizer::Algorithm;
let log_likelihood: Vec<Vec<f32>> =
vec![
vec![ -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.0100503 ],
vec![ -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.0100503, -0.371713 ],
vec![ -0.0100503, -0.0100503, -0.0100503, -0.371713, -0.371713, -0.371713, -4.60517, -4.60517, -4.60517, -0.0100503 ],
vec![ -0.0100503, -0.371713, -4.60517, -0.0100503, -0.371713, -4.60517, -0.0100503, -0.371713, -4.60517, -0.0100503 ],
];
let counts: Vec<u32> = vec![2167, 1145, 943, 196, 175, 158, 1041, 957, 1447, 2135];
let prior_counts: Vec<f32> = vec![1.0, 1.0, 1.0, 1.0];
let expected: Vec<f32> = vec![0.9990609232614853, 0.0007300889486079688, 9.656361438673255e-5, 0.00011242417552052694];
let opts = OptimizerOpts { tolerance: 1e-7_f64, max_iters: 100, device: BurnBackend::NdArray32, algorithm: Algorithm::RCG };
let (got, _) = optimize(&log_likelihood, &counts, &prior_counts, Some(opts)).unwrap();
got.iter().zip(expected.iter()).for_each(|(x, y)| { assert_approx_eq!(x, y, 1e-4) });
}
}