@@ -254,3 +254,59 @@ def test_indel_positions_insertion_end_to_end(tmp_path):
254254 row = df .filter (pl .col ('Matched Sequence' ) == 'NALVEXATRFC' )
255255 assert row .height == 1
256256 assert row ['Indel Positions' ].item () == 'i: X[6]'
257+
258+
259+ def test_indel_honors_explicit_k_up_to_optimal_and_reuses_table (tmp_path , capsys ):
260+ # Issue #28: a user who already built a k-mer table (e.g. for a mismatch run) and
261+ # passes that same k to indel search should have it reused, not silently swapped
262+ # for the optimized k and rebuilt. Query len 10, max_indels=1 -> optimized k = 5;
263+ # k=3 <= 5 so it is honored, and the pre-built 3-mer index is used as-is.
264+ proteome_path = tmp_path / 'proteome.fasta'
265+ proteome_path .write_text ('>ProtDel\n MKVNALVETRFCGHI\n ' )
266+ # the user "already has" a 3-mer table from an earlier run
267+ Matcher (query = ['NALVEATRFC' ], proteome_file = str (proteome_path ), max_mismatches = 0 ,
268+ k = 3 , preprocessed_files_path = str (tmp_path ), output_format = 'dataframe' ).match ()
269+ assert (tmp_path / 'proteome_3mers.pepidx' ).exists ()
270+ capsys .readouterr () # discard the pre-build output
271+ df = Matcher (query = ['NALVEATRFC' ], proteome_file = str (proteome_path ), max_indels = 1 ,
272+ k = 3 , preprocessed_files_path = str (tmp_path ), output_format = 'dataframe' ).match ()
273+ out = capsys .readouterr ().out
274+ assert '(k=3,' in out # honored the user's k, not the optimal 5
275+ assert 'No preprocessed file' not in out # reused the existing table, no rebuild
276+ assert 'NALVETRFC' in df ['Matched Sequence' ].to_list () # recall preserved at k=3
277+
278+
279+ def test_indel_clamps_k_above_optimal_and_warns (tmp_path , capsys ):
280+ # An explicit k above the pigeonhole optimal drops below max_indels+1 disjoint
281+ # seeds and would forfeit complete recall, so indel search clamps it to the
282+ # optimal and warns. Query len 10, max_indels=1 -> optimized k = 5; k=7 (still
283+ # <= min_len, so it passes the constructor guard) is clamped down to 5.
284+ proteome_path = tmp_path / 'proteome.fasta'
285+ proteome_path .write_text ('>ProtDel\n MKVNALVETRFCGHI\n ' )
286+ df = Matcher (query = ['NALVEATRFC' ], proteome_file = str (proteome_path ), max_indels = 1 ,
287+ k = 7 , preprocessed_files_path = str (tmp_path ), output_format = 'dataframe' ).match ()
288+ out = capsys .readouterr ().out
289+ assert 'Requested k=7 exceeds k=5' in out # warned about the clamp
290+ assert '(k=5,' in out # searched at the clamped optimal
291+ assert 'NALVETRFC' in df ['Matched Sequence' ].to_list () # recall still complete
292+ assert (tmp_path / 'proteome_5mers.pepidx' ).exists () # built the optimal index
293+ assert not (tmp_path / 'proteome_7mers.pepidx' ).exists () # never the too-large one
294+
295+
296+ def test_indel_unspecified_k_derives_optimal (tmp_path , capsys ):
297+ # With no k passed, indel search derives the pigeonhole optimal: max(2, 10//2) = 5.
298+ proteome_path = tmp_path / 'proteome.fasta'
299+ proteome_path .write_text ('>ProtDel\n MKVNALVETRFCGHI\n ' )
300+ Matcher (query = ['NALVEATRFC' ], proteome_file = str (proteome_path ), max_indels = 1 ,
301+ preprocessed_files_path = str (tmp_path ), output_format = 'dataframe' ).match ()
302+ out = capsys .readouterr ().out
303+ assert '(k=5,' in out
304+ assert (tmp_path / 'proteome_5mers.pepidx' ).exists ()
305+
306+
307+ def test_indel_k_exceeding_shortest_query_raises ():
308+ # The constructor rejects an explicit k larger than the shortest query peptide
309+ # (shorter peptides would be silently skipped). The guard is global, so it also
310+ # governs indel search -- an explicit k reaching indel_search is always <= min_len.
311+ with pytest .raises (ValueError , match = 'cannot exceed the shortest query peptide' ):
312+ Matcher (query = ['NALVEATRFC' ], proteome_file = 'unused.fasta' , max_indels = 1 , k = 11 )
0 commit comments