-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add 1-indel search via bidirectional DFS in Rust #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
46a7f2d
feat: add 1-indel search via exhaustive bidirectional DFS in Rust
27e48f1
test: add indel search test covering 1-deletion and 1-insertion cases
eb5e4fb
test: add terminal deletion guard test; exclude pepidx from git
09a6757
fix: restrict terminal deletion guard to protein boundaries only
Roman-Young 1ca61d3
refactor: move indel.py to tests/ as reference implementation
Roman-Young e0b45ec
feat: add dedicated Indels column to output schema
Roman-Young eb8533c
fix: off-by-one in terminal deletion guard blocked valid matches
Roman-Young 4a78675
test: strengthen indel test suite per PR #24 review item 4
Roman-Young 5edd330
fix: make indel_search's index-build message explicit
Roman-Young 190125b
docs: add --max_indels CLI flag and README section for indel mode
Roman-Young 7bd7092
docs: match indel section's caveat placement to house style
Roman-Young 63cbb92
fix: raise ValueError for max_indels combined with best_match or coun…
Roman-Young 231bcb2
docs: clarify indel property test's mutation-helper naming
Roman-Young b8f349d
fix: block query-terminal deletions and insertions unconditionally
Roman-Young 089fc97
fix: emit one edit-count column per search mode; reject indel + disco…
Roman-Young File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| .~* | ||
| *.db | ||
| *.pepidx | ||
| *.pickle | ||
| *.pkl | ||
| *.pyc | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |
| import polars as pl | ||
| from pathlib import Path | ||
| from Bio import SeqIO | ||
| from ._rs import rs_preprocess, rs_match, rs_discontinuous, rs_metadata, rs_match_counts | ||
| from ._rs import rs_preprocess, rs_match, rs_discontinuous, rs_metadata, rs_match_counts, rs_indel_match | ||
|
|
||
| VALID_OUTPUT_FORMATS = ['dataframe', 'csv', 'tsv', 'xlsx', 'json'] | ||
| FASTA_EXTENSIONS = { | ||
|
|
@@ -31,6 +31,7 @@ def __init__( | |
| query, | ||
| proteome_file, | ||
| max_mismatches=0, | ||
| max_indels=0, | ||
| k=0, | ||
| preprocessed_files_path='.', | ||
| best_match=False, | ||
|
|
@@ -53,6 +54,20 @@ def __init__( | |
| if k != 0 and k < 2: | ||
| raise ValueError('k must be >= 2.') | ||
|
|
||
| if max_indels > 1: | ||
| raise ValueError('max_indels > 1 is not yet supported. Only max_indels=1 has been validated.') | ||
|
|
||
| if max_indels > 0 and max_mismatches > 0: | ||
| raise ValueError('max_indels and max_mismatches are mutually exclusive.') | ||
|
|
||
| if max_indels > 0 and best_match: | ||
| raise ValueError('max_indels and best_match are not yet supported together.') | ||
|
|
||
| if max_indels > 0 and counts_only: | ||
| raise ValueError('max_indels and counts_only are not yet supported together.') | ||
|
|
||
| self.max_indels = max_indels | ||
|
|
||
| if output_format not in VALID_OUTPUT_FORMATS: | ||
| raise ValueError( | ||
| f'Invalid output format. Choose from: {VALID_OUTPUT_FORMATS}' | ||
|
|
@@ -73,6 +88,8 @@ def __init__( | |
|
|
||
| self.query = self._parse_query(query) | ||
| self.discontinuous_epitopes = self._find_discontinuous_epitopes() | ||
| if self.max_indels > 0 and self.discontinuous_epitopes: | ||
| raise ValueError('max_indels and discontinuous epitopes are not supported together.') | ||
| self.query = self._clean_query() | ||
| if not self.query and not self.discontinuous_epitopes: | ||
| raise ValueError('Query is empty.') | ||
|
|
@@ -128,7 +145,9 @@ def match(self): | |
| return output_matches(df, self.output_format, self.output_name) | ||
|
|
||
| if self.query: | ||
| if self.best_match and self.k_specified: | ||
| if self.max_indels > 0: | ||
| linear_df = self.indel_search() | ||
| elif self.best_match and self.k_specified: | ||
| results = self._search(self.k, self.max_mismatches) | ||
| linear_df = self._best_match_filter(self._to_dataframe(results)) | ||
| elif self.best_match: | ||
|
|
@@ -155,6 +174,19 @@ def match(self): | |
| else: | ||
| output_matches(df, self.output_format, self.output_name) | ||
|
|
||
| def indel_search(self): | ||
| min_len = min(len(seq) for _, seq in self.query) | ||
| k = max(2, min_len // (self.max_indels + 1)) | ||
| pepidx_path = self._pepidx_path(k) | ||
| if not os.path.isfile(pepidx_path): | ||
| print(f"No preprocessed file found for k={k}, building index now " | ||
| f"(this may take a moment)...") | ||
| rs_preprocess(self.proteome_file, k, pepidx_path) | ||
| print(f"Searching {len(self.query)} peptides against {self.proteome_name} " | ||
| f"(k={k}, max_indels={self.max_indels})...") | ||
| results = rs_indel_match(pepidx_path, self.query, self.max_indels) | ||
| return self._to_dataframe(results, is_indels=True) | ||
|
|
||
| def _pepidx_path(self, k): | ||
| return os.path.join( | ||
| self.preprocessed_files_path, f'{self.proteome_name}_{k}mers.pepidx' | ||
|
|
@@ -327,20 +359,31 @@ def _metadata_table(self) -> pl.DataFrame: | |
| pl.col('SwissProt Reviewed').cast(pl.Int64).cast(pl.Boolean), | ||
| ]) | ||
|
|
||
| FINAL_COLUMNS = [ | ||
| 'Query ID','Query Sequence','Matched Sequence','Protein ID','Protein Name','Species', | ||
| 'Taxon ID','Gene','Mismatches','Mutated Positions','Index start','Index end', | ||
| 'Protein Existence Level','Gene Priority','SwissProt Reviewed', | ||
| ] | ||
| def _final_columns(self, is_indels): | ||
| # One edit-count column per mode, in a fixed position: Indels for indel search, | ||
| # Mismatches for every other mode. Never both — an all-zero twin column would | ||
| # imply a search that didn't run. | ||
| edit_col = 'Indels' if is_indels else 'Mismatches' | ||
| return [ | ||
| 'Query ID','Query Sequence','Matched Sequence','Protein ID','Protein Name','Species', | ||
| 'Taxon ID','Gene', edit_col, 'Mutated Positions','Index start','Index end', | ||
| 'Protein Existence Level','Gene Priority','SwissProt Reviewed', | ||
| ] | ||
|
|
||
| def _to_dataframe(self, cols): | ||
| def _to_dataframe(self, cols, is_indels=False): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, we already have a flag here, so popping the indel column in should be easy. |
||
| """Build the result DataFrame from columnar Rust output, reconstructing protein | ||
| metadata via a single join (instead of cloning it into every hit row).""" | ||
| qid, qseq, matched, pnum, mm, mutated, istart, iend = cols | ||
|
|
||
| # rs_indel_match packs the indel count into the same slot rs_match/rs_discontinuous | ||
| # use for mismatches, so the values are identical in shape — only the column name | ||
| # differs by mode (Indels vs Mismatches), and only one is ever emitted. | ||
| edit_col = 'Indels' if is_indels else 'Mismatches' | ||
| final_columns = self._final_columns(is_indels) | ||
|
|
||
| if not qid: | ||
| schema = {c: pl.Utf8 for c in self.FINAL_COLUMNS} | ||
| for c in ('Mismatches','Index start','Index end','Protein Existence Level','Gene Priority'): | ||
| schema = {c: pl.Utf8 for c in final_columns} | ||
| for c in (edit_col, 'Index start', 'Index end', 'Protein Existence Level', 'Gene Priority'): | ||
| schema[c] = pl.Int64 | ||
| schema['SwissProt Reviewed'] = pl.Boolean | ||
| return pl.DataFrame(schema=schema) | ||
|
|
@@ -350,7 +393,7 @@ def _to_dataframe(self, cols): | |
| 'Query Sequence': qseq, | ||
| 'Matched Sequence': matched, | ||
| 'protein_num': pl.Series(pnum, dtype=pl.UInt32), | ||
| 'Mismatches': pl.Series(mm, dtype=pl.Int64), | ||
| edit_col: pl.Series(mm, dtype=pl.Int64), | ||
| 'Mutated Positions': mutated, | ||
| 'Index start': pl.Series(istart, dtype=pl.Int64), | ||
| 'Index end': pl.Series(iend, dtype=pl.Int64), | ||
|
|
@@ -366,4 +409,4 @@ def _to_dataframe(self, cols): | |
| .alias('Protein ID') | ||
| ) | ||
|
|
||
| return df.drop('Sequence Version').select(self.FINAL_COLUMNS) | ||
| return df.drop('Sequence Version').select(final_columns) | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.