Skip to content

Commit

Permalink
feat: add ProbeParams to Primer3Parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
emmcauley committed Sep 22, 2024
1 parent 3f64ade commit 07a1c33
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 72 deletions.
14 changes: 7 additions & 7 deletions prymer/primer3/primer3.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@
parameters and target region.
```python
>>> from prymer.primer3.primer3_parameters import Primer3Parameters
>>> from prymer.primer3.primer3_parameters import PrimerAndAmpliconParameters
>>> from prymer.api import MinOptMax
>>> target = Span(refname="chr1", start=201, end=250, strand=Strand.POSITIVE)
>>> params = Primer3Parameters( \
>>> params = PrimerAndAmpliconParameters( \
amplicon_sizes=MinOptMax(min=100, max=250, opt=200), \
amplicon_tms=MinOptMax(min=55.0, max=100.0, opt=70.0), \
primer_sizes=MinOptMax(min=29, max=31, opt=30), \
Expand All @@ -60,7 +60,7 @@
)
>>> design_input = Primer3Input( \
target=target, \
params=params, \
primer_and_amplicon_params=params, \
task=DesignLeftPrimersTask(), \
)
>>> left_result = designer.design_primers(design_input=design_input)
Expand Down Expand Up @@ -312,7 +312,7 @@ def get_design_sequences(self, region: Span) -> tuple[str, str]:
def _is_valid_primer(design_input: Primer3Input, primer_design: Primer) -> bool:
return (
primer_design.longest_dinucleotide_run_length()
<= design_input.params.primer_max_dinuc_bases
<= design_input.primer_and_amplicon_params.primer_max_dinuc_bases
)

@staticmethod
Expand All @@ -335,13 +335,13 @@ def _screen_pair_results(
valid: bool = True
if (
primer_pair.left_primer.longest_dinucleotide_run_length()
> design_input.params.primer_max_dinuc_bases
> design_input.primer_and_amplicon_params.primer_max_dinuc_bases
): # if the left primer has too many dinucleotide bases, fail it
dinuc_pair_failures.append(primer_pair.left_primer)
valid = False
if (
primer_pair.right_primer.longest_dinucleotide_run_length()
> design_input.params.primer_max_dinuc_bases
> design_input.primer_and_amplicon_params.primer_max_dinuc_bases
): # if the right primer has too many dinucleotide bases, fail it
dinuc_pair_failures.append(primer_pair.right_primer)
valid = False
Expand Down Expand Up @@ -374,7 +374,7 @@ def design_primers(self, design_input: Primer3Input) -> Primer3Result: # noqa:

region: Span = self._pad_target_region(
target=design_input.target,
max_amplicon_length=design_input.params.max_amplicon_length,
max_amplicon_length=design_input.primer_and_amplicon_params.max_amplicon_length,
)

soft_masked, hard_masked = self.get_design_sequences(region)
Expand Down
129 changes: 117 additions & 12 deletions prymer/primer3/primer3_parameters.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
"""
# Primer3Parameters Class and Methods
# PrimerAndAmpliconParameters Class and Methods
The [`Primer3Parameters`][prymer.primer3.primer3_parameters.Primer3Parameters] class stores
user input and maps it to the correct Primer3 fields.
The [`PrimerAndAmpliconParameters`][prymer.primer3.primer3_parameters.PrimerAndAmpliconParameters]
class stores user input and maps it to the correct Primer3 fields.
Primer3 considers many criteria for primer design, including characteristics of candidate primers
and the resultant amplicon product, as well as potential complications (off-target priming,
primer dimer formation). Users can specify many of these constraints in Primer3,
some of which are used to quantify a "score" for each primer design.
The Primer3Parameters class stores commonly used constraints for primer design: GC content, melting
temperature, and size of both primers and expected amplicon. Additional criteria include the maximum
homopolymer length, ambiguous bases, and bases in a dinucleotide run within a primer. By default,
primer design avoids masked bases, returns 5 primers, and sets the GC clamp to be no larger than 5.
The PrimerAndAmpliconParameters class stores commonly used constraints for primer design:
GC content, melting temperature, and size of both primers and expected amplicon.
Additional criteria include the maximum homopolymer length, ambiguous bases, and bases in a
dinucleotide run within a primer. By default, primer design avoids masked bases, returns 5 primers,
and sets the GC clamp to be no larger than 5.
The `to_input_tags()` method in `Primer3Parameters` converts these parameters into tag-values pairs
for use when executing `Primer3`.
The `to_input_tags()` method in `PrimerAndAmpliconParameters` converts these parameters into
tag-values pairs for use when executing `Primer3`.
## Examples
```python
>>> params = Primer3Parameters( \
>>> params = PrimerAndAmpliconParameters( \
amplicon_sizes=MinOptMax(min=100, max=250, opt=200), \
amplicon_tms=MinOptMax(min=55.0, max=100.0, opt=70.0), \
primer_sizes=MinOptMax(min=29, max=31, opt=30), \
Expand Down Expand Up @@ -53,16 +54,18 @@
```
"""

import warnings
from dataclasses import dataclass
from typing import Any
from typing import Optional

from prymer.api.minoptmax import MinOptMax
from prymer.primer3.primer3_input_tag import Primer3InputTag


@dataclass(frozen=True, init=True, slots=True)
class Primer3Parameters:
"""Holds common primer design options that Primer3 uses to inform primer design.
class PrimerAndAmpliconParameters:
"""Holds common primer and amplicon design options that Primer3 uses to inform primer design.
Attributes:
amplicon_sizes: the min, optimal, and max amplicon size
Expand Down Expand Up @@ -140,3 +143,105 @@ def max_amplicon_length(self) -> int:
def max_primer_length(self) -> int:
"""Max primer length"""
return int(self.primer_sizes.max)


@dataclass(frozen=True, init=True, slots=True)
class Primer3Parameters(PrimerAndAmpliconParameters):
"""A deprecated alias for `PrimerAndAmpliconParameters` intended to maintain backwards
compatibility with earlier releases of `prymer`."""

warnings.warn(
"The Primer3Parameters class was deprecated, use PrimerAndAmpliconParameters instead",
DeprecationWarning,
)


@dataclass(frozen=True, init=True, slots=True)
class ProbeParameters:
"""Holds common primer design options that Primer3 uses to inform internal probe design.
Attributes:
probe_sizes: the min, optimal, and max probe size
probe_tms: the min, optimal, and max probe melting temperatures
probe_gcs: the min and maximal GC content for individual probes
probe_excluded_regions: excluded regions (start, length) that probes must not overlap
probe_max_polyX: the max homopolymer length acceptable within a probe
probe_max_Ns: the max number of ambiguous bases acceptable within a probe
probe_max_self_any: max allowable local alignment score when evaluating an individual probe
for self-complementarity throughout the probe sequence
probe_max_self_any_thermo: max allowable score for self-complementarity of the probe
sequence using a thermodynamic approach
probe_max_self_end: max allowable 3'-anchored global alignment score when testing a single
probe for self-complementarity
probe_max_self_end_thermo: similar to `probe_max_end_any` but uses a thermodynamic approach
to evaluate a probe for self-complementarity
probe_max_hairpin_thermo: most stable monomer structure as calculated by a thermodynamic
approach
Defaults in this class are set as recommended by the Primer3 manual.
Please see the Primer3 manual for additional details: https://primer3.org/manual.html#globalTags
Note that the Primer3 documentation advises that, while `probe_max_end_any` is meaningless
when applied to internal oligos used for hybridization-based detection,
`PRIMER_INTERNAL_MAX_SELF_END` should be set at least as high as `PRIMER_INTERNAL_MAX_SELF_ANY`.
Therefore, both parameters are exposed here.
"""

probe_sizes: MinOptMax[int]
probe_tms: MinOptMax[float]
probe_gcs: MinOptMax[float]
probe_excluded_regions: Optional[list[tuple[int, int]]] = None
probe_max_polyX: int = 5
probe_max_Ns: int = 0
probe_max_self_any: float = 12.0
probe_max_self_any_thermo: float = 47.0
probe_max_self_end: float = 12.0
probe_max_self_end_thermo: float = 47.0
probe_max_hairpin_thermo: float = 47.0

def __post_init__(self) -> None:
if not isinstance(self.probe_sizes.min, int):
raise TypeError("Probe sizes must be integers")
if not isinstance(self.probe_tms.min, float) or not isinstance(self.probe_gcs.min, float):
raise TypeError("Probe melting temperatures and GC content must be floats")
if self.probe_excluded_regions is not None:
# if probe_excluded regions are provided, ensure they are each a tuple[int, int]
if not all(
isinstance(region, tuple) for region in self.probe_excluded_regions
) or not all(
isinstance(constraint, int)
for region in self.probe_excluded_regions
for constraint in region
):
raise TypeError(
"Excluded regions for probe design must be list[tuple[int, int]]"
" to enumerate the start and length of each excluded region"
)

def to_input_tags(self) -> dict[Primer3InputTag, Any]:
"""Converts input params to Primer3InputTag to feed directly into Primer3."""
mapped_dict = {
Primer3InputTag.PRIMER_INTERNAL_MIN_SIZE: self.probe_sizes.min,
Primer3InputTag.PRIMER_INTERNAL_OPT_SIZE: self.probe_sizes.opt,
Primer3InputTag.PRIMER_INTERNAL_MAX_SIZE: self.probe_sizes.max,
Primer3InputTag.PRIMER_INTERNAL_MIN_TM: self.probe_tms.min,
Primer3InputTag.PRIMER_INTERNAL_OPT_TM: self.probe_tms.opt,
Primer3InputTag.PRIMER_INTERNAL_MAX_TM: self.probe_tms.max,
Primer3InputTag.PRIMER_INTERNAL_MIN_GC: self.probe_gcs.min,
Primer3InputTag.PRIMER_INTERNAL_OPT_GC_PERCENT: self.probe_gcs.opt,
Primer3InputTag.PRIMER_INTERNAL_MAX_GC: self.probe_gcs.max,
Primer3InputTag.PRIMER_INTERNAL_MAX_POLY_X: self.probe_max_polyX,
Primer3InputTag.PRIMER_INTERNAL_MAX_NS_ACCEPTED: self.probe_max_Ns,
Primer3InputTag.PRIMER_INTERNAL_MAX_SELF_ANY: self.probe_max_self_any,
Primer3InputTag.PRIMER_INTERNAL_MAX_SELF_ANY_TH: self.probe_max_self_any_thermo,
Primer3InputTag.PRIMER_INTERNAL_MAX_SELF_END: self.probe_max_self_end,
Primer3InputTag.PRIMER_INTERNAL_MAX_SELF_END_TH: self.probe_max_self_end_thermo,
Primer3InputTag.PRIMER_INTERNAL_MAX_HAIRPIN_TH: self.probe_max_hairpin_thermo,
}

if self.probe_excluded_regions is not None:
mapped_dict[Primer3InputTag.SEQUENCE_INTERNAL_EXCLUDED_REGION] = (
self.probe_excluded_regions
)
return mapped_dict
Loading

0 comments on commit 07a1c33

Please sign in to comment.