From a47953e82bb49f41a5ebdd3339abffbebba1d92e 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 --- prymer/primer3/primer3_input.py | 22 ++++++++----- prymer/primer3/primer3_weights.py | 46 +++++++++++++++++++++++---- tests/primer3/test_primer3_weights.py | 26 ++++++++++++--- 3 files changed, 74 insertions(+), 20 deletions(-) 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..67fa675 100644 --- a/prymer/primer3/primer3_weights.py +++ b/prymer/primer3/primer3_weights.py @@ -15,9 +15,9 @@ ```python ->>> Primer3Weights(product_size_lt=1, product_size_gt=1) +>>> PrimerAndAmpliconWeights(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) +>>> PrimerAndAmpliconWeights(product_size_lt=5, product_size_gt=1) Primer3Weights(product_size_lt=5, product_size_gt=1, ...) ``` @@ -30,22 +30,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, probe_size_lt=0.25, probe_size_gt=0.25, probe_tm_lt=1.0, probe_tm_gt=1.0, probe_gc_lt=0.5, probe_gc_gt=0.5, probe_self_any=1.0, probe_self_end=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) + >>> 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, probe_size_lt=0.25, probe_size_gt=0.25, probe_tm_lt=1.0, probe_tm_gt=1.0, probe_gc_lt=0.5, probe_gc_gt=0.5, probe_self_any=1.0, probe_self_end=1.0) """ # noqa: E501 product_size_lt: int = 1 @@ -80,3 +81,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 1919a0f..3351b74 100644 --- a/tests/primer3/test_primer3_weights.py +++ b/tests/primer3/test_primer3_weights.py @@ -1,10 +1,11 @@ from prymer.primer3.primer3_input_tag import Primer3InputTag -from prymer.primer3.primer3_weights import Primer3Weights +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