fast-nnt 0.4.0

Rust implementation of the NeighborNet phylogenetic network algorithm.
Documentation
use std::fmt::Write as FmtWrite;

use anyhow::{Context, Result};
use fixedbitset::FixedBitSet;
use ndarray::Array2;

use crate::data::splits_blocks::SplitsBlock;
use crate::phylo::phylo_splits_graph::{GraphEdge, PhyloSplitsGraph};

pub mod network_writer;
pub mod nexus_writer;

/* ---------- helpers ---------- */

/// Format with 7 decimal places, trimming trailing zeros.
fn fmt_f(x: f64) -> String {
    let mut s = format!("{:.7}", x);
    while s.contains('.') && s.ends_with('0') {
        s.pop();
    }
    if s.ends_with('.') {
        s.pop();
    }
    if s.is_empty() {
        s.push('0');
    }
    s
}

fn trim_float(x: f64, digits: usize) -> String {
    let mut s = format!("{:.1$}", x, digits);
    while s.ends_with('0') && s.contains('.') {
        s.pop();
    }
    if s.ends_with('.') {
        s.pop();
    }
    if s.is_empty() {
        s.push('0');
    }
    s
}

/// Convert FixedBitSet (1-based) into "i j k" string; skips bit 0 if present.
fn bitset_to_string(bs: &FixedBitSet) -> String {
    let mut out = String::new();
    let mut first = true;
    for t in bs.ones() {
        if t == 0 {
            continue;
        }
        if !first {
            out.push(' ');
        }
        first = false;
        out.push_str(&t.to_string());
    }
    out
}

fn bitset_to_vec(bs: &FixedBitSet) -> Vec<usize> {
    bs.ones().collect()
}

fn escape_label(s: &str) -> String {
    s.replace('\'', "''")
}

fn write_title_and_link<W: FmtWrite>(
    mut w: W,
    title: Option<&str>,
    link: Option<&str>,
) -> Result<()> {
    if let Some(t) = title {
        writeln!(w, "TITLE '{}';", escape_label(t))?;
    }
    if let Some(l) = link {
        writeln!(w, "LINK '{}';", escape_label(l))?;
    }
    Ok(())
}

/// (label, weight, confidence, side A taxa, side B taxa)
pub type SplitsRecord = (String, f64, f64, Vec<usize>, Vec<usize>);

pub struct Nexus {
    pub labels: Vec<String>,
    pub distance_matrix: Array2<f64>,
    pub splits: SplitsBlock,
    pub graph: PhyloSplitsGraph,
}

impl Nexus {
    pub fn new(
        labels: Vec<String>,
        distance_matrix: Array2<f64>,
        splits: SplitsBlock,
        graph: PhyloSplitsGraph,
    ) -> Self {
        Nexus {
            labels,
            distance_matrix,
            splits,
            graph,
        }
    }

    pub fn get_labels(&self) -> &[String] {
        &self.labels
    }

    pub fn num_labels(&self) -> usize {
        self.labels.len()
    }

    pub fn distance_matrix(&self) -> &Array2<f64> {
        &self.distance_matrix
    }

    pub fn splits(&self) -> &SplitsBlock {
        &self.splits
    }

    pub fn num_splits(&self) -> usize {
        self.splits.nsplits()
    }

    pub fn graph(&self) -> &PhyloSplitsGraph {
        &self.graph
    }

    pub fn get_splits_records(&self) -> Vec<SplitsRecord> {
        self.splits
            .get_splits()
            .iter()
            .map(|s| {
                (
                    s.label().unwrap_or("").to_string(),
                    s.weight,
                    s.confidence,
                    bitset_to_vec(s.get_a()),
                    bitset_to_vec(s.get_b()),
                )
            })
            .collect()
    }

    pub fn get_node_translations(&self) -> Result<Vec<(usize, String)>> {
        self.graph
            .get_node_translations(&self.labels)
            .context("Failed to get node translations")
    }

    pub fn get_node_positions(&self) -> Result<Vec<(usize, f64, f64)>> {
        self.graph
            .get_node_positions()
            .context("Failed to get node positions")
    }

    pub fn get_graph_edges(&self) -> Result<Vec<GraphEdge>> {
        self.graph
            .get_graph_edges()
            .context("Failed to get graph edges")
    }
}