-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtutorial_4_advanced.py
More file actions
199 lines (171 loc) · 5.95 KB
/
tutorial_4_advanced.py
File metadata and controls
199 lines (171 loc) · 5.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#' # MAlign Tutorial 4: Advanced Features
#'
#' This tutorial covers advanced features including block detection,
#' evaluation metrics, multi-sequence dispatch, and performance tips.
#'
#' ## Block Detection and Column Merging
#'
#' In phonological alignment, certain sound changes produce 1-to-many
#' correspondences that appear as complementary gaps:
#'
#' ```
#' Diphthongization (a -> je): Metathesis (tr -> rt):
#' a - t r -
#' j e - r t
#' ```
#'
#' MAlign detects these **block patterns** and merges them into compound
#' symbols (tuples).
#'
#' ### Automatic Merging via align()
import malign
# Diphthongization: "a" becomes "je"
alms = malign.align(
[["a"], ["j", "e"]],
k=1,
merge_blocks=True,
)
print("Diphthongization (merge_blocks=True):")
for i, seq in enumerate(alms[0].seqs):
print(f" Seq {i}: {list(seq)}")
# Seq 0: ["a"]
# Seq 1: [("j", "e")] -- compound tuple
#' ### Manual Merging
# Create an alignment with complementary gaps
aln = malign.Alignment(
seqs=[("a", "-", "x"), ("j", "e", "x")],
score=1.0,
)
merged = malign.merge_alignment_blocks(aln)
print("\nManual merge:")
print(f" Before: {[list(s) for s in aln.seqs]}")
print(f" After: {[list(s) for s in merged.seqs]}")
print(f" Score preserved: {merged.score == aln.score}")
#' ### How Block Detection Works
#'
#' 1. Columns classified as FULL (no gaps), PARTIAL (some gaps), ALL_GAP
#' 2. Maximal runs of PARTIAL columns found
#' 3. Runs extended to adjacent FULL columns if within `max_block_size`
#' 4. Within a block per sequence:
#' - 0 non-gap symbols -> gap symbol
#' - 1 symbol -> unwrapped
#' - 2+ symbols -> tuple
from malign.blocks import detect_blocks
# Detect blocks directly
seqs = [("a", "-", "x", "b", "-"), ("j", "e", "x", "-", "c")]
blocks = detect_blocks(seqs)
print(f"\nDetected blocks in 5-column alignment: {blocks}")
# [(0, 1), (3, 4)] -- two 2-column blocks
#' ### Configuring max_block_size
#'
#' The default `max_block_size=2` handles diphthongization and simple
#' metathesis. Increase for longer patterns:
# F P F pattern with max_block_size=2 vs 3
seqs = [("a", "-", "b"), ("j", "e", "b")]
print(f"\nF P F with max_block_size=2: {detect_blocks(seqs, max_block_size=2)}")
print(f"F P F with max_block_size=3: {detect_blocks(seqs, max_block_size=3)}")
#' ## Evaluation Metrics
#'
#' MAlign provides standard alignment evaluation metrics:
# Create a "gold standard" alignment
gold = malign.Alignment(
seqs=[("A", "C", "G", "T"), ("A", "C", "G", "T")],
score=4.0,
)
# Create a predicted alignment (one column differs)
predicted = malign.Alignment(
seqs=[("A", "C", "G", "T"), ("A", "G", "C", "T")],
score=2.0,
)
accuracy = malign.alignment_accuracy(predicted, gold)
precision, recall = malign.alignment_precision_recall(predicted, gold)
f1 = malign.alignment_f1(predicted, gold)
print("\nEvaluation metrics:")
print(f" Accuracy: {accuracy:.2%}")
print(f" Precision: {precision:.2%}")
print(f" Recall: {recall:.2%}")
print(f" F1: {f1:.2%}")
#' ## Multi-Sequence Alignment Dispatch
#'
#' MAlign automatically selects the best alignment strategy:
#'
#' | Sequences | Strategy | Quality |
#' |---|---|---|
#' | N=2 | Direct pairwise | Optimal |
#' | N=3-4 (small grid) | True N-dimensional | Globally optimal |
#' | N=5+ or large grid | UPGMA progressive | Near-optimal |
#'
#' The dispatch is transparent -- just call `align()`:
# 2 sequences: direct pairwise
alms2 = malign.align(["ACGT", "AGCT"], k=1)
print(f"\n2-seq alignment: {len(alms2[0].seqs[0])} columns")
# 3 sequences: true N-dimensional (if grid is small enough)
alms3 = malign.align(
[["k", "a", "t"], ["g", "a", "t", "o"], ["k", "a", "t", "z"]],
k=1,
)
print(f"3-seq alignment: {len(alms3[0].seqs[0])} columns")
#' ## Algorithm Selection: ANW vs YenKSP
#'
#' ```
#' How many sequences?
#' |-- 2-4 sequences
#' | |-- k <= 10? -> Use YenKSP (maximum quality)
#' | |-- k > 10? -> Use ANW (faster for large k)
#' |-- 5+ sequences -> Use ANW (YenKSP too slow)
#' ```
# Small problem, want maximum quality
alms = malign.align(["cat", "bat"], k=5, method="yenksp")
print(f"\nYenKSP (2 seqs, k=5): {len(alms)} alignments")
# Large k: use ANW
alms = malign.align(["cat", "bat"], k=20, method="anw")
print(f"ANW (2 seqs, k=20): {len(alms)} alignments")
#' ## Full Pipeline Example
#'
#' Combining multiple features for a realistic linguistic workflow:
# Step 1: Learn a matrix from cognate pairs
pairs = [
(["p", "a", "t", "a"], ["b", "a", "d", "a"]),
(["p", "a", "k", "a"], ["b", "a", "g", "a"]),
(["t", "a", "p", "a"], ["d", "a", "b", "a"]),
(["k", "a", "t", "a"], ["g", "a", "d", "a"]),
]
matrix = malign.bootstrap_matrix(pairs, max_iter=10)
# Step 2: Align new data with learned matrix
alms = malign.align(
[["p", "a", "t"], ["b", "a", "d"]],
k=3,
matrix=matrix,
merge_blocks=True,
)
print("\nFull pipeline:")
print(f" Learned matrix with {len(matrix.scores)} scores")
print(f" Got {len(alms)} alignments")
print(malign.tabulate_alms(alms[:2]))
#' ## Performance Tips
#'
#' 1. **Choose the right algorithm**: Use `anw` for 5+ sequences or k > 10
#' 2. **Limit k**: Don't compute more alternatives than needed
#' 3. **Use learned matrices**: They often produce faster convergence
#' 4. **Batch with multiprocessing**: Align multiple sets in parallel
#'
#' ```python
#' from multiprocessing import Pool
#'
#' def align_task(sequences):
#' return malign.align(sequences, k=3, method="anw")
#'
#' with Pool(4) as pool:
#' results = pool.map(align_task, sequence_batches)
#' ```
#'
#' ## Summary
#'
#' | Feature | API |
#' |---|---|
#' | Block merging | `align(merge_blocks=True)` or `merge_alignment_blocks()` |
#' | Evaluation | `alignment_accuracy()`, `alignment_f1()`, etc. |
#' | N-dim alignment | Automatic for N<=4 via `align()` |
#' | Progressive | Automatic for N>4 via `align()` |
#' | Prior-guided learning | `bootstrap_matrix(prior_matrix=...)` |
#' | Block-aware learning | `bootstrap_matrix(block_merge=True)` |