use anyhow::{Result, anyhow, bail};
use fixedbitset::FixedBitSet;
use petgraph::{
csr::IndexType,
prelude::{EdgeIndex, NodeIndex},
visit::{EdgeRef, NodeIndexable},
};
use std::collections::{HashMap, HashSet};
use crate::{
algorithms::convex_hull::convex_hull_apply,
data::splits_blocks::{SplitsBlock, SplitsProvider},
phylo::phylo_splits_graph::PhyloSplitsGraph,
splits::asplit::ASplitView,
};
use log::info;
#[derive(Copy, Clone, Debug)]
pub struct Pt(pub f64, pub f64);
#[derive(Copy, Clone, Debug)]
pub struct EqualAngleOpts {
pub use_weights: bool,
pub total_angle_deg: f64,
pub root_split: i32,
}
impl Default for EqualAngleOpts {
fn default() -> Self {
Self {
use_weights: true,
total_angle_deg: 360.0,
root_split: 0,
}
}
}
pub fn equal_angle_apply(
opts: EqualAngleOpts,
taxa_labels: &[String],
splits: &SplitsBlock,
graph: &mut PhyloSplitsGraph,
forbidden_splits: Option<&FixedBitSet>,
used_splits: &mut FixedBitSet,
) -> Result<bool> {
let t0 = std::time::Instant::now();
graph.clear();
used_splits.clear();
let ntax = taxa_labels.len();
if ntax == 0 {
info!("EqualAngle: no taxa; nothing to do");
return Ok(true);
}
let cycle = {
let c = splits
.cycle()
.ok_or_else(|| anyhow::anyhow!("SplitsBlock has no cycle"))?;
normalize_cycle_1based(c)?
};
init_graph_star(taxa_labels, splits, &cycle, graph)?;
let ordered = get_nontrivial_splits_ordered(splits);
let mut all_used = true;
for &sid in &ordered {
if is_circular_by_cycle(splits, sid, &cycle) {
wrap_split(ntax, splits, sid, &cycle, graph)?;
used_splits.grow(splits.nsplits() + 1);
used_splits.insert(sid);
} else {
all_used = false;
}
}
remove_temporary_trivial_edges(graph);
if !all_used {
convex_hull_apply(ntax, taxa_labels, splits.get_splits(), graph)?;
}
assign_angles_to_edges(
ntax,
splits,
&cycle,
graph,
forbidden_splits,
opts.total_angle_deg,
);
let (_align, _extra) = rotate_angles_align_then_offset(graph, 1, 180.0, 0.0);
let _coords = assign_coordinates_to_nodes(opts.use_weights, graph, 1, opts.root_split);
add_leaf_labels_from_taxa(graph, taxa_labels);
info!(
"EqualAngle: initialized network with {} nodes, {} edges in {:?} (use_weights={})",
graph.base.graph.node_count(),
graph.base.graph.edge_count(),
t0.elapsed(),
opts.use_weights
);
Ok(all_used)
}
pub fn normalize_cycle_1based(cycle_1based: &[usize]) -> Result<Vec<usize>> {
if cycle_1based.is_empty() || cycle_1based[0] != 0 {
bail!("cycle must be 1-based with index 0 unused");
}
let n = cycle_1based.len() - 1;
let mut k = 1usize;
while k <= n && cycle_1based[k] != 1 {
k += 1;
}
if k > n {
bail!("cycle does not contain taxon 1");
}
let mut out = vec![0usize; n + 1];
let mut i = 1;
let mut j = k;
while j <= n {
out[i] = cycle_1based[j];
i += 1;
j += 1;
}
j = 1;
while i <= n {
out[i] = cycle_1based[j];
i += 1;
j += 1;
}
Ok(out)
}
fn init_graph_star(
taxa: &[String],
splits: &SplitsBlock,
cycle: &[usize], g: &mut PhyloSplitsGraph,
) -> Result<()> {
g.clear();
let ntax = taxa.len();
let mut taxon2trivial: Vec<i32> = vec![0; ntax + 1];
for s in 1..=splits.nsplits() {
let sp = splits.split(s);
if sp.size() == 1 {
if let Some(t) = first_member(sp.smaller_part()) {
taxon2trivial[t] = s as i32;
}
}
}
let center = g.base.new_node();
for i in 1..=ntax {
let t = cycle[i];
let leaf = g.base.new_node();
g.base.add_taxon(leaf, t);
let e = g.new_edge(center, leaf)?;
let sid = taxon2trivial[t];
if sid != 0 {
let w = splits.split(sid as usize).weight();
g.base.set_weight(e, w);
g.set_split(e, sid);
} else {
g.set_split(e, -1); }
}
Ok(())
}
fn get_nontrivial_splits_ordered(splits: &SplitsBlock) -> Vec<usize> {
let mut pairs: Vec<(usize, usize)> = Vec::new(); for s in 1..=splits.nsplits() {
let sp = splits.split(s);
if sp.size() > 1 {
let size = sp.part_containing(1).count_ones(..);
pairs.push((size, s));
}
}
pairs.sort_by_key(|p| p.0);
pairs.into_iter().map(|p| p.1).collect()
}
fn is_circular_by_cycle(splits: &SplitsBlock, sid: usize, cycle: &[usize]) -> bool {
let sp = splits.split(sid);
let ntax = cycle.len() - 1;
let part = sp.part_not_containing(cycle[1]);
let mut pos: Vec<usize> = Vec::new();
for i in 2..=ntax {
let t = cycle[i];
if part.contains(t) {
pos.push(i);
}
}
if pos.is_empty() {
return true;
} for w in pos.windows(2) {
if w[1] != w[0] + 1 {
return false;
}
}
true
}
fn remove_temporary_trivial_edges(g: &mut PhyloSplitsGraph) {
let mut to_remove: Vec<EdgeIndex> = Vec::new();
for e in g.base.graph.edge_indices() {
if g.get_split(e) == -1 {
to_remove.push(e);
}
}
for e in to_remove {
if let Some((u, v)) = g.base.graph.edge_endpoints(e) {
let du = g.base.graph.neighbors(u).count();
let dv = g.base.graph.neighbors(v).count();
let (leaf, keep) = if du == 1 {
(u, v)
} else if dv == 1 {
(v, u)
} else {
continue;
};
if let Some(list) = g
.base
.node2taxa()
.expect("No taxa mapping")
.get(&leaf)
.cloned()
{
for t in list {
g.base.add_taxon(keep, t);
}
g.base.clear_taxa_for_node(leaf);
}
g.base.graph.remove_edge(e);
if g.base.graph.neighbors(leaf).count() == 0 {
g.base.graph.remove_node(leaf);
}
}
}
}
pub fn assign_angles_to_edges(
ntaxa: usize,
splits: &SplitsBlock,
cycle: &[usize],
g: &mut PhyloSplitsGraph,
forbidden: Option<&fixedbitset::FixedBitSet>,
total_angle: f64,
) {
let split2angle = assign_angles_to_splits(ntaxa, splits, cycle, total_angle);
let edges: Vec<petgraph::prelude::EdgeIndex> = g.base.graph.edge_indices().collect();
for e in edges {
let sid = g.get_split(e);
if sid > 0 {
let sidu = sid as usize;
let is_forbidden = forbidden.map_or(false, |bs| sidu < bs.len() && bs.contains(sidu));
if !is_forbidden {
if let Some(&theta) = split2angle.get(sidu) {
g.set_angle(e, theta);
}
}
}
}
}
pub fn assign_angles_to_splits(
ntaxa: usize,
splits: &SplitsBlock,
cycle: &[usize],
total_angle: f64,
) -> Vec<f64> {
let mut taxa_angles = vec![0.0f64; ntaxa + 1];
for tpos in 1..=ntaxa {
taxa_angles[tpos] =
total_angle * ((tpos - 1) as f64) / (ntaxa as f64) + (270.0 - 0.5 * total_angle);
}
let mut split2angle = vec![0.0f64; splits.nsplits() + 1];
for s in 1..=splits.nsplits() {
let part = splits.split(s).part_not_containing(cycle[1]);
let mut xp = 0usize;
let mut xq = 0usize;
for i in 2..=ntaxa {
let t = cycle[i];
if part.contains(t) {
if xp == 0 {
xp = i;
}
xq = i;
}
}
if xp == 0 {
split2angle[s] = modulo360(taxa_angles[1]);
} else {
split2angle[s] = modulo360(0.5 * (taxa_angles[xp] + taxa_angles[xq]));
}
}
split2angle
}
pub fn rotate_angles_align_then_offset(
g: &mut PhyloSplitsGraph,
taxon_id: usize,
target_deg: f64,
extra_offset_deg: f64,
) -> (Option<f64>, f64) {
let align_delta = rotate_angles_align_leaf(g, taxon_id, target_deg);
if extra_offset_deg != 0.0 {
rotate_angles_in_place(g, extra_offset_deg);
}
(align_delta, extra_offset_deg)
}
pub fn rotate_angles_in_place(g: &mut PhyloSplitsGraph, delta_deg: f64) {
let edges: Vec<petgraph::prelude::EdgeIndex> = g.base.graph.edge_indices().collect();
for e in edges {
let a = g.get_angle(e);
g.set_angle(e, modulo360(a + delta_deg));
}
}
pub fn rotate_angles_align_leaf(
g: &mut PhyloSplitsGraph,
taxon_id: usize,
target_deg: f64,
) -> Option<f64> {
let v = g
.base
.taxon2node()
.expect("No taxon2node map")
.get(&taxon_id)?;
let e = g.base.graph.edges(*v).next()?.id(); let cur = g.get_angle(e);
let delta = modulo360(target_deg - cur);
rotate_angles_in_place(g, delta);
Some(delta)
}
pub fn assign_coordinates_to_nodes(
use_weights: bool,
g: &PhyloSplitsGraph,
start_taxon_id: usize,
root_split: i32,
) -> HashMap<NodeIndex, Pt> {
let mut pts = HashMap::new();
let Some(&v0) = g.base.taxon2node().expect("taxon map").get(&start_taxon_id) else {
return pts;
};
pts.insert(v0, Pt(0.0, 0.0));
let mut visited = FixedBitSet::with_capacity(g.base.graph.node_bound().index());
let mut splits_in_path = FixedBitSet::with_capacity((g.max_split_id().max(0) as usize) + 2);
dfs_coords(
use_weights,
g,
v0,
&mut visited,
&mut splits_in_path,
&mut pts,
root_split,
);
pts
}
fn dfs_coords(
use_weights: bool,
g: &PhyloSplitsGraph,
v: NodeIndex,
visited: &mut FixedBitSet,
splits_in_path: &mut FixedBitSet,
pts: &mut HashMap<NodeIndex, Pt>,
root_split: i32,
) {
if visited.contains(v.index()) {
return;
}
visited.insert(v.index());
let nbrs: Vec<(EdgeIndex, NodeIndex)> = g
.base
.graph
.edges(v)
.map(|e| (e.id(), e.target()))
.collect();
for (e, w) in nbrs {
let sid = g.get_split(e);
if sid <= 0 {
continue;
}
let s = sid as usize;
if splits_in_path.len() <= s {
splits_in_path.grow(s + 1);
}
if splits_in_path.contains(s) {
continue;
} splits_in_path.insert(s);
let step = if use_weights {
g.base.weight(e)
} else if sid == root_split {
0.1
} else {
1.0
};
let Pt(x, y) = *pts.get(&v).unwrap();
let a = g.get_angle(e).to_radians();
pts.insert(w, Pt(x + step * a.cos(), y + step * a.sin()));
dfs_coords(use_weights, g, w, visited, splits_in_path, pts, root_split);
splits_in_path.set(s, false); }
}
fn add_leaf_labels_from_taxa(g: &mut PhyloSplitsGraph, taxa: &[String]) {
let mut updates: Vec<(NodeIndex, String)> = Vec::new();
for v in g.base.graph.node_indices() {
if g.base.graph.neighbors(v).count() == 1 {
if let Some(list) = g.base.node2taxa().expect("No node2taxa map").get(&v) {
if list.len() == 1 {
let t = list[0];
if t >= 1 && t <= taxa.len() {
updates.push((v, taxa[t - 1].clone()));
}
}
}
}
}
for (v, lab) in updates {
g.base.set_node_label(v, lab);
}
}
pub fn wrap_split(
ntax: usize,
splits: &SplitsBlock,
s: usize, cycle: &[usize], graph: &mut PhyloSplitsGraph,
) -> Result<()> {
let part = splits.get(s).part_not_containing(1);
let (mut xp, mut xq) = (0usize, 0usize);
for i in 1..=ntax {
let t = cycle[i];
if part.contains(t) {
if xp == 0 {
xp = t;
}
xq = t;
}
}
if xp == 0 || xq == 0 {
bail!(
"wrap_split: split {} has empty non-1 part after cycle filtering",
s
);
}
let vp = graph
.base
.get_taxon_node(xp)
.ok_or_else(|| anyhow!("wrap_split: no node for taxon {}", xp))?;
let vq = graph
.base
.get_taxon_node(xq)
.ok_or_else(|| anyhow!("wrap_split: no node for taxon {}", xq))?;
let e_vp = graph
.first_adjacent_edge(vp)
.ok_or_else(|| anyhow!("wrap_split: taxon node vp ({:?}) has no incident edge", vp))?;
let e_vq = graph
.first_adjacent_edge(vq)
.ok_or_else(|| anyhow!("wrap_split: taxon node vq ({:?}) has no incident edge", vq))?;
let target_leaf_edge = e_vq;
let mut e = e_vp;
let mut v = graph.base.get_opposite(vp, e);
let mut leaf_edges: Vec<EdgeIndex> = Vec::with_capacity(ntax);
leaf_edges.push(e);
let mut nodes_visited: HashSet<NodeIndex> = HashSet::new();
let mut prev_u: Option<NodeIndex> = None;
loop {
debug!(
"wrap_split: entering node {:?} via edge {:?} (nodes visits {})",
v,
e,
nodes_visited.len()
);
if !nodes_visited.insert(v) {
bail!("wrap_split: node revisited during wrapping: {:?}", v);
}
let f0 = e;
let mut reached_end = false;
let mut next_e: Option<EdgeIndex> = None;
if let Some(mut f) = graph.next_adjacent_edge_cyclic(v, e) {
while graph.is_leaf_edge(f) {
leaf_edges.push(f);
if f == target_leaf_edge {
break;
}
if f == f0 {
return Err(anyhow!(
"wrap_split: next adjacent edge cyclic after f0 ({:?}) is also f0; cycle detected at node {:?}",
f0,
v
));
}
f = graph.next_adjacent_edge_cyclic(v, f).ok_or_else(|| {
anyhow!(
"wrap_split: no next leaf edge at node {:?} (after entering via {:?})",
v,
e
)
})?;
}
if f == target_leaf_edge {
next_e = None; reached_end = true;
} else {
next_e = Some(f);
}
debug!(
"wrap_split: reached node {:?} edge {:?} (via {:?})",
v, f, f0
);
};
let u = graph.base.new_node();
let f_uv = graph.new_edge_after(u, v, f0)?;
graph.set_split(f_uv, s as i32);
graph.base.set_weight(f_uv, splits.get(s).weight().into());
if let Some(prev) = prev_u {
let e_split = graph.get_split(e);
let e_w = graph.base.weight(e);
let f_uprev = graph.new_edge(u, prev)?;
graph.set_split(f_uprev, e_split);
graph.base.set_weight(f_uprev, e_w);
}
let mut copies: Vec<(NodeIndex, i32, f64, EdgeIndex)> =
Vec::with_capacity(leaf_edges.len());
for f in leaf_edges.iter().copied() {
let w = graph.base.get_opposite(v, f);
let s_id = graph.get_split(f);
let wgt = graph.base.weight(f);
copies.push((w, s_id, wgt, f));
}
for (w, s_id, wgt, f_old) in copies {
let f_new = graph.new_edge(u, w)?;
graph.set_split(f_new, s_id);
graph.base.set_weight(f_new, wgt);
graph.remove_edge(f_old); }
leaf_edges.clear();
if reached_end {
break;
} else {
let ne = next_e.ok_or_else(|| {
anyhow!(
"wrap_split: no next boundary edge at node {:?} (after entering via {:?})",
v,
f0
)
})?;
v = graph.base.get_opposite(v, ne);
e = ne;
prev_u = Some(u);
}
}
Ok(())
}
fn first_member(bs: &fixedbitset::FixedBitSet) -> Option<usize> {
for i in 1..bs.len() {
if bs.contains(i) {
return Some(i);
}
}
None
}
fn modulo360(a: f64) -> f64 {
let mut x = a % 360.0;
if x < 0.0 {
x += 360.0;
}
x
}
#[cfg(test)]
mod tests {
use super::*;
use fixedbitset::FixedBitSet;
use ndarray::arr2;
use crate::{
algorithms::equal_angle::{
get_nontrivial_splits_ordered, init_graph_star, normalize_cycle_1based,
},
data::splits_blocks::SplitsBlock,
ordering::ordering_huson2023::compute_order_huson_2023,
phylo::phylo_splits_graph::PhyloSplitsGraph,
utils::compute_least_squares_fit,
weights::active_set_weights::{NNLSParams, compute_asplits},
};
#[test]
fn smoke_10_1() {
let d = arr2(&[
[0.0, 5.0, 12.0, 7.0, 3.0, 9.0, 11.0, 6.0, 4.0, 10.0],
[5.0, 0.0, 8.0, 2.0, 14.0, 5.0, 13.0, 7.0, 12.0, 1.0],
[12.0, 8.0, 0.0, 4.0, 9.0, 3.0, 8.0, 2.0, 5.0, 6.0],
[7.0, 2.0, 4.0, 0.0, 11.0, 7.0, 10.0, 4.0, 6.0, 9.0],
[3.0, 14.0, 9.0, 11.0, 0.0, 8.0, 1.0, 13.0, 2.0, 7.0],
[9.0, 5.0, 3.0, 7.0, 8.0, 0.0, 12.0, 5.0, 3.0, 4.0],
[11.0, 13.0, 8.0, 10.0, 1.0, 12.0, 0.0, 6.0, 2.0, 8.0],
[6.0, 7.0, 2.0, 4.0, 13.0, 5.0, 6.0, 0.0, 9.0, 7.0],
[4.0, 12.0, 5.0, 6.0, 2.0, 3.0, 2.0, 9.0, 0.0, 5.0],
[10.0, 1.0, 6.0, 9.0, 7.0, 4.0, 8.0, 7.0, 5.0, 0.0],
]);
let cycle = compute_order_huson_2023(&d).unwrap();
let mut params = NNLSParams::default();
let progress = None; let labels = vec![
"t1".to_string(),
"t2".to_string(),
"t3".to_string(),
"t4".to_string(),
"t5".to_string(),
"t6".to_string(),
"t7".to_string(),
"t8".to_string(),
"t9".to_string(),
"t10".to_string(),
];
let splits = compute_asplits(&cycle, &d, &mut params, progress).expect("NNLS solve");
let fit = compute_least_squares_fit(&d, &splits);
let mut splits_blocks = SplitsBlock::new();
splits_blocks.set_splits(splits);
splits_blocks.set_fit(fit);
splits_blocks.set_cycle(cycle, true).expect("set cycle");
let mut graph = PhyloSplitsGraph::new();
let mut used_splits = FixedBitSet::with_capacity(splits_blocks.nsplits() + 1);
let cycle_exp = vec![0, 1, 5, 7, 9, 3, 8, 4, 2, 10, 6];
let c = splits_blocks
.cycle()
.ok_or_else(|| anyhow::anyhow!("SplitsBlock has no cycle"))
.expect("get cycle");
let norm_cycle = normalize_cycle_1based(&c).expect("normalize cycle");
assert_eq!(norm_cycle, cycle_exp);
init_graph_star(&labels, &splits_blocks, &norm_cycle, &mut graph).expect("init graph star");
assert!(graph.base.graph.node_count() == 11);
assert!(graph.base.graph.edge_count() == 10);
let ordered = get_nontrivial_splits_ordered(&splits_blocks);
assert_eq!(ordered, vec![2, 3, 11, 9, 16, 4, 8, 15, 7, 14, 19, 21]);
let ntax = labels.len();
for &sid in &ordered {
if is_circular_by_cycle(&splits_blocks, sid, &norm_cycle) {
wrap_split(ntax, &splits_blocks, sid, &norm_cycle, &mut graph).expect("wrap split");
used_splits.grow(splits_blocks.nsplits() + 1);
used_splits.insert(sid);
}
}
assert!(graph.base.graph.node_count() == 41);
assert!(graph.base.graph.edge_count() == 58);
let exp_angles = vec![
0.0, 270.0, 288.0, 324.0, 18.0, 54.0, 126.0, 144.0, 162.0, 180.0, 162.0, 234.0, 198.0,
234.0, 252.0, 270.0, 288.0, 270.0, 306.0, 324.0, 342.0, 0.0, 18.0,
];
let split2angle = assign_angles_to_splits(ntax, &splits_blocks, &norm_cycle, 360.);
assert_eq!(split2angle, exp_angles);
}
}