fast-nnt 0.4.0

Rust implementation of the NeighborNet phylogenetic network algorithm.
Documentation
use std::collections::BTreeMap;

use anyhow::Result;
use fixedbitset::FixedBitSet;

use crate::algorithms::equal_angle::normalize_cycle_1based;
use crate::splits::asplit::ASplit;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Compatibility {
    #[default]
    Unknown,
    Compatible,
    Cyclic,
    WeaklyCompatible,
    Incompatible,
}

#[derive(Debug, Clone, Default)]
pub struct SplitsFormat {
    pub labels: bool,
    pub weights: bool,
    pub confidences: bool,
    pub intervals: bool,
    pub show_both_sides: bool,
}

/// Container of circular splits for Equal-Angle / NEXUS writing.
#[derive(Debug, Clone)]
pub struct SplitsBlock {
    pub splits: Vec<ASplit>, // 1-based access via get(i)
    compatibility: Compatibility,
    fit: f32,
    threshold: f32,
    cycle: Option<Vec<usize>>, // 1-based: [0, t1, ..., tn]
    split_labels: BTreeMap<usize, String>,
    format: SplitsFormat,
}

impl SplitsBlock {
    pub fn new() -> Self {
        Self {
            splits: Vec::new(),
            compatibility: Compatibility::Unknown,
            fit: -1.0,
            threshold: 0.0,
            cycle: None,
            split_labels: BTreeMap::new(),
            format: SplitsFormat {
                labels: false,
                weights: true,
                confidences: false,
                intervals: false,
                show_both_sides: false,
            },
        }
    }

    pub fn clear(&mut self) {
        self.splits.clear();
        self.compatibility = Compatibility::Unknown;
        self.fit = -1.0;
        self.threshold = 0.0;
        self.cycle = None;
        self.split_labels.clear();
        self.format = SplitsFormat::default();
    }

    pub fn set_splits(&mut self, splits: Vec<ASplit>) {
        self.splits = splits;
    }

    pub fn nsplits(&self) -> usize {
        self.splits.len()
    }
    pub fn splits(&self) -> impl Iterator<Item = &ASplit> {
        self.splits.iter()
    }
    pub fn get_splits(&self) -> &[ASplit] {
        &self.splits
    }

    /// 1-based accessor; panics if out of range.
    pub fn get(&self, i: usize) -> &ASplit {
        &self.splits[i - 1]
    }

    /// Push a split; returns its 1-based id.
    pub fn push(&mut self, s: ASplit) -> usize {
        self.splits.push(s);
        self.splits.len()
    }

    pub fn set_compatibility(&mut self, c: Compatibility) {
        self.compatibility = c;
    }
    pub fn compatibility(&self) -> Compatibility {
        self.compatibility
    }

    pub fn set_fit(&mut self, fit: f32) {
        self.fit = fit;
    }
    pub fn fit(&self) -> f32 {
        self.fit
    }

    pub fn set_threshold(&mut self, thr: f32) {
        self.threshold = thr;
    }
    pub fn threshold(&self) -> f32 {
        self.threshold
    }

    pub fn format(&self) -> &SplitsFormat {
        &self.format
    }
    pub fn format_mut(&mut self) -> &mut SplitsFormat {
        &mut self.format
    }

    pub fn split_labels(&self) -> &BTreeMap<usize, String> {
        &self.split_labels
    }
    pub fn split_labels_mut(&mut self) -> &mut BTreeMap<usize, String> {
        &mut self.split_labels
    }
    pub fn set_split_label<S: Into<String>>(&mut self, sid: usize, label: S) {
        self.split_labels.insert(sid, label.into());
    }

    /// Get 1-based cycle; returns `None` if not set.
    /// This is the `[0, t1, ..., tn]` representation expected by equal-angle.
    pub fn cycle(&self) -> Option<&[usize]> {
        self.cycle.as_deref()
    }

    /// Set a **1-based** cycle `[0, t1..tn]`. If `normalize=true`, rotate so that `cycle[1]==1`.
    pub fn set_cycle(&mut self, mut cycle: Vec<usize>, normalize: bool) -> Result<()> {
        if normalize {
            cycle = normalize_cycle_1based(&cycle)?;
        }
        // Ensure it's a permutation (rough check).
        let mut seen = FixedBitSet::with_capacity(cycle.len() + 1);
        for &v in &cycle {
            if v >= seen.len() {
                seen.grow(v + 1);
            }
            if seen.contains(v) {
                // duplicate
                self.cycle = None;
                return Err(anyhow::anyhow!("cycle is not a permutation"));
            }
            seen.insert(v);
        }
        self.cycle = Some(cycle);
        Ok(())
    }

    pub fn has_confidence_values(&self) -> bool {
        self.splits.iter().any(|s| s.confidence != -1.0)
    }
}

impl Default for SplitsBlock {
    fn default() -> Self {
        Self::new()
    }
}

/* ---------- Equal-Angle adapter ---------- */
pub trait SplitsProvider {
    fn nsplits(&self) -> usize;
    fn split(&self, id: usize) -> &ASplit;
    fn cycle(&self) -> Option<&[usize]>;
}

impl SplitsProvider for SplitsBlock {
    fn nsplits(&self) -> usize {
        self.nsplits()
    }
    fn split(&self, id: usize) -> &ASplit {
        self.get(id)
    }
    fn cycle(&self) -> Option<&[usize]> {
        self.cycle()
    }
}