From d32a7acdb41cc8074c5c9e98bee10da441526b58 Mon Sep 17 00:00:00 2001 From: Erin McAuley Date: Wed, 18 Sep 2024 17:27:52 -0400 Subject: [PATCH] feat: add penalty weights for pick_hyb_probe task --- docs/overview.md | 6 +-- prymer/primer3/primer3_input.py | 22 ++++++---- prymer/primer3/primer3_weights.py | 58 +++++++++++++++++++++------ tests/primer3/test_primer3_weights.py | 28 ++++++++++--- 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index 1e92398..acf62da 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -22,10 +22,10 @@ Designing primers (left or right) or primer pairs using Primer3 is primarily per for a single target. The `Primer3` instance is intended to be re-used to design primers across multiple targets, or re-design (after changing parameters) for the same target, or both! -Common input parameters are specified in [`Primer3Parameters()`][prymer.primer3.primer3_parameters.Primer3Parameters] and -[`Primer3Weights()`][prymer.primer3.primer3_weights.Primer3Weights], while the task type (left primer, +Common input parameters for designing primers are specified in [`Primer3Parameters()`][prymer.primer3.primer3_parameters.Primer3Parameters] and +[`PrimerAndAmpliconWeights()`][prymer.primer3.primer3_weights.PrimerAndAmpliconWeights], while the task type (left primer, right primer, or primer pair design) is specified with the corresponding -[`Primer3Task`][prymer.primer3.primer3_task.Primer3Task]. +[`Primer3Task`][prymer.primer3.primer3_task.Primer3Task]. Penalty weights for designing internal probes are specified in [`ProbeWeights()`][prymer.primer3.primer3_weights.ProbeWeights] The result of a primer design is encapsulated in the [`Primer3Result`][prymer.primer3.primer3.Primer3Result] class. It provides the primers (or primer pairs) that were designed, as well as a list of reasons some primers were not returned, diff --git a/prymer/primer3/primer3_input.py b/prymer/primer3/primer3_input.py index 952ef69..f2aea96 100644 --- a/prymer/primer3/primer3_input.py +++ b/prymer/primer3/primer3_input.py @@ -8,12 +8,14 @@ The module uses: 1. [`Primer3Parameters`][prymer.primer3.primer3_parameters.Primer3Parameters] -to specify user-specified criteria for primer design -2. [`Primer3Weights`][prymer.primer3.primer3_weights.Primer3Weights] to establish penalties -based on those criteria -3. [`Primer3Task`][prymer.primer3.primer3_task.Primer3Task] to organize task-specific + to specify user-specified criteria for primer design +2. [`PrimerAndAmpliconWeights`][prymer.primer3.primer3_weights.PrimerAndAmpliconWeights] + to establish penalties based on those criteria +3. [`ProbeWeights`][prymer.primer3.primer3_weights.ProbeWeights] to specify penalties based on probe + design criteria +4. [`Primer3Task`][prymer.primer3.primer3_task.Primer3Task] to organize task-specific logic. -4. [`Span`](index.md#prymer.api.span.Span] to specify the target region. +5. [`Span`](index.md#prymer.api.span.Span] to specify the target region. The `Primer3Input.to_input_tags(]` method The main purpose of this class is to generate the @@ -81,12 +83,14 @@ from dataclasses import dataclass from typing import Any +from typing import Optional from prymer.api.span import Span from prymer.primer3.primer3_input_tag import Primer3InputTag from prymer.primer3.primer3_parameters import Primer3Parameters from prymer.primer3.primer3_task import Primer3TaskType -from prymer.primer3.primer3_weights import Primer3Weights +from prymer.primer3.primer3_weights import PrimerAndAmpliconWeights +from prymer.primer3.primer3_weights import ProbeWeights @dataclass(frozen=True, init=True, slots=True) @@ -96,7 +100,8 @@ class Primer3Input: target: Span task: Primer3TaskType params: Primer3Parameters - weights: Primer3Weights = Primer3Weights() + primer_weights: Optional[PrimerAndAmpliconWeights] = PrimerAndAmpliconWeights() + probe_weights: Optional[ProbeWeights] = None def to_input_tags(self, design_region: Span) -> dict[Primer3InputTag, Any]: """Assembles `Primer3InputTag` and values for input to `Primer3` @@ -116,6 +121,7 @@ def to_input_tags(self, design_region: Span) -> dict[Primer3InputTag, Any]: assembled_tags = { **primer3_task_params, **self.params.to_input_tags(), - **self.weights.to_input_tags(), + **self.primer_weights.to_input_tags(), + **(self.probe_weights.to_input_tags() if self.probe_weights is not None else {}), } return assembled_tags diff --git a/prymer/primer3/primer3_weights.py b/prymer/primer3/primer3_weights.py index 6de7887..2db2fa7 100644 --- a/prymer/primer3/primer3_weights.py +++ b/prymer/primer3/primer3_weights.py @@ -1,7 +1,10 @@ """ # Primer3Weights Class and Methods -The Primer3Weights class holds the penalty weights that Primer3 uses to score primer designs. +The PrimerAndAmpliconWeights class holds the penalty weights that Primer3 uses to score +primer designs. + +The ProbeWeights class holds the penalty weights that Primer3 uses to score internal probe designs. Primer3 considers the differential between user input (e.g., constraining the optimal primer size to be 18 bp) and the characteristics of a specific primer design (e.g., if the primer @@ -11,14 +14,14 @@ By modifying these weights, users can prioritize specific primer design characteristics. Each of the defaults provided here are derived from the Primer3 manual: https://primer3.org/manual.html -## Examples of interacting with the `Primer3Weights` class +## Examples of interacting with the `PrimerAndAmpliconWeights` class ```python ->>> Primer3Weights(product_size_lt=1, product_size_gt=1) -Primer3Weights(product_size_lt=1, product_size_gt=1, ...) ->>> Primer3Weights(product_size_lt=5, product_size_gt=1) -Primer3Weights(product_size_lt=5, product_size_gt=1, ...) +>>> PrimerAndAmpliconWeights(product_size_lt=1, product_size_gt=1) +PrimerAndAmpliconWeights(product_size_lt=1, product_size_gt=1, ...) +>>> PrimerAndAmpliconWeights(product_size_lt=5, product_size_gt=1) +PrimerAndAmpliconWeights(product_size_lt=5, product_size_gt=1, ...) ``` """ @@ -30,23 +33,23 @@ @dataclass(frozen=True, init=True, slots=True) -class Primer3Weights: +class PrimerAndAmpliconWeights: """Holds the weights that Primer3 uses to adjust penalties that originate from the designed primer(s). The weights that Primer3 uses when a parameter is less than optimal are labeled with "_lt". "_gt" weights are penalties applied when a parameter is greater than optimal. + Some of these settings depart from the default settings enumerated in the Primer3 manual. Please see the Primer3 manual for additional details: https://primer3.org/manual.html#globalTags Example: - >>> Primer3Weights() #default implementation - Primer3Weights(product_size_lt=1, product_size_gt=1, product_tm_lt=0.0, product_tm_gt=0.0, primer_end_stability=0.25, primer_gc_lt=0.25, primer_gc_gt=0.25, primer_self_any=0.1, primer_self_end=0.1, primer_size_lt=0.5, primer_size_gt=0.1, primer_tm_lt=1.0, primer_tm_gt=1.0) + >>> PrimerAndAmpliconWeights() #default implementation + PrimerAndAmpliconWeights(product_size_lt=1, product_size_gt=1, product_tm_lt=0.0, product_tm_gt=0.0, primer_end_stability=0.25, primer_gc_lt=0.25, primer_gc_gt=0.25, primer_self_any=0.1, primer_self_end=0.1, primer_size_lt=0.5, primer_size_gt=0.1, primer_tm_lt=1.0, primer_tm_gt=1.0) - >>> Primer3Weights(product_size_lt=5) - Primer3Weights(product_size_lt=5, product_size_gt=1, product_tm_lt=0.0, product_tm_gt=0.0, primer_end_stability=0.25, primer_gc_lt=0.25, primer_gc_gt=0.25, primer_self_any=0.1, primer_self_end=0.1, primer_size_lt=0.5, primer_size_gt=0.1, primer_tm_lt=1.0, primer_tm_gt=1.0) - """ # noqa: E501 + >>> PrimerAndAmpliconWeights(product_size_lt=5) + PrimerAndAmpliconWeights(product_size_lt=5, product_size_gt=1, product_tm_lt=0.0, product_tm_gt=0.0, primer_end_stability=0.25, primer_gc_lt=0.25, primer_gc_gt=0.25, primer_self_any=0.1, primer_self_end=0.1, primer_size_lt=0.5, primer_size_gt=0.1, primer_tm_lt=1.0, primer_tm_gt=1.0)""" # noqa: E501 product_size_lt: int = 1 product_size_gt: int = 1 @@ -80,3 +83,34 @@ def to_input_tags(self) -> dict[Primer3InputTag, Any]: Primer3InputTag.PRIMER_WT_TM_GT: self.primer_tm_gt, } return mapped_dict + + +@dataclass(frozen=True, init=True, slots=True) +class ProbeWeights: + """Holds the weights that Primer3 uses to adjust penalties + that originate from the designed internal probe(s).""" + + probe_size_lt: float = 0.25 + probe_size_gt: float = 0.25 + probe_tm_lt: float = 1.0 + probe_tm_gt: float = 1.0 + probe_gc_lt: float = 0.5 + probe_gc_gt: float = 0.5 + probe_self_any: float = 1.0 + probe_self_end: float = 1.0 + probe_hairpin_th: float = 1.0 + + def to_input_tags(self) -> dict[Primer3InputTag, Any]: + """Maps weights to Primer3InputTag to feed directly into Primer3.""" + mapped_dict = { + Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_LT: self.probe_size_lt, + Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_GT: self.probe_size_gt, + Primer3InputTag.PRIMER_INTERNAL_WT_TM_LT: self.probe_tm_lt, + Primer3InputTag.PRIMER_INTERNAL_WT_TM_GT: self.probe_tm_gt, + Primer3InputTag.PRIMER_INTERNAL_WT_GC_PERCENT_LT: self.probe_gc_lt, + Primer3InputTag.PRIMER_INTERNAL_WT_GC_PERCENT_GT: self.probe_gc_gt, + Primer3InputTag.PRIMER_INTERNAL_WT_SELF_ANY: self.probe_self_any, + Primer3InputTag.PRIMER_INTERNAL_WT_SELF_END: self.probe_self_end, + Primer3InputTag.PRIMER_INTERNAL_WT_HAIRPIN_TH: self.probe_hairpin_th, + } + return mapped_dict diff --git a/tests/primer3/test_primer3_weights.py b/tests/primer3/test_primer3_weights.py index c9dd175..3351b74 100644 --- a/tests/primer3/test_primer3_weights.py +++ b/tests/primer3/test_primer3_weights.py @@ -1,10 +1,11 @@ -from prymer.primer3 import Primer3InputTag -from prymer.primer3 import Primer3Weights +from prymer.primer3.primer3_input_tag import Primer3InputTag +from prymer.primer3.primer3_weights import PrimerAndAmpliconWeights +from prymer.primer3.primer3_weights import ProbeWeights def test_primer_weights_valid() -> None: - """Test instantiation of Primer3Weights object with valid input""" - test_weights = Primer3Weights() + """Test instantiation of `PrimerAndAmpliconWeights` object with valid input""" + test_weights = PrimerAndAmpliconWeights() test_dict = test_weights.to_input_tags() assert test_dict[Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_LT] == 1 assert test_dict[Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_GT] == 1 @@ -22,9 +23,24 @@ def test_primer_weights_valid() -> None: assert len((test_dict.values())) == 13 +def test_probe_weights_valid() -> None: + test_weights = ProbeWeights() + test_dict = test_weights.to_input_tags() + assert test_dict[Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_LT] == 0.25 + assert test_dict[Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_GT] == 0.25 + assert test_dict[Primer3InputTag.PRIMER_INTERNAL_WT_TM_LT] == 1.0 + assert test_dict[Primer3InputTag.PRIMER_INTERNAL_WT_TM_GT] == 1.0 + assert test_dict[Primer3InputTag.PRIMER_INTERNAL_WT_GC_PERCENT_LT] == 0.5 + assert test_dict[Primer3InputTag.PRIMER_INTERNAL_WT_GC_PERCENT_GT] == 0.5 + assert test_dict[Primer3InputTag.PRIMER_INTERNAL_WT_SELF_ANY] == 1.0 + assert test_dict[Primer3InputTag.PRIMER_INTERNAL_WT_SELF_END] == 1.0 + assert test_dict[Primer3InputTag.PRIMER_INTERNAL_WT_HAIRPIN_TH] == 1.0 + assert len((test_dict.values())) == 9 + + def test_primer_weights_to_input_tags() -> None: """Test results from to_input_tags() with and without default values""" - default_map = Primer3Weights().to_input_tags() + default_map = PrimerAndAmpliconWeights().to_input_tags() assert default_map[Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_LT] == 1 - customized_map = Primer3Weights(product_size_lt=5).to_input_tags() + customized_map = PrimerAndAmpliconWeights(product_size_lt=5).to_input_tags() assert customized_map[Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_LT] == 5