Skip to content

Commit 3a0c2c1

Browse files
Unify matcher path coordinates to genomic positions
Wrap legacy AncestorMatcher to convert site-index edges to genomic positions, mapping left=positions[0] to 0 for consistency. Update Matcher._match_one to use genomic positions directly. Both matchers now pass all 16 fixture tests identically.
1 parent 83af80a commit 3a0c2c1

2 files changed

Lines changed: 48 additions & 20 deletions

File tree

tests/test_matcher_fixtures.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -216,25 +216,25 @@ def setup(self):
216216
def test_copy_node2(self):
217217
"""Query [0,1,0] exactly matches node 2."""
218218
path, mutations = _match(self.ts, self.positions, [0, 1, 0], self.am)
219-
assert path == [(10.0, 100.0, 2)]
219+
assert path == [(0.0, 100.0, 2)]
220220
assert mutations == []
221221

222222
def test_copy_node3(self):
223223
"""Query [1,0,0] exactly matches node 3."""
224224
path, mutations = _match(self.ts, self.positions, [1, 0, 0], self.am)
225-
assert path == [(10.0, 100.0, 3)]
225+
assert path == [(0.0, 100.0, 3)]
226226
assert mutations == []
227227

228228
def test_mixed(self):
229229
"""Query [1,1,0] recombines between nodes 2 and 3."""
230230
path, mutations = _match(self.ts, self.positions, [1, 1, 0], self.am)
231-
assert path == [(20.0, 100.0, 2), (10.0, 20.0, 3)]
231+
assert path == [(20.0, 100.0, 2), (0.0, 20.0, 3)]
232232
assert mutations == []
233233

234234
def test_ancestral(self):
235235
"""Query [0,0,0] (all ancestral) copies from virtual root."""
236236
path, mutations = _match(self.ts, self.positions, [0, 0, 0], self.am)
237-
assert path == [(10.0, 100.0, 1)]
237+
assert path == [(0.0, 100.0, 1)]
238238
assert mutations == []
239239

240240

@@ -248,19 +248,19 @@ def setup(self):
248248
def test_copy_leaf_node3(self):
249249
"""Query [1,0,1,0] exactly matches node 3."""
250250
path, mutations = _match(self.ts, self.positions, [1, 0, 1, 0], self.am)
251-
assert path == [(10.0, 100.0, 3)]
251+
assert path == [(0.0, 100.0, 3)]
252252
assert mutations == []
253253

254254
def test_copy_leaf_node4(self):
255255
"""Query [1,0,0,1] exactly matches node 4."""
256256
path, mutations = _match(self.ts, self.positions, [1, 0, 0, 1], self.am)
257-
assert path == [(10.0, 100.0, 4)]
257+
assert path == [(0.0, 100.0, 4)]
258258
assert mutations == []
259259

260260
def test_copy_internal(self):
261261
"""Query [1,0,0,0] matches internal node 2."""
262262
path, mutations = _match(self.ts, self.positions, [1, 0, 0, 0], self.am)
263-
assert path == [(10.0, 100.0, 2)]
263+
assert path == [(0.0, 100.0, 2)]
264264
assert mutations == []
265265

266266
def test_mixed_leaves(self):
@@ -269,7 +269,7 @@ def test_mixed_leaves(self):
269269
assert path == [
270270
(40.0, 100.0, 4),
271271
(30.0, 40.0, 3),
272-
(10.0, 30.0, 1),
272+
(0.0, 30.0, 1),
273273
]
274274
assert mutations == []
275275

@@ -284,19 +284,19 @@ def setup(self):
284284
def test_copy_A(self):
285285
"""Query [1,0,0,0] copies entirely from A (node 2)."""
286286
path, mutations = _match(self.ts, self.positions, [1, 0, 0, 0], self.am)
287-
assert path == [(10.0, 100.0, 2)]
287+
assert path == [(0.0, 100.0, 2)]
288288
assert mutations == []
289289

290290
def test_copy_B_site2(self):
291291
"""Query [0,0,1,0] copies from B (node 3)."""
292292
path, mutations = _match(self.ts, self.positions, [0, 0, 1, 0], self.am)
293-
assert path == [(10.0, 100.0, 3)]
293+
assert path == [(0.0, 100.0, 3)]
294294
assert mutations == []
295295

296296
def test_recombination(self):
297297
"""Query [1,0,1,0] matches B's haplotype, requiring recombination."""
298298
path, mutations = _match(self.ts, self.positions, [1, 0, 1, 0], self.am)
299-
assert path == [(30.0, 100.0, 3), (10.0, 30.0, 2)]
299+
assert path == [(30.0, 100.0, 3), (0.0, 30.0, 2)]
300300
assert mutations == []
301301

302302
def test_partial_missing(self):
@@ -316,24 +316,24 @@ def setup(self):
316316
def test_copy_C(self):
317317
"""Query [1,1,1,0] exactly matches deepest node C (4)."""
318318
path, mutations = _match(self.ts, self.positions, [1, 1, 1, 0], self.am)
319-
assert path == [(10.0, 100.0, 4)]
319+
assert path == [(0.0, 100.0, 4)]
320320
assert mutations == []
321321

322322
def test_copy_B(self):
323323
"""Query [1,1,0,0] matches node B (3)."""
324324
path, mutations = _match(self.ts, self.positions, [1, 1, 0, 0], self.am)
325-
assert path == [(10.0, 100.0, 3)]
325+
assert path == [(0.0, 100.0, 3)]
326326
assert mutations == []
327327

328328
def test_copy_A(self):
329329
"""Query [1,0,0,0] matches node A (2)."""
330330
path, mutations = _match(self.ts, self.positions, [1, 0, 0, 0], self.am)
331-
assert path == [(10.0, 100.0, 2)]
331+
assert path == [(0.0, 100.0, 2)]
332332
assert mutations == []
333333

334334
def test_novel_haplotype(self):
335335
"""Query [0,0,0,1] doesn't match any node — copies virtual root
336336
with a mutation."""
337337
path, mutations = _match(self.ts, self.positions, [0, 0, 0, 1], self.am)
338-
assert path == [(10.0, 100.0, 1)]
338+
assert path == [(0.0, 100.0, 1)]
339339
assert mutations == [(40.0, 1)]

tsinfer/matching.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,11 @@ def _match_one(self, job, reader) -> tuple:
288288
if self._use_matcher2:
289289
matcher = AncestorMatcher2(self._matcher_indexes, self._rho, self._mu)
290290
else:
291-
matcher = _tsinfer.AncestorMatcher(self._tsb, self._rho, self._mu)
291+
matcher = _LegacyMatcherWrapper(
292+
_tsinfer.AncestorMatcher(self._tsb, self._rho, self._mu),
293+
pos,
294+
seq_len,
295+
)
292296
t_init = time_.monotonic() - t0
293297

294298
t0 = time_.monotonic()
@@ -307,12 +311,10 @@ def _match_one(self, job, reader) -> tuple:
307311
left, right, parent = matcher.find_path(h, start, end, match_out)
308312
t_match = time_.monotonic() - t1
309313

310-
# Convert site-index edges to absolute-position PathSegments
314+
# Build PathSegments from genomic-position edges
311315
path = []
312316
for li, ri, pi in zip(left, right, parent):
313-
l_pos = float(pos[li])
314-
r_pos = float(pos[ri]) if ri < num_sites else float(seq_len)
315-
path.append(PathSegment(left=l_pos, right=r_pos, parent=int(pi)))
317+
path.append(PathSegment(left=float(li), right=float(ri), parent=int(pi)))
316318

317319
# Detect mutations and convert to absolute positions
318320
in_range = np.zeros(num_sites, dtype=bool)
@@ -516,6 +518,32 @@ def add_vestigial_root(ts):
516518
return tables.tree_sequence()
517519

518520

521+
class _LegacyMatcherWrapper:
522+
"""Wraps _tsinfer.AncestorMatcher to return genomic positions."""
523+
524+
def __init__(self, matcher, positions, seq_len):
525+
self._matcher = matcher
526+
self._positions = positions
527+
self._seq_len = seq_len
528+
529+
def find_path(self, h, start, end, match_out):
530+
left, right, parent = self._matcher.find_path(h, start, end, match_out)
531+
num_sites = len(self._positions)
532+
left_pos = self._positions[left]
533+
left_pos = np.where(left_pos == self._positions[0], 0, left_pos)
534+
clipped = np.minimum(right, num_sites - 1)
535+
right_pos = np.where(right < num_sites, self._positions[clipped], self._seq_len)
536+
return left_pos, right_pos, parent
537+
538+
@property
539+
def total_memory(self):
540+
return self._matcher.total_memory
541+
542+
@property
543+
def mean_traceback_size(self):
544+
return self._matcher.mean_traceback_size
545+
546+
519547
class MatcherIndexes(_tsinfer.MatcherIndexes):
520548
"""Wrapper around the C MatcherIndexes, built from a tree sequence."""
521549

0 commit comments

Comments
 (0)