fast-nnt 0.4.0

Rust implementation of the NeighborNet phylogenetic network algorithm.
Documentation
use fixedbitset::FixedBitSet;

use crate::splits::bipartition::BiPartition;
use std::fmt::{self, Display};
use std::hash::{Hash, Hasher};

/// A split with weight/confidence/label, extending `BiPartition` semantics.
/// Indices are 1-based; bit 0 is ignored.
#[derive(Debug, Clone)]
pub struct ASplit {
    pub base: BiPartition,
    pub weight: f64,
    pub confidence: f64,
    pub label: Option<String>,
}

impl ASplit {
    /* -------- constructors: A,B variants -------- */

    /// ASplit(A,B) with weight=1, confidence=-1, label=None
    pub fn new(a: FixedBitSet, b: FixedBitSet) -> Self {
        Self::new_full(a, b, 1.0, -1.0, None)
    }

    /// ASplit(A,B, weight, confidence, label)
    pub fn new_full(
        a: FixedBitSet,
        b: FixedBitSet,
        weight: f64,
        confidence: f64,
        label: Option<String>,
    ) -> Self {
        let base = BiPartition::new(a, b);
        Self {
            base,
            weight,
            confidence,
            label,
        }
    }

    /* -------- constructors: A, ntax variants (B is complement of A in 1..=ntax) -------- */

    pub fn from_a_ntax(a: FixedBitSet, ntax: usize) -> Self {
        Self::from_a_ntax_full(a, ntax, 1.0, -1.0, None)
    }

    pub fn from_a_ntax_with_weight(a: FixedBitSet, ntax: usize, weight: f64) -> Self {
        Self::from_a_ntax_full(a, ntax, weight, -1.0, None)
    }

    pub fn from_a_ntax_with_weight_conf(
        a: FixedBitSet,
        ntax: usize,
        weight: f64,
        confidence: f64,
    ) -> Self {
        Self::from_a_ntax_full(a, ntax, weight, confidence, None)
    }

    pub fn from_a_ntax_full(
        a: FixedBitSet,
        ntax: usize,
        weight: f64,
        confidence: f64,
        label: Option<String>,
    ) -> Self {
        let b = complement_1_based(&a, ntax);
        Self::new_full(a, b, weight, confidence, label)
    }

    /* -------- accessors -------- */

    pub fn label(&self) -> Option<&str> {
        self.label.as_deref()
    }

    /* -------- BiPartition delegation -------- */

    pub fn get_a(&self) -> &FixedBitSet {
        self.base.get_a()
    }
    pub fn get_b(&self) -> &FixedBitSet {
        self.base.get_b()
    }
    pub fn ntax(&self) -> usize {
        self.base.ntax()
    }
    pub fn size(&self) -> usize {
        self.base.size()
    }
    pub fn part_containing(&self, t: usize) -> &FixedBitSet {
        self.base.part_containing(t)
    }
    pub fn part_not_containing(&self, t: usize) -> &FixedBitSet {
        self.base.part_not_containing(t)
    }
    pub fn smaller_part(&self) -> &FixedBitSet {
        self.base.smaller_part()
    }
    pub fn is_trivial(&self) -> bool {
        self.base.is_trivial()
    }

    /// Access the underlying `BiPartition`.
    pub fn base(&self) -> &BiPartition {
        &self.base
    }
}

/* -------- Equality / Hash / Display -------- */

impl PartialEq for ASplit {
    /// Equality depends only on the partition, anchored at taxon 1; weight,
    /// confidence, and label are ignored.
    fn eq(&self, other: &Self) -> bool {
        same_bits_1based(self.part_containing(1), other.part_containing(1))
            && same_bits_1based(self.part_not_containing(1), other.part_not_containing(1))
    }
}
impl Eq for ASplit {}

impl Hash for ASplit {
    /// Hash only the partition, consistent with the equality semantics.
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.base.hash(state);
    }
}

impl Display for ASplit {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{} weight={} confidence={} label={}",
            self.base,
            self.weight,
            self.confidence,
            self.label.as_deref().unwrap_or("null")
        )
    }
}

/* -------- helpers (BitSet-like) -------- */

fn same_bits_1based(a: &FixedBitSet, b: &FixedBitSet) -> bool {
    // Compare membership for indices >=1 (ignore bit 0).
    // Iterate over the union of ones for speed.
    let mut seen = FixedBitSet::with_capacity(a.len().max(b.len()));
    seen.union_with(a);
    seen.union_with(b);
    for i in seen.ones() {
        if i == 0 {
            continue;
        }
        let ai = a.contains(i);
        let bi = b.contains(i);
        if ai != bi {
            return false;
        }
    }
    true
}

fn complement_1_based(a: &FixedBitSet, ntax: usize) -> FixedBitSet {
    let mut out = FixedBitSet::with_capacity(ntax + 1);
    out.grow(ntax + 1);
    for i in 1..=ntax {
        if !a.contains(i) {
            out.set(i, true);
        }
    }
    out
}

/* ---------------- Minimal adapter for split consumers ---------------- */
pub trait ASplitView {
    fn weight(&self) -> f64;
    fn size(&self) -> usize;
    fn smaller_part(&self) -> &fixedbitset::FixedBitSet;
    fn ntax(&self) -> usize;
}

impl ASplitView for ASplit {
    fn weight(&self) -> f64 {
        self.weight
    }
    fn size(&self) -> usize {
        self.size()
    }
    fn smaller_part(&self) -> &fixedbitset::FixedBitSet {
        self.smaller_part()
    }
    fn ntax(&self) -> usize {
        self.ntax()
    }
}