Skip to content
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

feat: speed improvements for primer pair hit building from single primer hits #99

Merged
merged 13 commits into from
Jan 10, 2025

Conversation

ameynert
Copy link
Contributor

@ameynert ameynert commented Dec 5, 2024

Closes #94

  1. OffTargetDetector._to_amplicons signature changes from
def _to_amplicons(lefts: list[BwaHit], rights: list[BwaHit], max_len: int) -> list[Span]:

to

def _to_amplicons(
    positive_hits: list[BwaHit], negative_hits: list[BwaHit], max_len: int, strand: Strand
) -> list[Span]:
  • Amplicons are built from a list of single primer hits on the positive strand and a list of single primer hits on the negative strand.
  • New parameter strand is used to set the strand attribute on each of the returned Spans for the amplicon hits.
  • Validates that all positive hits are on the positive strand and all negative hits are on the negative strand.
  • Validates that all hits are on the same reference (e.g. contig/chr).
  1. The existing unit test for OffTargetDetector.to_amplicons is split into two, one where no error should be raised, and one where a ValueError should be raised, with expected error messages.

  2. OffTargetDetector._build_off_target_result does not change signature.

  • exits early setting pass = False to the returned OffTargetResult if there are too many hits for either the left or the right primer.
  • hits are separated by reference (e.g. contig/chr), left/right, and strand (positive/negative) using defaultdict collections
  • this enables passing hits that are on the same strand and in the correct relative orientation to each other to _to_amplicons, along with a known amplicon strand - positive when the positive hits are from the left primer of a pair and the negative hits are from the right primer, and negative in the inverse.
  • the full set of left and right primer hits are still returned if OffTargetDetector._keep_primer_spans is True.
  1. Unit test for OffTargetDetector._build_off_target_result is added.

Copy link

coderabbitai bot commented Dec 5, 2024

Walkthrough

The pull request modifies the OffTargetDetector class in prymer/offtarget/offtarget_detector.py, enhancing off-target detection for primer pairs. A new type alias, ReferenceName, is introduced. The _build_off_target_result method is refactored to improve hit handling, utilizing defaultdict to categorize hits by strand and reference name. It now checks hit counts against a maximum threshold, adjusting the passes attribute of OffTargetResult accordingly. The _to_amplicons method is updated to accept positive and negative hits along with strand type, enforcing validation and raising ValueError for incorrect conditions. In the test file tests/offtarget/test_offtarget.py, new tests are added, including test_build_off_target_result and test_to_amplicons_value_error, while existing tests are refined to align with the changes in method signatures and parameters.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (1)
tests/offtarget/test_offtarget.py (1)

240-284: Update parameter names for clarity

Changing parameter names from left and right to positive and negative improves clarity regarding strand orientation.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between f62c4b5 and 9ac73dc.

📒 Files selected for processing (2)
  • prymer/offtarget/offtarget_detector.py (4 hunks)
  • tests/offtarget/test_offtarget.py (2 hunks)
🔇 Additional comments (2)
prymer/offtarget/offtarget_detector.py (1)

497-500: Validate hit strands before processing

The checks ensure positive hits are on the positive strand and negative hits on the negative strand, which is good. No issues here.

tests/offtarget/test_offtarget.py (1)

175-235: Well-structured test for _build_off_target_result

The new test test_build_off_target_result is comprehensive and correctly verifies the function's behavior with various hits.

prymer/offtarget/offtarget_detector.py Outdated Show resolved Hide resolved
Copy link

codecov bot commented Dec 5, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 96.73%. Comparing base (f62c4b5) to head (0036b83).
Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #99      +/-   ##
==========================================
+ Coverage   96.66%   96.73%   +0.07%     
==========================================
  Files          26       26              
  Lines        1708     1746      +38     
  Branches      333      349      +16     
==========================================
+ Hits         1651     1689      +38     
  Misses         31       31              
  Partials       26       26              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (1)
prymer/offtarget/offtarget_detector.py (1)

477-506: Enhance error messages with hit details.

Error messages could be more specific by including which hits are invalid.

Apply this diff:

-            raise ValueError("Positive hits must be on the positive strand.")
+            invalid_hits = [h for h in positive_hits if h.negative]
+            raise ValueError(f"Found {len(invalid_hits)} positive hits on negative strand: {invalid_hits}")
-            raise ValueError("Negative hits must be on the negative strand.")
+            invalid_hits = [h for h in negative_hits if not h.negative]
+            raise ValueError(f"Found {len(invalid_hits)} negative hits on positive strand: {invalid_hits}")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 9ac73dc and 7774578.

📒 Files selected for processing (1)
  • prymer/offtarget/offtarget_detector.py (4 hunks)
🔇 Additional comments (2)
prymer/offtarget/offtarget_detector.py (2)

130-145: LGTM! Well-structured dataclass.

Clean implementation with appropriate field defaults and documentation.


359-365: Add early return after exceeding primer hit threshold.

Currently, when primer hits exceed the maximum allowed, the function sets passes=False but continues processing. To avoid unnecessary computation, return the result immediately after setting passes=False.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (1)
prymer/offtarget/offtarget_detector.py (1)

479-522: Consider pre-filtering hits before product operation

Current implementation is good, but could be optimized further.

Consider filtering hits by position before using itertools.product to reduce unnecessary combinations:

-        for positive_hit, negative_hit in itertools.product(positive_hits, negative_hits):
+        # Pre-filter hits that could never form valid amplicons
+        filtered_negative_hits = [h for h in negative_hits if h.start > min(p.end for p in positive_hits)]
+        filtered_positive_hits = [h for h in positive_hits if h.end < max(n.start for n in negative_hits)]
+        for positive_hit, negative_hit in itertools.product(filtered_positive_hits, filtered_negative_hits):
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 7774578 and ad4575c.

📒 Files selected for processing (2)
  • prymer/offtarget/offtarget_detector.py (4 hunks)
  • tests/offtarget/test_offtarget.py (2 hunks)
🔇 Additional comments (5)
tests/offtarget/test_offtarget.py (3)

175-234: LGTM: Comprehensive test coverage for off-target result building

Test effectively validates hit organization by reference and strand orientation.


240-268: LGTM: Test cases cover critical scenarios

Test matrix effectively validates primer orientation and amplicon size constraints.


288-326: LGTM: Error cases well covered

Comprehensive validation of error conditions for invalid primer configurations.

prymer/offtarget/offtarget_detector.py (2)

130-145: LGTM: Well-structured data organization

Clean dataclass implementation for hit organization.


356-423: LGTM: Improved efficiency with early exit and structured hit organization

Clean implementation with proper hit organization and early termination.

if (
left_bwa_result.hit_count > self._max_primer_hits
or right_bwa_result.hit_count > self._max_primer_hits
):
result = OffTargetResult(primer_pair=primer_pair, passes=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just return here? Is there any value to caching the result in this case, where it's so little work to compute the result?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caching is required by the unit test at

# Test that using the cache (or not) does not affect the results

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok. Perhaps I'll "fix" this in a separate PR.

[self._hit_to_span(h) for h in p2.hits] if self._keep_primer_spans else []
),
if self._cache_results:
self._primer_pair_cache[primer_pair] = replace(result, cached=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If keeping this (and again on line 408), why not just set cached=self._cache_results at construction time, so there's no need to replace(cached=True?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the logic is that the returned object should have cached=False, because it's the first time it's been generated, and the cached object has cached=True for when it's retrieved from the cache. This jives with the description of cached at

cached: True if this result is part of a cache, False otherwise. This is useful for testing

        cached: True if this result is part of a cache, False otherwise.  This is useful for testing

I suggest we come back to the cache issue on a subsequent PR.

# Get the set of reference names with hits
hits_by_refname: dict[str, PrimerPairBwaHitsBySideAndStrand] = {
hit.refname: PrimerPairBwaHitsBySideAndStrand()
for hit in left_bwa_result.hits + right_bwa_result.hits
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These feels pretty inefficient .. it will concatenate two fairly large lists and (I think?) generate a PrimerPairBwaHitsBySideAndStrand per hit, then unique them, rather than generate one per reference contig.

I think something like this would work:

for refname in set(hit.refname for hit in itertools.chain(left_bwa_result.hits, right_bwa_result.hits)

Comment on lines 376 to 386
for hit in left_bwa_result.hits:
if hit.negative:
hits_by_refname[hit.refname].left_negative.append(hit)
else:
hits_by_refname[hit.refname].left_positive.append(hit)

for hit in right_bwa_result.hits:
if hit.negative:
hits_by_refname[hit.refname].right_negative.append(hit)
else:
hits_by_refname[hit.refname].right_positive.append(hit)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I see this ... I think actually maybe you don't care any more about left vs. right here and beyond this point? Sorry, as I think I may have sent you down this path.

Specifically if a single left primer happens to bind to the genome in two places, close together, in the right relative orientations, that could still cause problems. So I think it's really just one collection of + strand mappings and one collection of - strand mappings.

positive_hits=hits.left_positive,
negative_hits=hits.right_negative,
max_len=self._max_amplicon_size,
strand=Strand.POSITIVE,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to think about strand here. I think it's probably mostly irrelevant and could just always be set to positive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the PrimerPair code, it's not explicitly enforced by a __post_init__ but is strongly assumed by e.g. calculate_amplicon_span that the left primer is left of the right primer. This implies that the left primer is on the positive strand and the right primer is on the negative strand. All amplicons are coded as on the positive strand (default value for Span, again, not explicit).

When a hit to a primer pair is in the same orientation as the primer pair definition, e.g. left positive and right negative, then it makes sense for the amplicon to also be on the positive strand.

If the hit is in the opposite orientation to the primer pair definition, the amplicon should be on the negative strand.

I think it makes sense to assign strandedness like this:

  • left primer + / right primer - = amplicon +
  • left primer + / left primer - = amplicon +
  • right primer + / left primer - = amplicon -
  • right primer + / right primer - = amplicon -

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed elsewhere, will leave questions of strand and L/L R/R hits to a separate PR.

plus, minus = (h2, h1) if h1.negative else (h1, h2)
if minus.start > plus.end and (minus.end - plus.start + 1) <= max_len:
amplicons.append(Span(refname=plus.refname, start=plus.start, end=minus.end))
refnames: set[str] = {h.refname for h in positive_hits + negative_hits}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
refnames: set[str] = {h.refname for h in positive_hits + negative_hits}
refnames: set[str] = {h.refname for h in itertools.chain(positive_hits,
negative_hits)}

amplicons.append(Span(refname=plus.refname, start=plus.start, end=minus.end))
refnames: set[str] = {h.refname for h in positive_hits + negative_hits}
if len(refnames) > 1:
raise ValueError("Hits are present on more than one reference.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Include the refnames here?

Comment on lines 512 to 513
negative_hit.start > positive_hit.end
and (negative_hit.end - positive_hit.start + 1) <= max_len
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we allow for situations where the two primers' mappings overlap on the genome? E.g. maybe the check should be:

negative_hit.start > positive_hit.start and negative_hit.end > positive_hit.end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable. Do you think we'd ever see a situation where a primer is a palindrome and hits the same target both forward and reverse?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change causes a number of tests to fail. Since it's a breaking change, I'd like to address it in a separate PR.

FAILED prymer/offtarget/offtarget_detector.py::prymer.offtarget.offtarget_detector
FAILED tests/offtarget/test_offtarget.py::test_ok[True] - AssertionError: assert 1187 == 856
FAILED tests/offtarget/test_offtarget.py::test_ok[False] - AssertionError: assert 1187 == 856
FAILED tests/offtarget/test_offtarget.py::test_check_too_many_primer_pair_hits[at the maximum-204-856-True-True] - AssertionError: at the maximum
FAILED tests/offtarget/test_offtarget.py::test_check_too_many_primer_pair_hits[at the maximum-204-856-True-False] - AssertionError: at the maximum
FAILED tests/offtarget/test_offtarget.py::test_to_amplicons[No mappings - overlapping primers (1bp overlap)-positive0-negative0-+-expected0-True] - AssertionError: No mappings - overlapping primers (1bp overlap)
FAILED tests/offtarget/test_offtarget.py::test_to_amplicons[No mappings - overlapping primers (1bp overlap)-positive0-negative0-+-expected0-False] - AssertionError: No mappings - overlapping primers (1bp overlap)
FAILED tests/offtarget/test_offtarget.py::test_to_amplicons[No mappings - amplicon size too big (1bp too big)-positive1-negative1-+-expected1-True] - AssertionError: No mappings - amplicon size too big (1bp too big)
FAILED tests/offtarget/test_offtarget.py::test_to_amplicons[No mappings - amplicon size too big (1bp too big)-positive1-negative1-+-expected1-False] - AssertionError: No mappings - amplicon size too big (1bp too big)

raise ValueError("Hits are present on more than one reference.")

amplicons: list[Span] = []
for positive_hit, negative_hit in itertools.product(positive_hits, negative_hits):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is good enough if it meets your performance needs @ameynert. As noted elsewhere, if you had many many hits to one reference, you could speed this up by sorting the positive and negative strand hits, looping over list indices, and avoiding whole swathes of pairs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tested this separately and the speed improvement by sorting the pos/neg strand hits is significant when there are a large number of hits returned, e.g. in a scenario where the max mismatches parameters are relaxed, one of the primer sequences is getting 100+ hits, and there are multiple highly similar reference contigs in the indexed genome that's being searched.

@tfenne tfenne assigned ameynert and unassigned tfenne Dec 13, 2024
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
prymer/offtarget/offtarget_detector.py (1)

356-378: Consider using dict comprehensions for hit organization.

Current implementation uses multiple loops to organize hits.

-        left_positive_hits: defaultdict[ReferenceName, list[BwaHit]] = defaultdict(list)
-        left_negative_hits: defaultdict[ReferenceName, list[BwaHit]] = defaultdict(list)
-        right_positive_hits: defaultdict[ReferenceName, list[BwaHit]] = defaultdict(list)
-        right_negative_hits: defaultdict[ReferenceName, list[BwaHit]] = defaultdict(list)
-
-        # Split the hits for left and right by reference name and strand
-        for hit in left_bwa_result.hits:
-            if hit.negative:
-                left_negative_hits[hit.refname].append(hit)
-            else:
-                left_positive_hits[hit.refname].append(hit)
-
-        for hit in right_bwa_result.hits:
-            if hit.negative:
-                right_negative_hits[hit.refname].append(hit)
-            else:
-                right_positive_hits[hit.refname].append(hit)
+        left_hits = {True: defaultdict(list), False: defaultdict(list)}
+        right_hits = {True: defaultdict(list), False: defaultdict(list)}
+        
+        for hit in left_bwa_result.hits:
+            left_hits[hit.negative][hit.refname].append(hit)
+            
+        for hit in right_bwa_result.hits:
+            right_hits[hit.negative][hit.refname].append(hit)
+            
+        left_positive_hits = left_hits[False]
+        left_negative_hits = left_hits[True]
+        right_positive_hits = right_hits[False]
+        right_negative_hits = right_hits[True]
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad4575c and f6321f4.

📒 Files selected for processing (1)
  • prymer/offtarget/offtarget_detector.py (5 hunks)
🔇 Additional comments (5)
prymer/offtarget/offtarget_detector.py (5)

104-106: LGTM: Type alias improves code clarity.

The ReferenceName type alias enhances code readability and maintainability.


343-355: LGTM: Early return optimization.

The early return after exceeding primer hit threshold is correctly implemented.


379-396: LGTM: Amplicon building logic is correct.

The code correctly handles both positive and negative strand amplicons.


399-414: LGTM: Result construction is well-structured.

The code properly handles spans based on configuration flags.


470-515: Consider optimizing amplicon generation for large hit sets.

As noted in past reviews, sorting hits could improve performance when dealing with many hits on a single reference.

         amplicons: list[Span] = []
+        # Sort hits by position for efficient iteration
+        positive_hits.sort(key=lambda h: h.start)
+        negative_hits.sort(key=lambda h: h.start)
+        
         for positive_hit, negative_hit in itertools.product(positive_hits, negative_hits):
             if (
                 negative_hit.start > positive_hit.end
                 and negative_hit.end - positive_hit.start + 1 <= max_len
             ):
+                # Can break inner loop if negative_hit.start is too far
+                if negative_hit.start - positive_hit.start + 1 > max_len:
+                    break
                 amplicons.append(
                     Span(
                         refname=positive_hit.refname,

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
prymer/offtarget/offtarget_detector.py (1)

514-549: Consider further optimizations.

  1. Current implementation has O(n*m) complexity where n and m are the number of positive and negative hits.
  2. TODO about overlapping hits should be addressed.

Consider:

  1. Using binary search to find valid negative hits
  2. Implementing parallel processing for large hit sets
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6321f4 and f6b1708.

📒 Files selected for processing (2)
  • prymer/offtarget/offtarget_detector.py (5 hunks)
  • tests/offtarget/test_offtarget.py (2 hunks)
🔇 Additional comments (6)
prymer/offtarget/offtarget_detector.py (5)

Line range hint 78-106: LGTM: Clean import additions and type alias.

The new imports and type alias enhance code organization and type safety.


343-413: LGTM: Improved hit organization and early return optimization.

Good improvements:

  1. Early return when hit count exceeds maximum
  2. Structured organization of hits by reference and strand

470-485: LGTM: Clear method signature update.

Parameters are well-named and documented.


486-501: LGTM: Thorough input validation.

Good error handling for strand orientation and reference consistency.


502-506: LGTM: Smart early exit optimization.

Prevents unnecessary sorting when no amplicons can be generated.

tests/offtarget/test_offtarget.py (1)

175-326: LGTM: Comprehensive test coverage.

Tests thoroughly validate:

  1. Off-target result building
  2. Strand handling
  3. Error conditions

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
prymer/offtarget/offtarget_detector.py (1)

525-525: Consider implementing overlapping hits support.

The TODO suggests allowing overlapping positive and negative hits. This could improve sensitivity for certain primer designs.

Would you like me to propose an implementation that handles overlapping hits?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6b1708 and 0036b83.

📒 Files selected for processing (1)
  • prymer/offtarget/offtarget_detector.py (5 hunks)
🔇 Additional comments (5)
prymer/offtarget/offtarget_detector.py (5)

104-106: LGTM: Type alias improves code clarity.

The ReferenceName type alias enhances code readability by providing semantic meaning to reference name strings.


343-354: LGTM: Early return optimization implemented.

Early return when hit count exceeds maximum prevents unnecessary computation.


357-377: LGTM: Efficient hit categorization.

Using defaultdict for hit categorization by reference and strand improves code organization and reduces boilerplate.


379-413: LGTM: Comprehensive amplicon building.

Correctly handles both strand orientations while maintaining proper strand information in the output.


470-514: LGTM: Efficient implementation with proper validation.

The method correctly validates hit orientations and uses sorting for efficient processing.

@ameynert ameynert requested a review from tfenne January 7, 2025 16:49
@ameynert ameynert assigned tfenne and unassigned ameynert Jan 7, 2025
if (
left_bwa_result.hit_count > self._max_primer_hits
or right_bwa_result.hit_count > self._max_primer_hits
):
result = OffTargetResult(primer_pair=primer_pair, passes=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok. Perhaps I'll "fix" this in a separate PR.

@ameynert ameynert merged commit baf2e65 into main Jan 10, 2025
7 checks passed
@ameynert ameynert deleted the am_primer_pair_hit_building branch January 10, 2025 22:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Speed up from O(n^2) iteration over all left/right primer hits in OffTargetDetector._to_amplicons
2 participants