Skip to content

Commit 6408a11

Browse files
authored
Merge pull request #40 from Adamtaranto/copilot/vscode1763330089411
Fix TSW and PN algorithms producing excess intermediate nodes
2 parents d40d526 + 10b7764 commit 6408a11

5 files changed

Lines changed: 525 additions & 116 deletions

File tree

ALGORITHM_REFINEMENTS.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Algorithm Refinements: TSW and PN
2+
3+
## Summary
4+
5+
This document describes the refinements made to the Tight Span Walker (TSW) and Parsimony Network (PN) algorithms to reduce the excess creation of intermediate nodes and improve network accuracy.
6+
7+
## Problem Statement
8+
9+
### TSW Issues
10+
- The original Python implementation was creating an excess of intermediate (median) nodes
11+
- Simplified geodesic path computation didn't match the C++ reference implementation
12+
- Missing pruning logic for unnecessary median vertices
13+
14+
### PN Issues
15+
- The algorithm was automatically subdividing all multi-mutation edges into single-mutation steps
16+
- This created many unnecessary intermediate nodes where edges should be direct
17+
- Deviated from the C++ reference implementation which relies on consensus sampling
18+
19+
## Solutions Implemented
20+
21+
### TSW Refinements
22+
23+
#### 1. Proper Geodesic Computation
24+
The geodesic path algorithm now follows the C++ reference implementation:
25+
26+
- **Bipartite Coloring**: Vertices are classified as "green" (closer to source) or "red" (closer to target) based on their dT distances
27+
- **Delta Calculation**: Computes the splitting parameter delta to determine if an intermediate vertex is needed
28+
- **Vertex Map**: Tracks dT vectors to avoid creating duplicate vertices with the same metric properties
29+
- **Recursive Construction**: Properly creates intermediate vertices only when delta < dT(f,g)
30+
31+
**Key Code Changes:**
32+
```python
33+
# Old approach - overly simplistic
34+
if actual_dist - dt_dist > self.epsilon:
35+
median_hap = self._create_median_vertex(hap1, hap2)
36+
# Always created medians when distances didn't match
37+
38+
# New approach - proper geodesic computation
39+
# 1. Compute bipartite coloring
40+
for i in range(n):
41+
if abs(fi + dt_fg + gi - distance_matrix.matrix[i, idx2]) < self.epsilon:
42+
green_vertices.add(i)
43+
elif abs(fi + dt_fg - gi) < self.epsilon:
44+
red_vertices.add(i)
45+
46+
# 2. Compute delta (splitting parameter)
47+
delta = min([(fi + fj - dij) for i, j in green_pairs]) / 2.0
48+
49+
# 3. Only create vertex if needed
50+
if dt_fg > delta + self.epsilon:
51+
# Create intermediate vertex and recurse
52+
```
53+
54+
#### 2. Median Vertex Pruning
55+
Added post-processing to remove medians that don't serve a topological purpose:
56+
57+
**Pruning Criteria:**
58+
A median vertex is kept only if it:
59+
1. Connects two or more observed (non-median) vertices (bridges observed nodes), OR
60+
2. Connects to at least one observed vertex and one or more medians (on path between observed), OR
61+
3. Connects to two or more medians (internal to a path)
62+
63+
**Benefits:**
64+
- Eliminates isolated or non-bridging intermediate nodes
65+
- Produces cleaner, more interpretable networks
66+
- Maintains connectivity while removing redundancy
67+
68+
### PN Refinements
69+
70+
#### 1. Removed Automatic Edge Subdivision
71+
The key change was to stop automatically subdividing multi-mutation edges:
72+
73+
**Old Behavior:**
74+
```python
75+
# Add edge if not already present
76+
if not network.has_edge(id1, id2):
77+
network.add_edge(id1, id2, distance=distance)
78+
79+
# Automatically subdivide edges > 1 mutation
80+
if distance > 1:
81+
self._add_median_vertices_along_edge(
82+
network, id1, id2, seq1, seq2, int(distance)
83+
)
84+
```
85+
86+
**New Behavior:**
87+
```python
88+
# Add edge if not already present
89+
# Let the consensus sampling handle network structure
90+
if not network.has_edge(id1, id2):
91+
network.add_edge(id1, id2, distance=distance)
92+
```
93+
94+
**Rationale:**
95+
- The PN algorithm samples edges from multiple random parsimony trees
96+
- The consensus process (frequency thresholding) naturally determines which edges appear in the final network
97+
- Artificially subdividing edges defeats the purpose of the consensus approach
98+
- The C++ implementation doesn't subdivide edges - it relies on the sampling to create the network structure
99+
100+
#### 2. Removed Median Vertex Creation Method
101+
The `_add_median_vertices_along_edge` method was completely removed as it's no longer needed.
102+
103+
## Validation
104+
105+
### Test Results
106+
All existing unit tests pass:
107+
- TSW: 17/17 tests passing
108+
- PN: 21/21 tests passing
109+
110+
### Example Improvements
111+
112+
#### TSW Example
113+
**Input:** 3 sequences with 4 mutations between seq1 and seq2
114+
- seq1: AAAA
115+
- seq2: TTTT (4 diffs)
116+
- seq3: AATT (2 diffs from each)
117+
118+
**Before:** Potentially created unnecessary medians
119+
**After:** 3 nodes (3 observed, 0 median), 3 edges
120+
121+
#### PN Example
122+
**Input:** 2 sequences with 4 mutations
123+
- seq1: AAAA
124+
- seq2: TTTT
125+
126+
**Before:** Would create 3 intermediate medians to break the edge into 4 single-mutation steps (5 total nodes)
127+
**After:** Direct edge with distance=4.0 (2 nodes, 1 edge)
128+
129+
## Impact
130+
131+
### TSW
132+
✓ Cleaner networks with only necessary medians
133+
✓ Better matches C++ reference implementation behavior
134+
✓ Improved computational efficiency (fewer vertices to process)
135+
✓ More interpretable network structures
136+
137+
### PN
138+
✓ Networks match the intended consensus sampling design
139+
✓ Edges accurately reflect sampled tree topologies
140+
✓ Reduced node count without loss of information
141+
✓ Aligns with C++ reference implementation
142+
143+
## Technical Details
144+
145+
### TSW Implementation Notes
146+
- Extended dT matrix dynamically as new medians are created
147+
- Vertex map uses tuple of dT distances as key for uniqueness
148+
- Bipartite coloring based on path decomposition from Dress & Huson (2004)
149+
- Delta calculation follows the formula: δ = min{(dT(f,i) + dT(f,j) - d(i,j))/2}
150+
151+
### PN Implementation Notes
152+
- Maintains tree sampling and frequency thresholding
153+
- Edge distances calculated directly from sequence differences
154+
- Random seed support ensures reproducibility
155+
- Consensus threshold (min_edge_frequency) controls network complexity
156+
157+
## References
158+
159+
1. Dress, A. W., & Huson, D. H. (2004). Constructing splits graphs. IEEE/ACM Transactions on Computational Biology and Bioinformatics, 1(3), 109-115.
160+
161+
2. Leigh, J. W., & Bryant, D. (2015). PopART: Full-feature software for haplotype network construction. Methods in Ecology and Evolution, 6(9), 1110-1116.
162+
163+
## Future Considerations
164+
165+
### Potential Enhancements
166+
1. **TSW**: Could optimize dT matrix updates to avoid full matrix expansion
167+
2. **TSW**: Could add configurable pruning strategies
168+
3. **PN**: Could expose more tree generation parameters
169+
4. **Both**: Could add visualization highlighting median vs observed nodes
170+
171+
### Performance Notes
172+
- TSW: O(n³) complexity remains, but with fewer vertices created
173+
- PN: Linear in number of edges sampled, no additional overhead from median creation
174+
- Both algorithms suitable for datasets with < 200 sequences

TSW_PN_REFINEMENT_SUMMARY.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# TSW and PN Algorithm Refinement Summary
2+
3+
## Overview
4+
5+
Successfully refined the Tight Span Walker (TSW) and Parsimony Network (PN) algorithms to eliminate excess intermediate node creation, matching the behavior of the original C++ PopART implementation.
6+
7+
## Problem Statement
8+
9+
### TSW Issue
10+
> "TSW: This method is currently producing an excess of intermediate nodes. Refine TSW to include pruning of inferred nodes that do not bridge real nodes or connect to other inferred nodes that do."
11+
12+
### PN Issue
13+
> "PN: Refine PN algorithm, it currently makes many intermediates so that all edges only represent one mutation each."
14+
15+
## Solutions Implemented
16+
17+
### TSW Refinements ✅
18+
19+
1. **Proper Geodesic Computation**
20+
- Implemented bipartite coloring scheme (green/red vertices)
21+
- Added delta calculation for splitting parameter
22+
- Vertex deduplication via dT vector mapping
23+
- Matches C++ TightSpanWalker.cpp lines 429-708
24+
25+
2. **Median Vertex Pruning**
26+
- Post-processing removes non-bridging medians
27+
- Keeps medians only if they:
28+
- Connect 2+ observed nodes, OR
29+
- Connect observed + median nodes, OR
30+
- Connect 2+ medians (path internal)
31+
32+
**Result:** Cleaner networks with only topologically necessary medians
33+
34+
### PN Refinements ✅
35+
36+
1. **Removed Edge Subdivision**
37+
- Eliminated automatic breaking of multi-mutation edges
38+
- Edges now represent actual sequence distances
39+
- Removed `_add_median_vertices_along_edge` method
40+
41+
2. **Consensus-Based Structure**
42+
- Network structure determined by tree sampling frequency
43+
- Matches C++ ParsimonyNet.cpp behavior
44+
45+
**Result:** Direct edges instead of artificial single-mutation chains
46+
47+
## Test Results
48+
49+
### Comprehensive Validation
50+
-**Total tests:** 582 passed, 0 failed
51+
-**TSW tests:** 17/17 passing (86% coverage)
52+
-**PN tests:** 21/21 passing (97% coverage)
53+
54+
### Example Improvements
55+
56+
**Example 1: PN with 4 mutations**
57+
```
58+
Input: seq1: AAAA, seq2: TTTT (4 differences)
59+
60+
Before: 5 nodes (2 observed + 3 medians), 4 edges of distance=1
61+
After: 2 nodes (2 observed + 0 medians), 1 edge of distance=4
62+
```
63+
64+
**Example 2: TSW star topology**
65+
```
66+
Input: 4 sequences in star pattern (center + 3 tips, 1 mutation each)
67+
68+
Result: 4 nodes (4 observed, 0 medians), fully connected, no excess medians
69+
```
70+
71+
## Code Changes
72+
73+
### Files Modified
74+
1. **src/pypopart/algorithms/tsw.py** (+173 lines)
75+
- New `_add_geodesic_path` with bipartite coloring
76+
- New `_prune_unnecessary_medians` method
77+
- Vertex map for dT vector tracking
78+
79+
2. **src/pypopart/algorithms/parsimony_net.py** (-114 lines)
80+
- Removed `_add_median_vertices_along_edge`
81+
- Simplified `_add_consensus_edges`
82+
83+
## Technical Implementation
84+
85+
### TSW Geodesic Algorithm
86+
```python
87+
# 1. Bipartite coloring
88+
for i in range(n):
89+
if abs(fi + dt_fg + gi - d(i,g)) < epsilon:
90+
green_vertices.add(i) # Closer to f
91+
elif abs(fi + dt_fg - gi) < epsilon:
92+
red_vertices.add(i) # Closer to g
93+
94+
# 2. Delta calculation
95+
delta = min([(dT(f,i) + dT(f,j) - d(i,j))/2 for i,j in green_pairs])
96+
97+
# 3. Recursive vertex creation
98+
if dT(f,g) > delta:
99+
h = create_intermediate_vertex(f, delta)
100+
geodesic(h, g) # Recurse
101+
else:
102+
add_direct_edge(f, g)
103+
104+
# 4. Prune unnecessary medians
105+
remove_non_bridging_medians()
106+
```
107+
108+
### PN Edge Addition
109+
```python
110+
# Old: Subdivided all edges > 1 mutation
111+
for edge, count in edge_counts.items():
112+
if count >= threshold:
113+
add_edge(id1, id2, distance)
114+
if distance > 1:
115+
add_median_vertices() # REMOVED
116+
117+
# New: Direct edges only
118+
for edge, count in edge_counts.items():
119+
if count >= threshold:
120+
add_edge(id1, id2, distance)
121+
```
122+
123+
## Performance Impact
124+
125+
### TSW
126+
- ⚡ Fewer nodes created → faster computation
127+
- 📊 Cleaner visualization
128+
- ✨ Easier network interpretation
129+
130+
### PN
131+
- ⚡ Significantly fewer nodes
132+
- 📊 Edges show actual distances
133+
- ✨ Matches phylogenetic intuition
134+
135+
## C++ Reference Compliance
136+
137+
### TSW vs TightSpanWalker.cpp
138+
- ✅ Geodesic path computation
139+
- ✅ Bipartite coloring (Kf graph)
140+
- ✅ Delta calculation
141+
- ✅ Vertex deduplication
142+
- ✅ Recursive structure
143+
144+
### PN vs ParsimonyNet.cpp
145+
- ✅ Tree sampling approach
146+
- ✅ Edge frequency thresholding
147+
- ✅ No automatic subdivision
148+
- ✅ Consensus network structure
149+
150+
## Documentation
151+
152+
Created comprehensive documentation:
153+
- **ALGORITHM_REFINEMENTS.md** - Detailed technical documentation
154+
- **TSW_PN_REFINEMENT_SUMMARY.md** - This executive summary
155+
156+
## Backward Compatibility
157+
158+
**No breaking changes**
159+
- All existing tests pass
160+
- Public API unchanged
161+
- Behavior aligns with C++ reference
162+
163+
## Conclusion
164+
165+
Both algorithms now accurately implement their C++ reference counterparts, producing cleaner, more accurate haplotype networks with appropriate use of intermediate nodes.
166+
167+
### Key Achievements
168+
✅ TSW produces networks with only necessary medians
169+
✅ PN creates direct edges without artificial subdivision
170+
✅ 100% test pass rate (582/582)
171+
✅ High code coverage (TSW: 86%, PN: 97%)
172+
✅ C++ reference compliance verified
173+
✅ Improved performance and interpretability

0 commit comments

Comments
 (0)