use std::fmt::{self, Write as FmtWrite};
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use anyhow::{Context, Result, anyhow};
use ndarray::Array2;
use crate::algorithms::equal_angle::assign_coordinates_to_nodes;
use crate::data::splits_blocks::{Compatibility, SplitsBlock};
use crate::nexus::{
bitset_to_string, escape_label, fmt_f, network_writer::write_network_block_splits, trim_float,
write_title_and_link,
};
use crate::phylo::phylo_splits_graph::PhyloSplitsGraph;
#[derive(Default, Clone)]
pub struct NexusProperties {
pub splits_properties: Option<String>,
pub total_angle_deg: Option<f64>,
pub network_root_split: Option<i32>,
pub distances_triangle_both: bool,
}
struct IoFmt<W: Write>(W);
impl<W: Write> FmtWrite for IoFmt<W> {
fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
self.0.write_all(s.as_bytes()).map_err(|_| fmt::Error)
}
}
pub fn write_nexus_all_to_path<P: AsRef<Path>>(
path: P,
taxa_labels: &[String],
distances: Option<&Array2<f64>>,
splits_block: Option<&SplitsBlock>,
cycle_1_based: Option<&[usize]>, graph: Option<&PhyloSplitsGraph>,
fit_percent: Option<f64>, props: NexusProperties,
) -> anyhow::Result<()> {
let file = File::create(&path)
.with_context(|| format!("failed to create NEXUS file at {}", path.as_ref().display()))?;
let buf = BufWriter::new(file);
write_nexus_all_to_writer(
buf,
taxa_labels,
distances,
splits_block,
cycle_1_based,
graph,
fit_percent,
props,
)
.with_context(|| format!("failed while writing NEXUS to {}", path.as_ref().display()))?;
Ok(())
}
pub fn write_nexus_all_to_writer<W: Write>(
mut sink: W,
taxa_labels: &[String],
distances: Option<&Array2<f64>>,
splits_block: Option<&SplitsBlock>,
cycle_1_based: Option<&[usize]>, graph: Option<&PhyloSplitsGraph>,
_fit_percent: Option<f64>, props: NexusProperties,
) -> anyhow::Result<()> {
let mut out = IoFmt(&mut sink);
debug!(
"Writing NEXUS with {} taxa, {} splits, distances: {:?}, cycle: {:?}, graph: {:?}",
taxa_labels.len(),
splits_block.map_or(0, |s| s.nsplits()),
distances.is_some(),
cycle_1_based,
graph.is_some()
);
debug!("Writing header.");
write_header(&mut out)?;
debug!("Writing taxa block.");
write_taxa_block(&mut out, taxa_labels)?;
if let Some(dm) = distances {
debug!("Writing distances block.");
write_distances_block(&mut out, dm, props.distances_triangle_both)?;
}
if let Some(sp) = splits_block {
debug!("Writing splits block with {} splits.", sp.nsplits());
write_splits_block(
&mut out,
taxa_labels.len(),
sp,
props.splits_properties.as_deref(),
None, )?;
}
if let Some(g) = graph {
debug!("Assigning coordinates to nodes.");
let coords = assign_coordinates_to_nodes(true, g, 1, props.network_root_split.unwrap_or(0));
debug!("Writing network block.");
write_network_block_splits(&mut out, g, &coords, taxa_labels)?;
}
write_st_assumptions_block(&mut out)?;
Ok(())
}
pub fn write_header<W: FmtWrite>(mut w: W) -> Result<()> {
writeln!(w, "#nexus\n")?;
Ok(())
}
pub fn write_taxa_block<W: FmtWrite>(mut w: W, taxa_labels: &[String]) -> Result<()> {
let ntax = taxa_labels.len();
writeln!(w, "BEGIN Taxa;")?;
writeln!(w, "DIMENSIONS ntax={};", ntax)?;
writeln!(w, "TAXLABELS")?;
for (i, name) in taxa_labels.iter().enumerate() {
writeln!(w, "[{}] '{}'", i + 1, escape_label(name))?;
}
writeln!(w, "\n;")?;
writeln!(w, "END; [Taxa]\n")?;
Ok(())
}
pub fn write_distances_block<W: FmtWrite>(
mut w: W,
matrix: &ndarray::Array2<f64>,
triangle_both_format: bool,
) -> Result<()> {
let (n, m) = (matrix.nrows(), matrix.ncols());
if n != m {
return Err(anyhow!("Distances matrix must be square"));
}
writeln!(w, "BEGIN Distances;")?;
writeln!(w, "DIMENSIONS ntax={};", n)?;
if triangle_both_format {
writeln!(w, "FORMAT labels=no diagonal triangle=both;")?;
} else {
writeln!(w, "FORMAT labels=no diagonal;")?;
}
writeln!(w, "MATRIX")?;
for i in 0..n {
let mut first = true;
for j in 0..m {
if !first {
write!(w, " ")?;
}
first = false;
write!(w, "{}", fmt_f(matrix[[i, j]]))?;
}
writeln!(w)?;
}
writeln!(w, ";")?;
writeln!(w, "END; [Distances]\n")?;
Ok(())
}
pub fn write_splits_block<W: FmtWrite>(
mut w: W,
ntax: usize,
splits_block: &SplitsBlock,
title: Option<&str>, link: Option<&str>, ) -> Result<()> {
let nsplits = splits_block.nsplits();
let format = splits_block.format();
let write_confidences = splits_block.has_confidence_values();
writeln!(w, "\nBEGIN SPLITS;")?;
write_title_and_link(&mut w, title, link)?;
writeln!(w, "DIMENSIONS ntax={} nsplits={};", ntax, nsplits)?;
write!(w, "FORMAT")?;
if format.labels {
write!(w, " labels=left")?;
} else {
write!(w, " labels=no")?;
}
if format.weights {
write!(w, " weights=yes")?;
} else {
write!(w, " weights=no")?;
}
if write_confidences {
write!(w, " confidences=yes")?;
} else {
write!(w, " confidences=no")?;
}
if format.show_both_sides {
write!(w, " showBothSides=yes")?;
} else {
write!(w, " showBothSides=no")?;
}
writeln!(w, ";")?;
if splits_block.threshold() != 0.0 {
writeln!(
w,
"THRESHOLD={};",
trim_float(splits_block.threshold() as f64, 8)
)?;
}
write!(
w,
"PROPERTIES fit={}",
trim_float(splits_block.fit() as f64, 2)
)?;
match splits_block.compatibility() {
Compatibility::Compatible => write!(w, " compatible")?,
Compatibility::Cyclic => write!(w, " cyclic")?,
Compatibility::WeaklyCompatible => write!(w, " weakly compatible")?,
Compatibility::Incompatible => write!(w, " non compatible")?,
Compatibility::Unknown => { }
}
writeln!(w, ";")?;
if let Some(cycle) = splits_block.cycle() {
write!(w, "CYCLE")?;
for i in 1..cycle.len() {
write!(w, " {}", cycle[i])?;
}
writeln!(w, ";")?;
}
if !splits_block.split_labels().is_empty() {
write!(w, "SPLITSLABELS")?;
for (_sid, label) in splits_block.split_labels().iter() {
write!(w, " '{}'", escape_label(label))?;
}
writeln!(w, ";")?;
}
writeln!(w, "MATRIX")?;
{
let mut t = 1usize; for split in splits_block.splits() {
write!(w, "[{}, size={}] \t", t, split.size())?;
if format.labels {
let lab = split.get_label().unwrap_or("");
write!(w, " '{}' \t", escape_label(lab))?;
}
if format.weights {
write!(w, " {} \t", trim_float(split.weight, 8))?;
}
if write_confidences {
write!(w, " {} \t", trim_float(split.get_confidence(), 8))?;
}
write!(w, " {}", bitset_to_string(split.get_a()))?;
if format.show_both_sides {
write!(w, " | {}", bitset_to_string(split.get_b()))?;
}
writeln!(w, ",")?;
t += 1;
}
}
writeln!(w, ";")?;
writeln!(w, "END; [SPLITS]")?;
Ok(())
}
pub fn write_st_assumptions_block<W: FmtWrite>(mut w: W) -> Result<()> {
writeln!(w, "BEGIN st_Assumptions;")?;
writeln!(w, "uptodate;")?;
writeln!(w, "disttransform=NeighborNet;")?;
writeln!(w, "splitstransform=EqualAngle;")?;
writeln!(w, "SplitsPostProcess filter=dimension value=4;")?;
writeln!(w, " exclude no missing;")?;
writeln!(w, "autolayoutnodelabels;")?;
writeln!(w, "END; [st_Assumptions]\n")?;
Ok(())
}