nbslim 0.1.0

Rust implementation of SIA, SIATEC, COSIATEC, and RecurSIA algorithms for compressing Note Block Studio (.nbs) music files by discovering translational equivalence classes in 2D point sets.
Documentation
use std::collections::HashSet;

use crate::sia::find_mtps;
use crate::tec::TranslationalEquivalence;

/// Builds translational equivalence classes (TECs) for every maximal translatable pattern (MTP)
/// found in the dataset by the SIA algorithm.
///
/// For each MTP (represented by a translation vector `v` and its pattern `P`), this function
/// determines all translation vectors `w` such that `P + w` is entirely contained in the dataset.
/// These `w`s become the TEC's translator set. The TEC is then stored as `⟨P, translators⟩`.
///
/// # Arguments
/// * `dataset` - A reference to the vector of points `(tick, pitch)`.
/// * `restrict_dpitch_zero` - If `true`, only translation vectors with zero pitch difference
///   are accepted (purely temporal shifts).
///
/// # Returns
/// A vector of `TranslationalEquivalence` objects, one per distinct MTP (i.e., per non‑zero
/// translation vector `v`). Patterns that have no translators (other than the trivial zero vector)
/// are omitted.
///
/// # Algorithmic notes
/// - For each MTP pattern `P`, the candidate translators are all vectors from the first pattern
///   point `p0` to any point in the dataset. Only those that map the whole `P` into the dataset
///   are kept.
/// - Complexity: `O(m · n)` where `m` is the number of MTPs and `n` is the dataset size.
///   In the worst case `m = O(n²)`.
pub fn build_tecs_from_mtps(dataset: &Vec<(u32, u32)>, restrict_dpitch_zero: bool) -> Vec<TranslationalEquivalence> {
    // Store dataset as i64 HashSet for easy containment checks
    let points_set: HashSet<(u32, u32)> = dataset.iter().copied().collect();
    let mtps = find_mtps(dataset, restrict_dpitch_zero); // returns Vec<((i32,i32), Vec<(u32,u32)>)>
    let mut tecs = Vec::new();

    for (v, start_pts) in mtps {
        if v == (0, 0) {
            continue;
        }
        // pattern is the set of points that can be translated by v
        let pattern = start_pts; // already sorted and deduped by sia
        if pattern.is_empty() {
            continue;
        }

        let p0 = pattern[0]; // (u32, u32)
        // Collect all candidate translation vectors w = q - p0  (signed differences)
        let candidates: HashSet<(i32, i32)> = points_set
            .iter()
            .map(|&(qx, qy)| ((qx as i64 - p0.0 as i64) as i32, (qy as i64 - p0.1 as i64) as i32))
            .collect();

        let mut translators = HashSet::<(i32, i32)>::new();
        for w in candidates {
            if w == (0, 0) {
                continue;
            }
            if restrict_dpitch_zero && w.1 != 0 {
                continue;
            }
            // Check if pattern + w is fully contained in dataset
            let ok = pattern.iter().all(|&(px, py)| {
                let tx = (px as i32 + w.0) as u32;
                let ty = (py as i32 + w.1) as u32;
                points_set.contains(&(tx, ty))
            });
            if ok {
                translators.insert(w);
            }
        }

        if !translators.is_empty() {
            let tec = TranslationalEquivalence::new(pattern, translators, None);
            tecs.push(tec);
        }
    }

    tecs
}