mod block;
mod block_chart;
mod block_scoring_gpu;
mod block_stream;
mod codes;
mod cofit;
mod cofit_arrow;
mod coordinate;
#[cfg(target_os = "linux")]
mod decoder_gpu;
mod scoring;
#[cfg(target_os = "linux")]
mod scoring_gpu;
mod split_lr_fdr;
mod stream;
mod update;
#[cfg(test)]
mod tests;
pub use block::{
BlockSeedPolicy, BlockSparseConfig, BlockSparseConvergence, BlockSparseFit,
BlockSparseFitError, block_gates, block_projections_row, block_sparse_dictionary_block_coords,
block_sparse_dictionary_lift_block, block_sparse_dictionary_project_residual,
block_sparse_dictionary_transform, coordinate_partition_frames, fit_block_sparse_dictionary,
fit_block_sparse_dictionary_with_seed, reconstruct_block_sparse_rows, reconstruct_row,
route_row_blocks, row_loss,
};
pub use block_chart::{
BlockChartComposeConfig, BlockChartComposeResult, BlockChartRecord, BlockSeedManifest,
BlockSeedManifestConfig, BlockSeedRecord, CHART_FDR_ALPHA, ChartEvidence, MdlFeaturizerRow,
block_sparse_dictionary_firings, block_sparse_dictionary_seed_manifest,
compose_block_coordinate_charts,
};
pub use block_scoring_gpu::{
BlockRoutePath, block_gate_block_cpu, block_gate_row_cpu, route_blocks_cpu,
};
#[cfg(target_os = "linux")]
pub use block_scoring_gpu::{DEVICE_BLOCK_GATE_MIN_ELEMS, route_blocks_required};
pub use block_stream::{
BlockEpochStats, BlockShardStats, BlockSparseStreamArtifact, BlockSparseStreamState,
};
pub use codes::SparseCode;
pub use cofit::{CofitConfig, CofitReport, CofitRound, cofit_block_and_curved};
pub use cofit_arrow::{
ArrowCofitConfig, ArrowCofitReport, cofit_composed_via_arrow, cofit_linear_via_arrow,
};
pub use coordinate::{
BlockCoordinateReport, BlockMeasureCoordinateReport, FiringCoordinate, MeasureSpikeCoordinate,
MeasureValuedCode, block_firing_coordinates, block_measure_valued_codes,
block_route_firing_coordinates, explained_variance_from_reconstruction,
harmonic_firing_coordinates, harmonic_measure_coordinates, harmonic_route_firing_coordinates,
reconstruct_measure_valued_rows, reconstruct_single_coordinate_rows, recover_measure_from_code,
};
pub use scoring::{ScoreRoutePath, ScoreRouteResult, ScoreRouteStats, TileScorer, top_s_online};
#[cfg(target_os = "linux")]
pub use scoring_gpu::{
DEVICE_SCORE_BLOCK_MIN_ELEMS, ScoreBlockPath, score_block_cpu, score_block_required,
};
pub use split_lr_fdr::{
FdrCertificate, crossfit_ui_log_evalue, family_fdr_certificate, shell_vs_ring_log_evalue,
};
pub use stream::{EpochStats, ShardStats, SparseDictArtifact, SparseDictStreamState};
pub use update::{
DecoderSolveStats, LinearBlockRemlStats, SparseDictionaryError, linear_block_reml_stats,
linear_shared_rho_fs_step,
};
use ndarray::{Array2, ArrayView2};
#[derive(Clone, Copy, Debug)]
pub struct SparseDictConfig {
pub n_atoms: usize,
pub active: usize,
pub minibatch: usize,
pub max_epochs: usize,
pub score_tile: usize,
pub code_ridge: f32,
pub decoder_ridge: f32,
pub tolerance: f64,
pub score_mode: gam_gpu::GpuPolicy,
}
impl SparseDictConfig {
pub fn new(n_atoms: usize) -> Self {
Self {
n_atoms,
..Self::default()
}
}
}
impl Default for SparseDictConfig {
fn default() -> Self {
Self {
n_atoms: 1,
active: 1,
minibatch: 512,
max_epochs: 30,
score_tile: 4096,
code_ridge: 1.0e-6,
decoder_ridge: 1.0e-6,
tolerance: 1.0e-6,
score_mode: gam_gpu::GpuPolicy::Auto,
}
}
}
#[derive(Clone, Debug)]
pub struct SparseDictFit {
pub decoder: Array2<f32>,
pub indices: Array2<u32>,
pub codes: Array2<f32>,
pub explained_variance: f64,
pub epochs: usize,
pub convergence: SparseDictConvergence,
pub active: usize,
pub score_route_stats: ScoreRouteStats,
pub decoder_solve_stats: DecoderSolveStats,
}
#[derive(Clone, Copy, Debug)]
pub struct SparseDictConvergence {
pub inner_ev_residual: f64,
pub inner_tolerance: f64,
pub decoder_residual: f64,
pub decoder_tolerance: f64,
pub routing_residual: f64,
pub routing_tolerance: f64,
pub outer_rho_residual: f64,
pub outer_tolerance: f64,
pub selected_rho: f64,
pub outer_iterations: usize,
pub certified: bool,
}
impl SparseDictConvergence {
pub fn trivially_converged() -> Self {
Self {
inner_ev_residual: 0.0,
inner_tolerance: 1e-6,
decoder_residual: 0.0,
decoder_tolerance: 1e-6,
routing_residual: 0.0,
routing_tolerance: 1e-6,
outer_rho_residual: 0.0,
outer_tolerance: 1e-6,
selected_rho: f64::INFINITY,
outer_iterations: 0,
certified: true,
}
}
}
impl SparseDictFit {
pub fn reconstruct(&self) -> Array2<f32> {
reconstruct_sparse_rows(self.decoder.view(), self.indices.view(), self.codes.view())
.expect("SparseDictFit stores internally validated routing")
}
}
pub fn reconstruct_sparse_rows(
decoder: ArrayView2<'_, f32>,
indices: ArrayView2<'_, u32>,
codes: ArrayView2<'_, f32>,
) -> Result<Array2<f32>, String> {
if indices.dim() != codes.dim() {
return Err(format!(
"reconstruct_sparse_rows: indices shape {:?} does not match codes shape {:?}",
indices.dim(),
codes.dim()
));
}
let n = indices.nrows();
let p = decoder.ncols();
let mut out = Array2::<f32>::zeros((n, p));
for i in 0..n {
for j in 0..indices.ncols() {
let atom = indices[[i, j]] as usize;
if atom >= decoder.nrows() {
return Err(format!(
"reconstruct_sparse_rows: atom index {atom} out of range 0..{}",
decoder.nrows()
));
}
let code = codes[[i, j]];
if code == 0.0 {
continue;
}
let row = decoder.row(atom);
for c in 0..p {
out[[i, c]] += code * row[c];
}
}
}
Ok(out)
}
#[derive(Clone, Debug)]
pub struct SparseDictTransform {
pub indices: Array2<u32>,
pub codes: Array2<f32>,
pub score_route_stats: ScoreRouteStats,
}
pub fn sparse_dictionary_transform(
x: ArrayView2<'_, f32>,
decoder: ArrayView2<'_, f32>,
active: usize,
score_tile: usize,
code_ridge: f32,
) -> Result<(Array2<u32>, Array2<f32>), String> {
let transform = sparse_dictionary_transform_with_mode(
x,
decoder,
active,
score_tile,
code_ridge,
gam_gpu::global_policy(),
)?;
Ok((transform.indices, transform.codes))
}
pub fn sparse_dictionary_transform_with_mode(
x: ArrayView2<'_, f32>,
decoder: ArrayView2<'_, f32>,
active: usize,
score_tile: usize,
code_ridge: f32,
score_mode: gam_gpu::GpuPolicy,
) -> Result<SparseDictTransform, String> {
let k = decoder.nrows();
if k == 0 {
return Err("sparse_dictionary_transform: dictionary has no atoms".to_string());
}
if x.ncols() != decoder.ncols() {
return Err(format!(
"sparse_dictionary_transform: X has P={} columns but the decoder has P={}",
x.ncols(),
decoder.ncols()
));
}
let s = active.min(k).max(1);
let scorer = TileScorer::new(s, score_tile.max(1));
let routed = scorer.route_minibatch_with_mode(x, decoder, score_mode)?;
let mut score_route_stats = ScoreRouteStats::default();
score_route_stats.record_result(&routed);
let m = x.nrows();
let mut indices = Array2::<u32>::zeros((m, s));
let mut codes = Array2::<f32>::zeros((m, s));
for (row_idx, active_pairs) in routed.selections.iter().enumerate() {
let code = codes::solve_row_codes(x.row(row_idx), decoder, active_pairs, s, code_ridge);
for j in 0..s {
indices[[row_idx, j]] = code.indices[j];
codes[[row_idx, j]] = code.codes[j];
}
}
Ok(SparseDictTransform {
indices,
codes,
score_route_stats,
})
}
pub fn fit_sparse_dictionary(
x: ArrayView2<'_, f32>,
config: &SparseDictConfig,
) -> Result<SparseDictFit, SparseDictionaryError> {
update::run_linear_reml_schedule(x, config)
}