1use anyhow::{Result, anyhow, bail};
2use fixedbitset::FixedBitSet;
3use petgraph::{
4 csr::IndexType,
5 prelude::{EdgeIndex, NodeIndex},
6 visit::{EdgeRef, NodeIndexable},
7};
8use std::collections::{HashMap, HashSet};
9
10use crate::{
11 algorithms::convex_hull::convex_hull_apply,
12 data::splits_blocks::{SplitsBlock, SplitsProvider},
13 phylo::phylo_splits_graph::PhyloSplitsGraph,
14 splits::asplit::ASplitView,
15};
16use log::info;
17
18#[derive(Copy, Clone, Debug)]
19pub struct Pt(pub f64, pub f64);
20
21#[derive(Copy, Clone, Debug)]
23pub struct EqualAngleOpts {
24 pub use_weights: bool,
26 pub total_angle_deg: f64,
28 pub root_split: i32,
30}
31
32impl Default for EqualAngleOpts {
33 fn default() -> Self {
34 Self {
35 use_weights: true,
36 total_angle_deg: 360.0,
37 root_split: 0,
38 }
39 }
40}
41
42pub fn equal_angle_apply(
51 opts: EqualAngleOpts,
52 taxa_labels: &[String],
53 splits: &SplitsBlock,
54 graph: &mut PhyloSplitsGraph,
55 forbidden_splits: Option<&FixedBitSet>,
56 used_splits: &mut FixedBitSet,
57) -> Result<bool> {
58 let t0 = std::time::Instant::now();
59 graph.clear();
60 used_splits.clear();
61
62 let ntax = taxa_labels.len();
63 if ntax == 0 {
64 info!("EqualAngle: no taxa; nothing to do");
65 return Ok(true);
66 }
67
68 let cycle = {
69 let c = splits
70 .cycle()
71 .ok_or_else(|| anyhow::anyhow!("SplitsBlock has no cycle"))?;
72 normalize_cycle_1based(c)?
73 };
74
75 init_graph_star(taxa_labels, splits, &cycle, graph)?;
77
78 let ordered = get_nontrivial_splits_ordered(splits);
80
81 let mut all_used = true;
83 for &sid in &ordered {
84 if is_circular_by_cycle(splits, sid, &cycle) {
85 wrap_split(ntax, splits, sid, &cycle, graph)?;
87 used_splits.grow(splits.nsplits() + 1);
89 used_splits.insert(sid);
90 } else {
91 all_used = false;
92 }
93 }
94
95 remove_temporary_trivial_edges(graph);
97
98 if !all_used {
99 convex_hull_apply(ntax, taxa_labels, splits.get_splits(), graph)?;
101 }
102 assign_angles_to_edges(
104 ntax,
105 splits,
106 &cycle,
107 graph,
108 forbidden_splits,
109 opts.total_angle_deg,
110 );
111
112 let (_align, _extra) = rotate_angles_align_then_offset(graph, 1, 180.0, 0.0);
114
115 let _coords = assign_coordinates_to_nodes(opts.use_weights, graph, 1, opts.root_split);
117
118 add_leaf_labels_from_taxa(graph, taxa_labels);
120
121 info!(
122 "EqualAngle: initialized network with {} nodes, {} edges in {:?} (use_weights={})",
123 graph.base.graph.node_count(),
124 graph.base.graph.edge_count(),
125 t0.elapsed(),
126 opts.use_weights
127 );
128
129 Ok(all_used)
130}
131
132pub fn normalize_cycle_1based(cycle_1based: &[usize]) -> Result<Vec<usize>> {
136 if cycle_1based.is_empty() || cycle_1based[0] != 0 {
137 bail!("cycle must be 1-based with index 0 unused");
138 }
139 let n = cycle_1based.len() - 1;
140 let mut k = 1usize;
141 while k <= n && cycle_1based[k] != 1 {
142 k += 1;
143 }
144 if k > n {
145 bail!("cycle does not contain taxon 1");
146 }
147
148 let mut out = vec![0usize; n + 1];
149 let mut i = 1;
150 let mut j = k;
151 while j <= n {
152 out[i] = cycle_1based[j];
153 i += 1;
154 j += 1;
155 }
156 j = 1;
157 while i <= n {
158 out[i] = cycle_1based[j];
159 i += 1;
160 j += 1;
161 }
162 Ok(out)
163}
164
165fn init_graph_star(
168 taxa: &[String],
169 splits: &SplitsBlock,
170 cycle: &[usize], g: &mut PhyloSplitsGraph,
172) -> Result<()> {
173 g.clear();
174
175 let ntax = taxa.len();
176 let mut taxon2trivial: Vec<i32> = vec![0; ntax + 1];
177 for s in 1..=splits.nsplits() {
178 let sp = splits.split(s);
179 if sp.size() == 1 {
180 if let Some(t) = first_member(sp.smaller_part()) {
181 taxon2trivial[t] = s as i32;
182 }
183 }
184 }
185
186 let center = g.base.new_node();
187 for i in 1..=ntax {
189 let t = cycle[i];
190 let leaf = g.base.new_node();
191 g.base.add_taxon(leaf, t);
192 let e = g.new_edge(center, leaf)?;
193 let sid = taxon2trivial[t];
194 if sid != 0 {
195 let w = splits.split(sid as usize).weight();
196 g.base.set_weight(e, w);
197 g.set_split(e, sid);
198 } else {
199 g.set_split(e, -1); }
201 }
202 Ok(())
203}
204
205fn get_nontrivial_splits_ordered(splits: &SplitsBlock) -> Vec<usize> {
207 let mut pairs: Vec<(usize, usize)> = Vec::new(); for s in 1..=splits.nsplits() {
209 let sp = splits.split(s);
210 if sp.size() > 1 {
211 let size = sp.part_containing(1).count_ones(..);
212 pairs.push((size, s));
213 }
214 }
215 pairs.sort_by_key(|p| p.0);
216 pairs.into_iter().map(|p| p.1).collect()
217}
218
219fn is_circular_by_cycle(splits: &SplitsBlock, sid: usize, cycle: &[usize]) -> bool {
221 let sp = splits.split(sid);
222 let ntax = cycle.len() - 1;
223 let part = sp.part_not_containing(cycle[1]);
224
225 let mut pos: Vec<usize> = Vec::new();
227 for i in 2..=ntax {
228 let t = cycle[i];
229 if part.contains(t) {
230 pos.push(i);
231 }
232 }
233 if pos.is_empty() {
234 return true;
235 } for w in pos.windows(2) {
238 if w[1] != w[0] + 1 {
239 return false;
240 }
241 }
242 true
243}
244
245fn remove_temporary_trivial_edges(g: &mut PhyloSplitsGraph) {
247 let mut to_remove: Vec<EdgeIndex> = Vec::new();
248 for e in g.base.graph.edge_indices() {
249 if g.get_split(e) == -1 {
250 to_remove.push(e);
251 }
252 }
253 for e in to_remove {
254 if let Some((u, v)) = g.base.graph.edge_endpoints(e) {
255 let du = g.base.graph.neighbors(u).count();
257 let dv = g.base.graph.neighbors(v).count();
258 let (leaf, keep) = if du == 1 {
259 (u, v)
260 } else if dv == 1 {
261 (v, u)
262 } else {
263 continue;
264 };
265
266 if let Some(list) = g
267 .base
268 .node2taxa()
269 .expect("No taxa mapping")
270 .get(&leaf)
271 .cloned()
272 {
273 for t in list {
274 g.base.add_taxon(keep, t);
275 }
276 g.base.clear_taxa_for_node(leaf);
278 }
279 g.base.graph.remove_edge(e);
280 if g.base.graph.neighbors(leaf).count() == 0 {
282 g.base.graph.remove_node(leaf);
283 }
284 }
285 }
286}
287
288pub fn assign_angles_to_edges(
291 ntaxa: usize,
292 splits: &SplitsBlock,
293 cycle: &[usize],
294 g: &mut PhyloSplitsGraph,
295 forbidden: Option<&fixedbitset::FixedBitSet>,
296 total_angle: f64,
297) {
298 let split2angle = assign_angles_to_splits(ntaxa, splits, cycle, total_angle);
299
300 let edges: Vec<petgraph::prelude::EdgeIndex> = g.base.graph.edge_indices().collect();
302
303 for e in edges {
304 let sid = g.get_split(e);
305 if sid > 0 {
306 let sidu = sid as usize;
307 let is_forbidden = forbidden.map_or(false, |bs| sidu < bs.len() && bs.contains(sidu));
308 if !is_forbidden {
309 if let Some(&theta) = split2angle.get(sidu) {
310 g.set_angle(e, theta);
311 }
312 }
313 }
314 }
315}
316
317pub fn assign_angles_to_splits(
320 ntaxa: usize,
321 splits: &SplitsBlock,
322 cycle: &[usize],
323 total_angle: f64,
324) -> Vec<f64> {
325 let mut taxa_angles = vec![0.0f64; ntaxa + 1];
327 for tpos in 1..=ntaxa {
328 taxa_angles[tpos] =
329 total_angle * ((tpos - 1) as f64) / (ntaxa as f64) + (270.0 - 0.5 * total_angle);
330 }
331
332 let mut split2angle = vec![0.0f64; splits.nsplits() + 1];
334
335 for s in 1..=splits.nsplits() {
336 let part = splits.split(s).part_not_containing(cycle[1]);
337 let mut xp = 0usize;
338 let mut xq = 0usize;
339 for i in 2..=ntaxa {
340 let t = cycle[i];
341 if part.contains(t) {
342 if xp == 0 {
343 xp = i;
344 }
345 xq = i;
346 }
347 }
348 if xp == 0 {
349 split2angle[s] = modulo360(taxa_angles[1]);
351 } else {
352 split2angle[s] = modulo360(0.5 * (taxa_angles[xp] + taxa_angles[xq]));
353 }
354 }
355 split2angle
356}
357
358pub fn rotate_angles_align_then_offset(
365 g: &mut PhyloSplitsGraph,
366 taxon_id: usize,
367 target_deg: f64,
368 extra_offset_deg: f64,
369) -> (Option<f64>, f64) {
370 let align_delta = rotate_angles_align_leaf(g, taxon_id, target_deg);
372
373 if extra_offset_deg != 0.0 {
375 rotate_angles_in_place(g, extra_offset_deg);
376 }
377 (align_delta, extra_offset_deg)
378}
379
380pub fn rotate_angles_in_place(g: &mut PhyloSplitsGraph, delta_deg: f64) {
382 let edges: Vec<petgraph::prelude::EdgeIndex> = g.base.graph.edge_indices().collect();
383
384 for e in edges {
385 let a = g.get_angle(e);
386 g.set_angle(e, modulo360(a + delta_deg));
387 }
388}
389
390pub fn rotate_angles_align_leaf(
393 g: &mut PhyloSplitsGraph,
394 taxon_id: usize,
395 target_deg: f64,
396) -> Option<f64> {
397 let v = g
398 .base
399 .taxon2node()
400 .expect("No taxon2node map")
401 .get(&taxon_id)?;
402 let e = g.base.graph.edges(*v).next()?.id(); let cur = g.get_angle(e);
404 let delta = modulo360(target_deg - cur);
405 rotate_angles_in_place(g, delta);
406 Some(delta)
407}
408
409pub fn assign_coordinates_to_nodes(
410 use_weights: bool,
411 g: &PhyloSplitsGraph,
412 start_taxon_id: usize,
413 root_split: i32,
414) -> HashMap<NodeIndex, Pt> {
415 let mut pts = HashMap::new();
416 let Some(&v0) = g.base.taxon2node().expect("taxon map").get(&start_taxon_id) else {
417 return pts;
418 };
419 pts.insert(v0, Pt(0.0, 0.0));
420
421 let mut visited = FixedBitSet::with_capacity(g.base.graph.node_bound().index());
422 let mut splits_in_path = FixedBitSet::with_capacity((g.max_split_id().max(0) as usize) + 2);
423
424 dfs_coords(
425 use_weights,
426 g,
427 v0,
428 &mut visited,
429 &mut splits_in_path,
430 &mut pts,
431 root_split,
432 );
433 pts
434}
435
436fn dfs_coords(
437 use_weights: bool,
438 g: &PhyloSplitsGraph,
439 v: NodeIndex,
440 visited: &mut FixedBitSet,
441 splits_in_path: &mut FixedBitSet,
442 pts: &mut HashMap<NodeIndex, Pt>,
443 root_split: i32,
444) {
445 if visited.contains(v.index()) {
446 return;
447 }
448 visited.insert(v.index());
449
450 let nbrs: Vec<(EdgeIndex, NodeIndex)> = g
451 .base
452 .graph
453 .edges(v)
454 .map(|e| (e.id(), e.target()))
455 .collect();
456
457 for (e, w) in nbrs {
458 let sid = g.get_split(e);
459 if sid <= 0 {
460 continue;
461 }
462 let s = sid as usize;
463 if splits_in_path.len() <= s {
464 splits_in_path.grow(s + 1);
465 }
466 if splits_in_path.contains(s) {
467 continue;
468 } splits_in_path.insert(s);
470
471 let step = if use_weights {
472 g.base.weight(e)
473 } else if sid == root_split {
474 0.1
475 } else {
476 1.0
477 };
478 let Pt(x, y) = *pts.get(&v).unwrap();
479 let a = g.get_angle(e).to_radians();
480 pts.insert(w, Pt(x + step * a.cos(), y + step * a.sin()));
481
482 dfs_coords(use_weights, g, w, visited, splits_in_path, pts, root_split);
483 splits_in_path.set(s, false); }
485}
486
487fn add_leaf_labels_from_taxa(g: &mut PhyloSplitsGraph, taxa: &[String]) {
489 let mut updates: Vec<(NodeIndex, String)> = Vec::new();
491 for v in g.base.graph.node_indices() {
492 if g.base.graph.neighbors(v).count() == 1 {
493 if let Some(list) = g.base.node2taxa().expect("No node2taxa map").get(&v) {
494 if list.len() == 1 {
495 let t = list[0];
496 if t >= 1 && t <= taxa.len() {
497 updates.push((v, taxa[t - 1].clone()));
498 }
499 }
500 }
501 }
502 }
503 for (v, lab) in updates {
504 g.base.set_node_label(v, lab);
505 }
506}
507
508pub fn wrap_split(
512 ntax: usize,
513 splits: &SplitsBlock,
514 s: usize, cycle: &[usize], graph: &mut PhyloSplitsGraph,
517) -> Result<()> {
518 let part = splits.get(s).part_not_containing(1);
520
521 let (mut xp, mut xq) = (0usize, 0usize);
524 for i in 1..=ntax {
525 let t = cycle[i];
526 if part.contains(t) {
527 if xp == 0 {
528 xp = t;
529 }
530 xq = t;
531 }
532 }
533 if xp == 0 || xq == 0 {
534 bail!(
535 "wrap_split: split {} has empty non-1 part after cycle filtering",
536 s
537 );
538 }
539
540 let vp = graph
542 .base
543 .get_taxon_node(xp)
544 .ok_or_else(|| anyhow!("wrap_split: no node for taxon {}", xp))?;
545 let vq = graph
546 .base
547 .get_taxon_node(xq)
548 .ok_or_else(|| anyhow!("wrap_split: no node for taxon {}", xq))?;
549
550 let e_vp = graph
551 .first_adjacent_edge(vp)
552 .ok_or_else(|| anyhow!("wrap_split: taxon node vp ({:?}) has no incident edge", vp))?;
553
554 let e_vq = graph
555 .first_adjacent_edge(vq)
556 .ok_or_else(|| anyhow!("wrap_split: taxon node vq ({:?}) has no incident edge", vq))?;
557
558 let target_leaf_edge = e_vq; let mut e = e_vp;
562 let mut v = graph.base.get_opposite(vp, e);
563
564 let mut leaf_edges: Vec<EdgeIndex> = Vec::with_capacity(ntax);
566 leaf_edges.push(e);
567 let mut nodes_visited: HashSet<NodeIndex> = HashSet::new();
569
570 let mut prev_u: Option<NodeIndex> = None;
572
573 loop {
575 debug!(
576 "wrap_split: entering node {:?} via edge {:?} (nodes visits {})",
577 v,
578 e,
579 nodes_visited.len()
580 );
581 if !nodes_visited.insert(v) {
582 bail!("wrap_split: node revisited during wrapping: {:?}", v);
584 }
585
586 let f0 = e;
588
589 let mut reached_end = false;
592
593 let mut next_e: Option<EdgeIndex> = None;
594
595 if let Some(mut f) = graph.next_adjacent_edge_cyclic(v, e) {
597 while graph.is_leaf_edge(f) {
598 leaf_edges.push(f);
599 if f == target_leaf_edge {
600 break;
601 }
602 if f == f0 {
603 return Err(anyhow!(
604 "wrap_split: next adjacent edge cyclic after f0 ({:?}) is also f0; cycle detected at node {:?}",
605 f0,
606 v
607 ));
608 }
609 f = graph.next_adjacent_edge_cyclic(v, f).ok_or_else(|| {
610 anyhow!(
611 "wrap_split: no next leaf edge at node {:?} (after entering via {:?})",
612 v,
613 e
614 )
615 })?;
616 }
617
618 if f == target_leaf_edge {
619 next_e = None; reached_end = true;
621 } else {
622 next_e = Some(f);
623 }
624 debug!(
625 "wrap_split: reached node {:?} edge {:?} (via {:?})",
626 v, f, f0
627 );
628 };
629
630 let u = graph.base.new_node();
632
633 let f_uv = graph.new_edge_after(u, v, f0)?;
635 graph.set_split(f_uv, s as i32);
636 graph.base.set_weight(f_uv, splits.get(s).weight().into());
637
638 if let Some(prev) = prev_u {
640 let e_split = graph.get_split(e);
642 let e_w = graph.base.weight(e);
643 let f_uprev = graph.new_edge(u, prev)?;
644 graph.set_split(f_uprev, e_split);
645 graph.base.set_weight(f_uprev, e_w);
646 }
647
648 let mut copies: Vec<(NodeIndex, i32, f64, EdgeIndex)> =
651 Vec::with_capacity(leaf_edges.len());
652 for f in leaf_edges.iter().copied() {
653 let w = graph.base.get_opposite(v, f);
654 let s_id = graph.get_split(f);
655 let wgt = graph.base.weight(f);
656 copies.push((w, s_id, wgt, f));
657 }
658 for (w, s_id, wgt, f_old) in copies {
659 let f_new = graph.new_edge(u, w)?;
660 graph.set_split(f_new, s_id);
661 graph.base.set_weight(f_new, wgt);
662 graph.remove_edge(f_old); }
664 leaf_edges.clear();
665 if reached_end {
667 break;
668 } else {
669 let ne = next_e.ok_or_else(|| {
670 anyhow!(
671 "wrap_split: no next boundary edge at node {:?} (after entering via {:?})",
672 v,
673 f0
674 )
675 })?;
676 v = graph.base.get_opposite(v, ne);
677 e = ne;
678 prev_u = Some(u);
679 }
680 }
681
682 Ok(())
683}
684
685fn first_member(bs: &fixedbitset::FixedBitSet) -> Option<usize> {
688 for i in 1..bs.len() {
690 if bs.contains(i) {
691 return Some(i);
692 }
693 }
694 None
695}
696
697fn modulo360(a: f64) -> f64 {
698 let mut x = a % 360.0;
699 if x < 0.0 {
700 x += 360.0;
701 }
702 x
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708 use fixedbitset::FixedBitSet;
709 use ndarray::arr2;
710
711 use crate::{
712 algorithms::equal_angle::{
713 get_nontrivial_splits_ordered, init_graph_star, normalize_cycle_1based,
714 },
715 data::splits_blocks::SplitsBlock,
716 ordering::ordering_huson2023::compute_order_huson_2023,
717 phylo::phylo_splits_graph::PhyloSplitsGraph,
718 utils::compute_least_squares_fit,
719 weights::active_set_weights::{NNLSParams, compute_asplits},
720 };
721
722 #[test]
723 fn smoke_10_1() {
724 let d = arr2(&[
725 [0.0, 5.0, 12.0, 7.0, 3.0, 9.0, 11.0, 6.0, 4.0, 10.0],
726 [5.0, 0.0, 8.0, 2.0, 14.0, 5.0, 13.0, 7.0, 12.0, 1.0],
727 [12.0, 8.0, 0.0, 4.0, 9.0, 3.0, 8.0, 2.0, 5.0, 6.0],
728 [7.0, 2.0, 4.0, 0.0, 11.0, 7.0, 10.0, 4.0, 6.0, 9.0],
729 [3.0, 14.0, 9.0, 11.0, 0.0, 8.0, 1.0, 13.0, 2.0, 7.0],
730 [9.0, 5.0, 3.0, 7.0, 8.0, 0.0, 12.0, 5.0, 3.0, 4.0],
731 [11.0, 13.0, 8.0, 10.0, 1.0, 12.0, 0.0, 6.0, 2.0, 8.0],
732 [6.0, 7.0, 2.0, 4.0, 13.0, 5.0, 6.0, 0.0, 9.0, 7.0],
733 [4.0, 12.0, 5.0, 6.0, 2.0, 3.0, 2.0, 9.0, 0.0, 5.0],
734 [10.0, 1.0, 6.0, 9.0, 7.0, 4.0, 8.0, 7.0, 5.0, 0.0],
735 ]);
736
737 let cycle = compute_order_huson_2023(&d);
738 let mut params = NNLSParams::default();
739 let progress = None; let labels = vec![
741 "t1".to_string(),
742 "t2".to_string(),
743 "t3".to_string(),
744 "t4".to_string(),
745 "t5".to_string(),
746 "t6".to_string(),
747 "t7".to_string(),
748 "t8".to_string(),
749 "t9".to_string(),
750 "t10".to_string(),
751 ];
752
753 let splits = compute_asplits(&cycle, &d, &mut params, progress).expect("NNLS solve");
754 let fit = compute_least_squares_fit(&d, &splits);
755 let mut splits_blocks = SplitsBlock::new();
756 splits_blocks.set_splits(splits);
757 splits_blocks.set_fit(fit);
758 splits_blocks.set_cycle(cycle, true).expect("set cycle");
759
760 let mut graph = PhyloSplitsGraph::new();
761 let mut used_splits = FixedBitSet::with_capacity(splits_blocks.nsplits() + 1);
763
764 let cycle_exp = vec![0, 1, 5, 7, 9, 3, 8, 4, 2, 10, 6];
765 let c = splits_blocks
766 .cycle()
767 .ok_or_else(|| anyhow::anyhow!("SplitsBlock has no cycle"))
768 .expect("get cycle");
769 let norm_cycle = normalize_cycle_1based(&c).expect("normalize cycle");
770 assert_eq!(norm_cycle, cycle_exp);
771 init_graph_star(&labels, &splits_blocks, &norm_cycle, &mut graph).expect("init graph star");
772 assert!(graph.base.graph.node_count() == 11);
773 assert!(graph.base.graph.edge_count() == 10);
774
775 let ordered = get_nontrivial_splits_ordered(&splits_blocks);
776 assert_eq!(ordered, vec![2, 3, 11, 9, 16, 4, 8, 15, 7, 14, 19, 21]);
777 let ntax = labels.len();
778 let mut all_used = true;
779 for &sid in &ordered {
780 if is_circular_by_cycle(&splits_blocks, sid, &norm_cycle) {
781 wrap_split(ntax, &splits_blocks, sid, &norm_cycle, &mut graph).expect("wrap split");
783 used_splits.grow(splits_blocks.nsplits() + 1);
785 used_splits.insert(sid);
786 println!("====================");
787 println!("used split {}", sid);
788 println!("{}", graph.base.graph.node_count());
789 println!("{}", graph.base.graph.edge_count());
790 } else {
791 all_used = false;
792 }
793 }
794 println!("all_used = {}", all_used);
795 println!("{}", graph.base.graph.node_count());
796 println!("{}", graph.base.graph.edge_count());
797 assert!(graph.base.graph.node_count() == 41);
798 assert!(graph.base.graph.edge_count() == 58);
799
800 let exp_angles = vec![
801 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,
802 234.0, 252.0, 270.0, 288.0, 270.0, 306.0, 324.0, 342.0, 0.0, 18.0,
803 ];
804 let split2angle = assign_angles_to_splits(ntax, &splits_blocks, &norm_cycle, 360.);
805 assert_eq!(split2angle, exp_angles);
806 }
819}