@@ -22,6 +22,44 @@ def output_matches(df: pl.DataFrame, output_format: str, output_name: str) -> No
2222 elif output_format == 'json' :
2323 df .write_json (path )
2424
25+
26+ def _indel_edit (query , matched ):
27+ """Return (kind, residues, low, high) for a single-indel match, or None for an
28+ exact match: kind 'd'/'i', the deleted query or inserted protein residue(s), and
29+ [low, high] the inclusive 1-based range of valid positions (a run of equivalent
30+ positions in a repeat)."""
31+ if matched == query :
32+ return None
33+ L = len (query )
34+ if len (matched ) < L :
35+ # interior positions only — query-terminal deletions are barred
36+ positions = [i for i in range (2 , L ) if query [:i - 1 ] + query [i :] == matched ]
37+ if not positions :
38+ return None
39+ return ('d' , query [positions [0 ] - 1 ], positions [0 ], positions [- 1 ])
40+ # interior matched residues only — boundary insertions are barred
41+ positions , residue = [], None
42+ for k in range (1 , len (matched ) - 1 ):
43+ if matched [:k ] + matched [k + 1 :] == query :
44+ positions .append (k + 1 ) # 1-based query position the inserted residue precedes
45+ residue = matched [k ]
46+ if not positions :
47+ return None
48+ return ('i' , residue , positions [0 ], positions [- 1 ])
49+
50+
51+ def format_indel_positions (query , matched ):
52+ """Render the Indel Positions column, e.g. 'd: A[6]', 'i: X[2,4]', or '[]' for
53+ an exact match. Positions are 1-based; a range [low,high] collapses to [n] when
54+ the position is unambiguous."""
55+ edit = _indel_edit (query , matched )
56+ if edit is None :
57+ return '[]'
58+ kind , residues , low , high = edit
59+ span = f'[{ low } ]' if low == high else f'[{ low } ,{ high } ]'
60+ return f'{ kind } : { residues } { span } '
61+
62+
2563class Matcher :
2664 """Searches query peptides against a preprocessed proteome index
2765 and returns matches as a Polars DataFrame or output file."""
@@ -360,13 +398,15 @@ def _metadata_table(self) -> pl.DataFrame:
360398 ])
361399
362400 def _final_columns (self , is_indels ):
363- # One edit-count column per mode, in a fixed position: Indels for indel search,
364- # Mismatches for every other mode. Never both — an all-zero twin column would
365- # imply a search that didn't run.
401+ # One edit-count column and one edit-position column per mode, in fixed
402+ # positions: Indels / Indel Positions for indel search, Mismatches / Mutated
403+ # Positions for every other mode. Never both — an all-zero/empty twin column
404+ # would imply a search that didn't run.
366405 edit_col = 'Indels' if is_indels else 'Mismatches'
406+ pos_col = 'Indel Positions' if is_indels else 'Mutated Positions'
367407 return [
368408 'Query ID' ,'Query Sequence' ,'Matched Sequence' ,'Protein ID' ,'Protein Name' ,'Species' ,
369- 'Taxon ID' ,'Gene' , edit_col , 'Mutated Positions' ,'Index start' ,'Index end' ,
409+ 'Taxon ID' ,'Gene' , edit_col , pos_col ,'Index start' ,'Index end' ,
370410 'Protein Existence Level' ,'Gene Priority' ,'SwissProt Reviewed' ,
371411 ]
372412
@@ -379,6 +419,7 @@ def _to_dataframe(self, cols, is_indels=False):
379419 # use for mismatches, so the values are identical in shape — only the column name
380420 # differs by mode (Indels vs Mismatches), and only one is ever emitted.
381421 edit_col = 'Indels' if is_indels else 'Mismatches'
422+ pos_col = 'Indel Positions' if is_indels else 'Mutated Positions'
382423 final_columns = self ._final_columns (is_indels )
383424
384425 if not qid :
@@ -388,13 +429,21 @@ def _to_dataframe(self, cols, is_indels=False):
388429 schema ['SwissProt Reviewed' ] = pl .Boolean
389430 return pl .DataFrame (schema = schema )
390431
432+ # In indel mode the edit position is derivable from (query, matched), so we
433+ # compute Indel Positions here rather than in Rust; miss rows (no match) stay null.
434+ if is_indels :
435+ positions = [format_indel_positions (q , m ) if m is not None else None
436+ for q , m in zip (qseq , matched )]
437+ else :
438+ positions = mutated
439+
391440 base = pl .DataFrame ({
392441 'Query ID' : qid ,
393442 'Query Sequence' : qseq ,
394443 'Matched Sequence' : matched ,
395444 'protein_num' : pl .Series (pnum , dtype = pl .UInt32 ),
396445 edit_col : pl .Series (mm , dtype = pl .Int64 ),
397- 'Mutated Positions' : mutated ,
446+ pos_col : positions ,
398447 'Index start' : pl .Series (istart , dtype = pl .Int64 ),
399448 'Index end' : pl .Series (iend , dtype = pl .Int64 ),
400449 })
0 commit comments