From 932c823e00a58417490e8e9894414acc5fe70823 Mon Sep 17 00:00:00 2001 From: Erin McAuley Date: Wed, 2 Oct 2024 17:31:33 -0400 Subject: [PATCH] refactor: revert thermo-related attributes, params, weights; revert doc-tests and unit-tests --- docs/overview.md | 4 +- prymer/api/oligo.py | 17 +--- prymer/api/oligo_like.py | 5 -- prymer/api/primer.py | 70 ++------------- prymer/api/primer_pair.py | 2 +- prymer/primer3/primer3.py | 17 ++-- prymer/primer3/primer3_input.py | 8 -- prymer/primer3/primer3_parameters.py | 106 +---------------------- prymer/primer3/primer3_weights.py | 40 +-------- tests/primer3/test_primer3.py | 16 ++-- tests/primer3/test_primer3_input.py | 9 +- tests/primer3/test_primer3_parameters.py | 5 -- tests/primer3/test_primer3_weights.py | 10 +-- 13 files changed, 38 insertions(+), 271 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index 10ac905..42759d2 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -3,7 +3,7 @@ The `prymer` Python library is intended to be used for three main purposes: 1. [Clustering targets](#clustering-targets) into larger amplicons prior to designing primers. -2. [Designing oligos](#designing-primers) using Primer3 for each target from (1). +2. [Designing](#designing-primers) primers (single and paired) and internal hybridization probes using Primer3 for each target from (1). 3. [Build and Picking a set of primer pairs](#build-and-picking-primer-pairs) from the design candidates produced in (2). ## Clustering Targets @@ -18,7 +18,7 @@ amplicons prior to primer design. Designing primers (left or right) or primer pairs using Primer3 is primarily performed using the [`Primer3`][prymer.primer3.primer3.Primer3] class, which wraps the [`primer3` command line tool](https://github.com/primer3-org/primer3). The -[`design_oligos()`][prymer.primer3.primer3.Primer3.design_oligos] facilitates the design of primers (single and paired) and internal hybridization probes +[`design()`][prymer.primer3.primer3.Primer3.design] method facilitates the design of primers (single and paired) and internal hybridization probes 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! diff --git a/prymer/api/oligo.py b/prymer/api/oligo.py index 75991db..4a003de 100644 --- a/prymer/api/oligo.py +++ b/prymer/api/oligo.py @@ -1,7 +1,7 @@ """ # Oligo Class and Methods -This module contains a class and class methods to represent an oligo designed by Primer3. +This module contains a class and class methods to represent an oligo (e.g., designed by Primer3). Oligos can represent single primer and/or internal probe designs. @@ -88,20 +88,10 @@ class Oligo(OligoLike, Metric["Oligo"]): The penalty for a primer is set by the combination of `PrimerAndAmpliconParameters` and `PrimerWeights`, whereas a probe penalty is set by `ProbeParameters` and `ProbeWeights`. - The values for `self_any`, `self_any_th`, `self_end`, `self_end_th`, and `hairpin_th` - are emitted by Primer3 as part of oligo design. These attributes are optional to maintain - flexibility for reading in and writing `Oligo` objects, espeically when design settings are - inconsistent. - Attributes: tm: the calculated melting temperature of the oligo penalty: the penalty or score for the oligo span: the mapping of the primer to the genome - self_any: probe self-complementarity, expressed as local alignment score - self_any_th: probe self-complementarity, expressed as melting temperature - self_end: 3' end complementarity, expressed as local alignment score - self_end_th: 3' end complementarity, expressed as melting temperature - hairpin_th: hairpin formation thermodynamics of the oligo as calculated by Primer3 bases: the base sequence of the oligo (excluding any tail) tail: an optional tail sequence to put on the 5' end of the primer name: an optional name to use for the primer @@ -111,11 +101,6 @@ class Oligo(OligoLike, Metric["Oligo"]): tm: float penalty: float span: Span - self_any: Optional[float] = None - self_any_th: Optional[float] = None - self_end: Optional[float] = None - self_end_th: Optional[float] = None - hairpin_th: Optional[float] = None bases: Optional[str] = None tail: Optional[str] = None diff --git a/prymer/api/oligo_like.py b/prymer/api/oligo_like.py index 604b1eb..2797b3f 100644 --- a/prymer/api/oligo_like.py +++ b/prymer/api/oligo_like.py @@ -25,7 +25,6 @@ from abc import abstractmethod from dataclasses import dataclass from typing import Optional -from typing import TypeVar from typing import assert_never from fgpyo.sequence import gc_content @@ -124,7 +123,3 @@ def _strand_to_location_string(self) -> str: case _: # pragma: no cover # Not calculating coverage on this line as it should be impossible to reach assert_never(f"Encountered unhandled Strand value: {self.span.strand}") - - -OligoLikeType = TypeVar("OligoLikeType", bound=OligoLike) -"""Type variable for classes generic over `OligoLike` types.""" diff --git a/prymer/api/primer.py b/prymer/api/primer.py index 029c112..aa1ac31 100644 --- a/prymer/api/primer.py +++ b/prymer/api/primer.py @@ -1,64 +1,4 @@ -""" -# Primer Class and Methods - -This module contains a class and class methods to represent a primer (e.g. designed by Primer3) - -Class attributes include the primer sequence, melting temperature, and the score of the primer. The -mapping of the primer to the genome is also stored. - -Optional attributes include naming information and a tail sequence to attach to the 5' end of the -primer (if applicable). - -## Examples of interacting with the `Primer` class - -```python ->>> from prymer.api.span import Span, Strand ->>> primer_span = Span(refname="chr1", start=1, end=20) ->>> primer = Oligo(tm=70.0, penalty=-123.0, span=primer_span) ->>> primer.longest_hp_length() -0 ->>> primer.length -20 ->>> primer.name is None -True ->>> primer = Oligo(tm=70.0, penalty=-123.0, span=primer_span) ->>> primer.longest_hp_length() -0 ->>> primer.untailed_length() -20 ->>> primer.tailed_length() -20 ->>> primer = primer.with_tail(tail="GATTACA") ->>> primer.untailed_length() -20 ->>> primer.tailed_length() -27 ->>> primer = primer.with_name(name="foobar") ->>> primer.name -'foobar' - -``` - -Primers may also be written to a file and subsequently read back in, as the `Primer` class is an -`fgpyo` `Metric` class: - -```python ->>> from pathlib import Path ->>> left_span = Span(refname="chr1", start=1, end=20) ->>> left = Oligo(tm=70.0, penalty=-123.0, span=left_span) ->>> right_span = Span(refname="chr1", start=101, end=120) ->>> right = Oligo(tm=70.0, penalty=-123.0, span=right_span) ->>> path = Path("/tmp/path/to/primers.txt") ->>> Oligo.write(path, left, right) # doctest: +SKIP ->>> primers = Oligo.read(path) # doctest: +SKIP ->>> list(primers) # doctest: +SKIP -[ - Primer(tm=70.0, penalty=-123.0, span=amplicon_span, bases="G"*20), - Primer(tm=70.0, penalty=-123.0, span=amplicon_span, bases="T"*20) -] - -``` -""" +"""This module is deprecated - see prymer/api/oligo.py""" import warnings from dataclasses import dataclass @@ -68,8 +8,12 @@ @dataclass(frozen=True, init=True, slots=True) class Primer(Oligo): - """A deprecated alias for `Oligo` intended to maintain backwards - compatibility with earlier releases of `prymer`.""" + """ + A deprecated alias for `Oligo`. + + This class exists to maintain backwards compatibility with earlier releases of `prymer` + and may be removed in a future version. + """ warnings.warn( "The Primer class was deprecated, use Oligo instead", diff --git a/prymer/api/primer_pair.py b/prymer/api/primer_pair.py index ea1a407..ccbff9e 100644 --- a/prymer/api/primer_pair.py +++ b/prymer/api/primer_pair.py @@ -37,7 +37,7 @@ class methods to represent a primer pair. The primer pair is comprised of a lef Span(refname='chr1', start=21, end=100, strand=) >>> list(primer_pair) -[Oligo(name=None, tm=70.0, penalty=-123.0, span=Span(refname='chr1', start=1, end=20, strand=), self_any=None, self_any_th=None, self_end=None, self_end_th=None, hairpin_th=None, bases='GGGGGGGGGGGGGGGGGGGG', tail=None), Oligo(name=None, tm=70.0, penalty=-123.0, span=Span(refname='chr1', start=101, end=120, strand=), self_any=None, self_any_th=None, self_end=None, self_end_th=None, hairpin_th=None, bases='TTTTTTTTTTTTTTTTTTTT', tail=None)] +[Oligo(name=None, tm=70.0, penalty=-123.0, span=Span(refname='chr1', start=1, end=20, strand=), bases='GGGGGGGGGGGGGGGGGGGG', tail=None), Oligo(name=None, tm=70.0, penalty=-123.0, span=Span(refname='chr1', start=101, end=120, strand=), bases='TTTTTTTTTTTTTTTTTTTT', tail=None)] ``` """ # noqa: E501 diff --git a/prymer/primer3/primer3.py b/prymer/primer3/primer3.py index 977a05b..5addb1b 100644 --- a/prymer/primer3/primer3.py +++ b/prymer/primer3/primer3.py @@ -43,7 +43,7 @@ ``` -The `design_oligos()` method on `Primer3` is used to design the primers given a +The `design()` method on `Primer3` is used to design the primers given a [`Primer3Input`][prymer.primer3.primer3_input.Primer3Input]. The latter includes all the parameters and target region. @@ -63,13 +63,13 @@ primer_and_amplicon_params=params, \ task=DesignLeftPrimersTask(), \ ) ->>> left_result = designer.design_oligos(design_input=design_input) +>>> left_result = designer.design(design_input=design_input) ``` The `left_result` returns the [`Primer3Result`][prymer.primer3.primer3.Primer3Result] container class. It contains two attributes: -1. `filtered_designs`: filtered and ordered (by objective function score) list of primer pairs or +1. `designs`: filtered and ordered (by objective function score) list of primer pairs or single primers that were returned by Primer3. 2. `failures`: ordered list of [`Primer3Failures`][prymer.primer3.primer3.Primer3Failure] detailing design failure reasons and corresponding count. @@ -84,7 +84,7 @@ ``` -While`filtered_designs` attribute on `Primer3Result` may be used to access the list of primers or +While the `designs` attribute on `Primer3Result` may be used to access the list of primers or primer pairs, it is more convenient to use the `primers()` and `primer_pairs()` methods to return the designed primers or primer pairs (use the method corresponding to the input task) so that the proper type is returned (i.e. [`Primer`][prymer.api.primer.Primer] or @@ -343,7 +343,7 @@ def _screen_pair_results( valid_primer_pair_designs.append(primer_pair) return valid_primer_pair_designs, dinuc_pair_failures - def design_oligos(self, design_input: Primer3Input) -> Primer3Result: # noqa: C901 + def design(self, design_input: Primer3Input) -> Primer3Result: # noqa: C901 """Designs primers, primer pairs, and/or internal probes given a target region. Args: @@ -491,7 +491,7 @@ def _build_oligos( Args: design_input: the target region, design task, specifications, and scoring penalties - design_results: design results emitted by Primer3 and captured by design_oligos() + design_results: design results emitted by Primer3 and captured by design() design_region: the padded design region design_task: the design task unmasked_design_seq: the reference sequence corresponding to the target region @@ -537,9 +537,6 @@ def _build_oligos( tm=float(design_results[f"PRIMER_{design_task.task_type}_{idx}_TM"]), penalty=float(design_results[f"PRIMER_{design_task.task_type}_{idx}_PENALTY"]), span=span, - self_any_th=float(design_results[f"{key}_SELF_ANY_TH"]), - self_end_th=float(design_results[f"{key}_SELF_END_TH"]), - hairpin_th=float(design_results[f"{key}_HAIRPIN_TH"]), ) ) return primers @@ -581,7 +578,7 @@ def _build_primer_pairs( Args: design_input: the target region, design task, specifications, and scoring penalties - design_results: design results emitted by Primer3 and captured by design_oligos() + design_results: design results emitted by Primer3 and captured by design() design_region: the padded design region unmasked_design_seq: the reference sequence corresponding to the target region diff --git a/prymer/primer3/primer3_input.py b/prymer/primer3/primer3_input.py index 36189b0..aca2f6a 100644 --- a/prymer/primer3/primer3_input.py +++ b/prymer/primer3/primer3_input.py @@ -72,11 +72,6 @@ PRIMER_MAX_NS_ACCEPTED -> 1 PRIMER_LOWERCASE_MASKING -> 1 PRIMER_NUM_RETURN -> 5 -PRIMER_MAX_SELF_ANY -> 8.0 -PRIMER_MAX_SELF_ANY_TH -> 53.0 -PRIMER_MAX_SELF_END -> 3.0 -PRIMER_MAX_SELF_END_TH -> 53.0 -PRIMER_MAX_HAIRPIN_TH -> 53.0 PRIMER_PAIR_WT_PRODUCT_SIZE_LT -> 1 PRIMER_PAIR_WT_PRODUCT_SIZE_GT -> 1 PRIMER_PAIR_WT_PRODUCT_TM_LT -> 0.0 @@ -90,9 +85,6 @@ PRIMER_WT_SIZE_GT -> 0.1 PRIMER_WT_TM_LT -> 1.0 PRIMER_WT_TM_GT -> 1.0 -PRIMER_WT_SELF_ANY_TH -> 0.0 -PRIMER_WT_SELF_END_TH -> 0.0 -PRIMER_WT_HAIRPIN_TH -> 0.0 """ from dataclasses import MISSING diff --git a/prymer/primer3/primer3_parameters.py b/prymer/primer3/primer3_parameters.py index f5ccf80..2d427fb 100644 --- a/prymer/primer3/primer3_parameters.py +++ b/prymer/primer3/primer3_parameters.py @@ -9,15 +9,11 @@ class stores user input for primer design and maps it to the correct Primer3 fie 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 PrimerAndAmpliconParameters class stores commonly used constraints for primer design: +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 PrimerAndAmpliconParameters also stores common -default settings to minimize the tendendancy of primers and probes to anneal to one another. This -self-complementarity can make PCR reactions less efficient and potentially yield nonspecific -amplification. Primer3 supports thermodynamic-based thresholds as well as parameters for maximal -alignment scores. +and sets the GC clamp to be no larger than 5. The `to_input_tags()` method in `PrimerAndAmpliconParameters` converts these parameters into tag-values pairs for use when executing `Primer3`. @@ -25,11 +21,8 @@ class stores user input for primer design and maps it to the correct Primer3 fie The [`ProbeParameters`][prymer.primer3.primer3_parameters.ProbeParameters] class stores user input for internal probe design and maps it to the correct Primer3 fields. -Similar to the PrimerAndAmpliconParameters class, the ProbeParameters class can be used to -specify the acceptable ranges of probe sizes, melting temperatures, and GC content. A region can be -excluded from internal probe design based on its start and the length of the region to exclude. This -attribute can help avoid regions that are problematic for oligo design, like low-complexity -sequence tracts. +Similar to the `PrimerAndAmpliconParameters` class, the `ProbeParameters` class can be used to +specify the acceptable ranges of probe sizes, melting temperatures, and GC content. ## Examples @@ -63,18 +56,12 @@ class stores user input for internal probe design and maps it to the correct Pri PRIMER_MAX_NS_ACCEPTED -> 1 PRIMER_LOWERCASE_MASKING -> 1 PRIMER_NUM_RETURN -> 5 -PRIMER_MAX_SELF_ANY -> 8.0 -PRIMER_MAX_SELF_ANY_TH -> 53.0 -PRIMER_MAX_SELF_END -> 3.0 -PRIMER_MAX_SELF_END_TH -> 53.0 -PRIMER_MAX_HAIRPIN_TH -> 53.0 ``` """ import warnings from dataclasses import dataclass -from dataclasses import field from typing import Any from prymer.api.minoptmax import MinOptMax @@ -97,30 +84,6 @@ class PrimerAndAmpliconParameters: primer_max_dinuc_bases: the maximal number of bases in a dinucleotide run in a primer avoid_masked_bases: whether Primer3 should avoid designing primers in soft-masked regions number_primers_return: the number of primers to return - primer_max_self_any: the maximal local alignment score of aligning the primer to itself - primer_max_self_any_thermo: the maximal melting temperature of the most stable structure - resulting from aligning the primer to itself - primer_max_self_end: the maximal 3' anchored global alignment score of aligning the primer - to itself - primer_max_self_end_thermo: the maximal melting temperature of the most stable structure - resulting from aligning the 3' end of the primer - primer_max_hairpin_thermo: the maximal melting temperature of the most stable hairpin - structure of the primer - - Primer3 uses both thermodynamic and alignment-based approaches to quantify primer - self-complementarity. - - `primer_max_self_any`, `primer_max_self_any_thermo`, `primer_max_self_end`, - `primer_max_self_end_thermo`, and `primer_max_hairpin_thermo` are all set to default values as - specified in the Primer3 manual. The default values of the thermodynamic attributes - (ending in `_th`) are set to 10 degrees less than the minimal melting temperature specified for - primer design. - - For `primer_max_self_any` and `primer_max_self_end`, a score of 0.00 indicates that there is no - reasonable local alignment across the individual primer under consideration. - - In general, these settings are meant to limit problematic oligo self-complementarity - and avoid primer-dimers or other nonspecific binding of oligos to target sequences. Please see the Primer3 manual for additional details: https://primer3.org/manual.html#globalTags @@ -137,11 +100,6 @@ class PrimerAndAmpliconParameters: primer_max_dinuc_bases: int = 6 avoid_masked_bases: bool = True number_primers_return: int = 5 - primer_max_self_any: float = 8.00 - primer_max_self_any_thermo: float = field(init=False) - primer_max_self_end: float = 3.0 - primer_max_self_end_thermo: float = field(init=False) - primer_max_hairpin_thermo: float = field(init=False) def __post_init__(self) -> None: if self.primer_max_dinuc_bases % 2 == 1: @@ -153,12 +111,6 @@ def __post_init__(self) -> None: if self.gc_clamp[0] > self.gc_clamp[1]: raise ValueError("Min primer GC-clamp must be <= max primer GC-clamp") - # Set melting temperature thresholds to be 10 degrees less than the minimum primer tm - default_thermo_tm: float = self.primer_tms.min - 10.0 - object.__setattr__(self, "primer_max_self_any_thermo", default_thermo_tm) - object.__setattr__(self, "primer_max_self_end_thermo", default_thermo_tm) - object.__setattr__(self, "primer_max_hairpin_thermo", default_thermo_tm) - def to_input_tags(self) -> dict[Primer3InputTag, Any]: """Converts input params to Primer3InputTag to feed directly into Primer3.""" mapped_dict: dict[Primer3InputTag, Any] = { @@ -184,11 +136,6 @@ def to_input_tags(self) -> dict[Primer3InputTag, Any]: Primer3InputTag.PRIMER_MAX_NS_ACCEPTED: self.primer_max_Ns, Primer3InputTag.PRIMER_LOWERCASE_MASKING: 1 if self.avoid_masked_bases else 0, Primer3InputTag.PRIMER_NUM_RETURN: self.number_primers_return, - Primer3InputTag.PRIMER_MAX_SELF_ANY: self.primer_max_self_any, - Primer3InputTag.PRIMER_MAX_SELF_ANY_TH: self.primer_max_self_any_thermo, - Primer3InputTag.PRIMER_MAX_SELF_END: self.primer_max_self_end, - Primer3InputTag.PRIMER_MAX_SELF_END_TH: self.primer_max_self_end_thermo, - Primer3InputTag.PRIMER_MAX_HAIRPIN_TH: self.primer_max_hairpin_thermo, } return mapped_dict @@ -233,41 +180,12 @@ class ProbeParameters: probe_max_dinuc_bases: the max number of bases in a dinucleotide run in a probe 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: the maximal local alignment score of aligning the probe to itself - probe_max_self_any_thermo: the maximal melting temperature of the most stable structure - resulting from aligning the probe to itself - probe_max_self_end: max allowable 3'-anchored global alignment score when testing a - probe for self-complementarity - probe_max_self_end_thermo: the maximal melting temperature of the most stable structure - resulting from aligning the 3' end of the probe - probe_max_hairpin_thermo: most stable monomer structure as calculated by a thermodynamic - approach The attributes that have default values specified take their default values from the Primer3 manual. Please see the Primer3 manual for additional details: https://primer3.org/manual.html#globalTags - Primer3 uses both thermodynamic and alignment-based approaches to quantify oligo - self-complementarity. - - `primer_max_self_any`, `primer_max_self_any_thermo`, `primer_max_self_end`, - `primer_max_self_end_thermo`, and `primer_max_hairpin_thermo` are all set to default values as - specified in the Primer3 manual. The default values of the thermodynamic attributes - (ending in `_th`) are set to 10 degrees less than the minimal melting temperature specified for - primer design. - - For `probe_max_self_any` and `probe_max_self_end`, a score of 0.00 indicates that there is no - reasonable local alignment across the individual primer under consideration. These scores are - always positive. - - In general, these settings are meant to limit problematic oligo self-complementarity - and avoid primer-dimers or other nonspecific binding of probes to target sequences. - - Note that the Primer3 documentation advises that, while `probe_max_end_any` is meaningless - when applied to internal probes 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. """ @@ -278,11 +196,6 @@ class ProbeParameters: probe_max_dinuc_bases: int = 4 probe_max_polyX: int = 5 probe_max_Ns: int = 0 - probe_max_self_any: float = 12.0 - probe_max_self_any_thermo: float = field(init=False) - probe_max_self_end: float = 12.0 - probe_max_self_end_thermo: float = field(init=False) - probe_max_hairpin_thermo: float = field(init=False) def __post_init__(self) -> None: if not isinstance(self.probe_sizes.min, int): @@ -292,12 +205,6 @@ def __post_init__(self) -> None: if self.probe_max_dinuc_bases % 2 == 1: raise ValueError("Max threshold for dinucleotide bases must be an even number of bases") - # Set melting temperature thresholds to be 10 degrees less than the minimum primer tm - default_thermo_tm: float = self.probe_tms.min - 10.0 - object.__setattr__(self, "probe_max_self_any_thermo", default_thermo_tm) - object.__setattr__(self, "probe_max_self_end_thermo", default_thermo_tm) - object.__setattr__(self, "probe_max_hairpin_thermo", default_thermo_tm) - def to_input_tags(self) -> dict[Primer3InputTag, Any]: """Converts input params to Primer3InputTag to feed directly into Primer3.""" mapped_dict: dict[Primer3InputTag, Any] = { @@ -312,11 +219,6 @@ def to_input_tags(self) -> dict[Primer3InputTag, Any]: 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, } return mapped_dict diff --git a/prymer/primer3/primer3_weights.py b/prymer/primer3/primer3_weights.py index 6d6ffd4..210959e 100644 --- a/prymer/primer3/primer3_weights.py +++ b/prymer/primer3/primer3_weights.py @@ -18,9 +18,9 @@ Example: >>> 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, primer_wt_self_any_thermo=0.0, primer_wt_self_end_thermo=0.0, primer_wt_hairpin_thermo=0.0) +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) >>> 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, primer_wt_self_any_thermo=0.0, primer_wt_self_end_thermo=0.0, primer_wt_hairpin_thermo=0.0) +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 @@ -56,24 +56,6 @@ class PrimerAndAmpliconWeights: `PrimerAndAmpliconParameters.primer_gcs.opt` primer_gc_gt: penalty weight for primers with GC percent higher than `PrimerAndAmpliconParameters.primer_gcs.opt` - primer_self_any: penalty for the individual primer self-binding value - defined by`PrimerAndAmpliconParameters.primer_max_self_any` - primer_self_end: penalty for the individual primer self-binding value - defined by `PrimerAndAmpliconParameters.primer_max_self_end` - primer_size_lt: weight for primers shorter than - `PrimerAndAmpliconParameters.primer_sizes.opt` - primer_size_gt: weight for primers longer than - `PrimerAndAmpliconParameters.primer_sizes.opt` - primer_tm_lt: weight for primers with Tm lower than - `PrimerAndAmpliconParameters.primer_tms.opt` - primer_tm_gt: weight for primers with Tm higher than - `PrimerAndAmpliconParameters.primer_tms.opt` - primer_wt_self_any_thermo: penalty for the individual primer self-binding value defined by - in `PrimerAndAmpliconParameters.primer_max_self_any_thermo` - primer_wt_self_end_thermo: penalty for the individual primer self binding value defined by - `PrimerAndAmpliconParameters.primer_max_self_end_thermo` - primer_wt_hairpin_thermo: penalty for the individual primer hairpin structure value defined - by `PrimerAndAmpliconParameters.primer_max_hairpin_thermo """ @@ -90,9 +72,6 @@ class PrimerAndAmpliconWeights: primer_size_gt: float = 0.1 primer_tm_lt: float = 1.0 primer_tm_gt: float = 1.0 - primer_wt_self_any_thermo: float = 0.0 - primer_wt_self_end_thermo: float = 0.0 - primer_wt_hairpin_thermo: float = 0.0 def to_input_tags(self) -> dict[Primer3InputTag, Any]: """Maps weights to Primer3InputTag to feed directly into Primer3.""" @@ -110,9 +89,6 @@ def to_input_tags(self) -> dict[Primer3InputTag, Any]: Primer3InputTag.PRIMER_WT_SIZE_GT: self.primer_size_gt, Primer3InputTag.PRIMER_WT_TM_LT: self.primer_tm_lt, Primer3InputTag.PRIMER_WT_TM_GT: self.primer_tm_gt, - Primer3InputTag.PRIMER_WT_SELF_ANY_TH: self.primer_wt_self_any_thermo, - Primer3InputTag.PRIMER_WT_SELF_END_TH: self.primer_wt_self_end_thermo, - Primer3InputTag.PRIMER_WT_HAIRPIN_TH: self.primer_wt_hairpin_thermo, } return mapped_dict @@ -128,12 +104,6 @@ class ProbeWeights: probe_tm_gt: penalty for probes with a Tm greater than `ProbeParameters.probe_tms.opt` probe_gc_lt: penalty for probes with GC content lower than `ProbeParameters.probe_gcs.opt` probe_gc_gt: penalty for probes with GC content greater than `ProbeParameters.probe_gcs.opt` - probe_wt_self_any: penalty for probe self-complementarity as defined in - `ProbeParameters.probe_max_self_any` - probe_wt_self_end: penalty for probe 3' complementarity as defined in - `ProbeParameters.probe_max_self_end` - probe_wt_hairpin_th: penalty for the most stable primer hairpin structure value as defined - in `ProbeParameters.probe_max_hairpin_thermo` """ @@ -143,9 +113,6 @@ class ProbeWeights: probe_tm_gt: float = 1.0 probe_gc_lt: float = 0.5 probe_gc_gt: float = 0.5 - probe_wt_self_any: float = 1.0 - probe_wt_self_end: float = 1.0 - probe_wt_hairpin_th: float = 1.0 def to_input_tags(self) -> dict[Primer3InputTag, Any]: """Maps weights to Primer3InputTag to feed directly into Primer3.""" @@ -156,8 +123,5 @@ def to_input_tags(self) -> dict[Primer3InputTag, Any]: 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_wt_self_any, - Primer3InputTag.PRIMER_INTERNAL_WT_SELF_END: self.probe_wt_self_end, - Primer3InputTag.PRIMER_INTERNAL_WT_HAIRPIN_TH: self.probe_wt_hairpin_th, } return mapped_dict diff --git a/tests/primer3/test_primer3.py b/tests/primer3/test_primer3.py index 50760e3..091af50 100644 --- a/tests/primer3/test_primer3.py +++ b/tests/primer3/test_primer3.py @@ -142,11 +142,11 @@ def valid_primer_pairs( return primer_pairs -def test_design_oligos_raises( +def test_design_raises( genome_ref: Path, single_primer_params: PrimerAndAmpliconParameters, ) -> None: - """Test that design_oligos() raises when given an invalid argument.""" + """Test that design() raises when given an invalid argument.""" target = Span(refname="chr1", start=201, end=250, strand=Strand.POSITIVE) @@ -160,7 +160,7 @@ def test_design_oligos_raises( task=DesignLeftPrimersTask(), ) with pytest.raises(ValueError, match="Primer3 failed"): - Primer3(genome_fasta=genome_ref).design_oligos(design_input=invalid_design_input) + Primer3(genome_fasta=genome_ref).design(design_input=invalid_design_input) # TODO: add other Value Errors @@ -178,7 +178,7 @@ def test_left_primer_valid_designs( with Primer3(genome_fasta=genome_ref) as designer: for _ in range(10): # run many times to ensure we can re-use primer3 - left_result = designer.design_oligos(design_input=design_input) + left_result = designer.design(design_input=design_input) designed_lefts: list[Oligo] = left_result.primers() assert all(isinstance(design, Oligo) for design in designed_lefts) for actual_design in designed_lefts: @@ -225,7 +225,7 @@ def test_right_primer_valid_designs( ) with Primer3(genome_fasta=genome_ref) as designer: for _ in range(10): # run many times to ensure we can re-use primer3 - right_result: Primer3Result = designer.design_oligos(design_input=design_input) + right_result: Primer3Result = designer.design(design_input=design_input) designed_rights: list[Oligo] = right_result.primers() assert all(isinstance(design, Oligo) for design in designed_rights) @@ -273,7 +273,7 @@ def test_primer_pair_design( task=DesignPrimerPairsTask(), ) with Primer3(genome_fasta=genome_ref) as designer: - pair_result: Primer3Result = designer.design_oligos(design_input=design_input) + pair_result: Primer3Result = designer.design(design_input=design_input) designed_pairs: list[PrimerPair] = pair_result.primer_pairs() assert all(isinstance(design, PrimerPair) for design in designed_pairs) lefts = [primer_pair.left_primer for primer_pair in designed_pairs] @@ -363,7 +363,7 @@ def test_fasta_close_valid( with pytest.raises( RuntimeError, match="Error, trying to use a subprocess that has already been terminated" ): - designer.design_oligos(design_input=design_input) + designer.design(design_input=design_input) @pytest.mark.parametrize( @@ -641,4 +641,4 @@ def test_probe_design_raises(genome_ref: Path, valid_probe_params: ProbeParamete with pytest.raises( ValueError, match="Target region required to be at least as large as the" ): - designer.design_oligos(design_input=design_input) + designer.design(design_input=design_input) diff --git a/tests/primer3/test_primer3_input.py b/tests/primer3/test_primer3_input.py index 27bb58d..54d6c8d 100644 --- a/tests/primer3/test_primer3_input.py +++ b/tests/primer3/test_primer3_input.py @@ -43,7 +43,7 @@ def valid_primer_weights() -> PrimerAndAmpliconWeights: @pytest.fixture def valid_probe_weights() -> ProbeWeights: - return ProbeWeights(probe_wt_hairpin_th=100.0) + return ProbeWeights() @pytest.mark.parametrize( @@ -68,7 +68,7 @@ def test_primer_design_only_valid( primer_and_amplicon_params=valid_primer_amplicon_params, ) mapped_dict = test_input.to_input_tags(design_region=test_design_region) - assert len(mapped_dict.keys()) == 46 + assert len(mapped_dict.keys()) == 38 @pytest.mark.parametrize( @@ -101,9 +101,8 @@ def test_probe_design_only_valid( ) mapped_dict = test_input.to_input_tags(design_region=test_design_region) assert mapped_dict[Primer3InputTag.PRIMER_PICK_INTERNAL_OLIGO] == 1 - assert mapped_dict[Primer3InputTag.PRIMER_INTERNAL_WT_HAIRPIN_TH] == 100.0 - assert len(mapped_dict.keys()) == 29 + assert len(mapped_dict.keys()) == 21 # test instantiation of default `ProbeWeights` when they are not provided altered_input = Primer3Input( @@ -114,7 +113,7 @@ def test_probe_design_only_valid( primer_and_amplicon_params=None, ) altered_mapped_dict = altered_input.to_input_tags(design_region=test_target) - assert altered_mapped_dict[Primer3InputTag.PRIMER_INTERNAL_WT_HAIRPIN_TH] == 1.0 + assert altered_mapped_dict[Primer3InputTag.PRIMER_INTERNAL_WT_GC_PERCENT_GT] == 0.5 def test_probe_design_only_raises(valid_probe_weights: ProbeWeights) -> None: diff --git a/tests/primer3/test_primer3_parameters.py b/tests/primer3/test_primer3_parameters.py index 589b130..efd4d24 100644 --- a/tests/primer3/test_primer3_parameters.py +++ b/tests/primer3/test_primer3_parameters.py @@ -53,11 +53,6 @@ def test_probe_param_construction_valid( assert valid_probe_params.probe_gcs.min == 45.0 assert valid_probe_params.probe_gcs.opt == 55.0 assert valid_probe_params.probe_gcs.max == 60.0 - # assert that `probe_max_self_any_thermo` was set to 55.0 (probe_tm.min - 10.0) - assert valid_probe_params.probe_max_self_any_thermo == 55.0 - mapped_dict = valid_probe_params.to_input_tags() - # because `probe_excluded_region` is not given, we do not expect a key in `mapped_dict` - assert Primer3InputTag.SEQUENCE_INTERNAL_EXCLUDED_REGION not in mapped_dict def test_primer_amplicon_param_construction_raises( diff --git a/tests/primer3/test_primer3_weights.py b/tests/primer3/test_primer3_weights.py index 1bf6719..f1a7726 100644 --- a/tests/primer3/test_primer3_weights.py +++ b/tests/primer3/test_primer3_weights.py @@ -20,10 +20,7 @@ def test_primer_weights_valid() -> None: assert test_dict[Primer3InputTag.PRIMER_WT_SIZE_GT] == 0.1 assert test_dict[Primer3InputTag.PRIMER_WT_TM_LT] == 1.0 assert test_dict[Primer3InputTag.PRIMER_WT_TM_GT] == 1.0 - assert test_dict[Primer3InputTag.PRIMER_WT_SELF_ANY_TH] == 0.0 - assert test_dict[Primer3InputTag.PRIMER_WT_SELF_END_TH] == 0.0 - assert test_dict[Primer3InputTag.PRIMER_WT_HAIRPIN_TH] == 0.0 - assert len((test_dict.values())) == 16 + assert len((test_dict.values())) == 13 def test_probe_weights_valid() -> None: @@ -35,10 +32,7 @@ def test_probe_weights_valid() -> None: 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 + assert len((test_dict.values())) == 6 def test_primer_weights_to_input_tags() -> None: