Skip to content

Commit

Permalink
feat: add penalty weights for pick_hyb_probe task
Browse files Browse the repository at this point in the history
  • Loading branch information
emmcauley committed Sep 23, 2024
1 parent 70f6840 commit 8275f3d
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 28 deletions.
6 changes: 3 additions & 3 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
22 changes: 14 additions & 8 deletions prymer/primer3/primer3_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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`
Expand All @@ -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
58 changes: 46 additions & 12 deletions prymer/primer3/primer3_weights.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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, ...)
```
"""
Expand All @@ -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
Expand Down Expand Up @@ -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
26 changes: 21 additions & 5 deletions tests/primer3/test_primer3_weights.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

0 comments on commit 8275f3d

Please sign in to comment.