diff --git a/prymer/api/oligo.py b/prymer/api/oligo.py index 3631693..3282a8c 100644 --- a/prymer/api/oligo.py +++ b/prymer/api/oligo.py @@ -88,25 +88,32 @@ 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_th: self-complementarity throughout the probe as calculated by Primer3 - self_end_th: 3' end complementarity of the probe as calculated by Primer3 - hairpin_th: hairpin formation thermodynamics of the probe as calculated by Primer3 + 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 - - """ 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 diff --git a/prymer/api/primer_pair.py b/prymer/api/primer_pair.py index ad59be5..ea1a407 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_th=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_th=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=), 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)] ``` """ # noqa: E501 diff --git a/prymer/primer3/primer3_input.py b/prymer/primer3/primer3_input.py index e9fb3ac..36189b0 100644 --- a/prymer/primer3/primer3_input.py +++ b/prymer/primer3/primer3_input.py @@ -52,7 +52,6 @@ PRIMER_PICK_RIGHT_PRIMER -> 0 PRIMER_PICK_INTERNAL_OLIGO -> 0 SEQUENCE_INCLUDED_REGION -> 1,51 -PRIMER_NUM_RETURN -> 5 PRIMER_PRODUCT_OPT_SIZE -> 200 PRIMER_PRODUCT_SIZE_RANGE -> 100-250 PRIMER_PRODUCT_MIN_TM -> 55.0 @@ -72,6 +71,12 @@ PRIMER_MAX_POLY_X -> 5 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 @@ -85,6 +90,9 @@ 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 2e0c7eb..0ffaa70 100644 --- a/prymer/primer3/primer3_parameters.py +++ b/prymer/primer3/primer3_parameters.py @@ -3,8 +3,6 @@ The [`PrimerAndAmpliconParameters`][prymer.primer3.primer3_parameters.PrimerAndAmpliconParameters] class stores user input for primer design and maps it to the correct Primer3 fields. -The [`ProbeParameters`][prymer.primer3.primer3_parameters.ProbeParameters] -class stores user input for internal probe design 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, @@ -15,11 +13,24 @@ class stores user input for internal probe design and maps it to the correct Pri 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. +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. The `to_input_tags()` method in `PrimerAndAmpliconParameters` converts these parameters into tag-values pairs for use when executing `Primer3`. +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. + ## Examples ```python @@ -32,7 +43,6 @@ class stores user input for internal probe design and maps it to the correct Pri ) >>> for tag, value in params.to_input_tags().items(): \ print(f"{tag.value} -> {value}") -PRIMER_NUM_RETURN -> 5 PRIMER_PRODUCT_OPT_SIZE -> 200 PRIMER_PRODUCT_SIZE_RANGE -> 100-250 PRIMER_PRODUCT_MIN_TM -> 55.0 @@ -52,12 +62,19 @@ class stores user input for internal probe design and maps it to the correct Pri PRIMER_MAX_POLY_X -> 5 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 typing import Optional @@ -81,6 +98,30 @@ 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 @@ -97,6 +138,11 @@ 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: @@ -108,10 +154,15 @@ 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] = { - Primer3InputTag.PRIMER_NUM_RETURN: self.number_primers_return, Primer3InputTag.PRIMER_PRODUCT_OPT_SIZE: self.amplicon_sizes.opt, Primer3InputTag.PRIMER_PRODUCT_SIZE_RANGE: ( f"{self.amplicon_sizes.min}-{self.amplicon_sizes.max}" @@ -133,7 +184,14 @@ def to_input_tags(self) -> dict[Primer3InputTag, Any]: Primer3InputTag.PRIMER_MAX_POLY_X: self.primer_max_polyX, 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 @property @@ -176,24 +234,40 @@ 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: 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_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: similar to `probe_max_end_any` but uses a thermodynamic approach - to evaluate 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 probe_excluded_region: the excluded region (start, length) that probes shall not overlap + The attributes that have default values specified take their default values from the + Primer3 manual. - 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 + 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 oligos used for hybridization-based detection, + 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. @@ -207,10 +281,10 @@ class ProbeParameters: 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_any_thermo: float = field(init=False) probe_max_self_end: float = 12.0 - probe_max_self_end_thermo: float = 47.0 - probe_max_hairpin_thermo: float = 47.0 + probe_max_self_end_thermo: float = field(init=False) + probe_max_hairpin_thermo: float = field(init=False) probe_excluded_region: Optional[tuple[int, int]] = None def __post_init__(self) -> None: @@ -230,6 +304,11 @@ def __post_init__(self) -> None: "Excluded region for probe design must be given as a tuple[int, int]" "for start and length of region (e.g., (10,20))" ) + # 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.""" diff --git a/prymer/primer3/primer3_weights.py b/prymer/primer3/primer3_weights.py index 2db2fa7..6d6ffd4 100644 --- a/prymer/primer3/primer3_weights.py +++ b/prymer/primer3/primer3_weights.py @@ -16,15 +16,13 @@ ## Examples of interacting with the `PrimerAndAmpliconWeights` class +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=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) -```python ->>> 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, ...) - -``` -""" +""" # noqa: E501 from dataclasses import dataclass from typing import Any @@ -34,8 +32,7 @@ @dataclass(frozen=True, init=True, slots=True) class PrimerAndAmpliconWeights: - """Holds the weights that Primer3 uses to adjust penalties - that originate from the designed primer(s). + """Holds the primer-specific weights that Primer3 uses to adjust design penalties. 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. @@ -44,12 +41,41 @@ class PrimerAndAmpliconWeights: Please see the Primer3 manual for additional details: https://primer3.org/manual.html#globalTags - 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) - - >>> 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 + Attributes: + product_size_lt: weight for products shorter than + `PrimerAndAmpliconParameters.amplicon_sizes.opt` + product_size_gt: weight for products longer than + `PrimerAndAmpliconParameters.amplicon_sizes.opt` + product_tm_lt: weight for products with a Tm lower than + `PrimerAndAmpliconParameters.amplicon_tms.opt` + product_tm_gt: weight for products with a Tm greater than + `PrimerAndAmpliconParameters.amplicon_tms.opt` + primer_end_stability: penalty for the calculated maximum stability + for the last five 3' bases of primer + primer_gc_lt: penalty for primers with GC percent lower than + `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 + + """ product_size_lt: int = 1 product_size_gt: int = 1 @@ -64,6 +90,9 @@ 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.""" @@ -81,14 +110,32 @@ 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 @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).""" + """Holds the probe-specific weights that Primer3 uses to adjust design penalties. + + Attributes: + probe_size_lt: penalty for probes shorter than `ProbeParameters.probe_sizes.opt` + probe_size_gt: penalty for probes longer than `ProbeParameters.probe_sizes.opt` + probe_tm_lt: penalty for probes with a Tm lower than `ProbeParameters.probe_tms.opt` + 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` + + """ probe_size_lt: float = 0.25 probe_size_gt: float = 0.25 @@ -96,9 +143,9 @@ class ProbeWeights: 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 + 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.""" @@ -109,8 +156,8 @@ 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_self_any, - Primer3InputTag.PRIMER_INTERNAL_WT_SELF_END: self.probe_self_end, - Primer3InputTag.PRIMER_INTERNAL_WT_HAIRPIN_TH: self.probe_hairpin_th, + 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_parameters.py b/tests/primer3/test_primer3_parameters.py index 12e4a23..b834d90 100644 --- a/tests/primer3/test_primer3_parameters.py +++ b/tests/primer3/test_primer3_parameters.py @@ -63,6 +63,8 @@ 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 diff --git a/tests/primer3/test_primer3_weights.py b/tests/primer3/test_primer3_weights.py index 3351b74..1bf6719 100644 --- a/tests/primer3/test_primer3_weights.py +++ b/tests/primer3/test_primer3_weights.py @@ -20,7 +20,10 @@ 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 len((test_dict.values())) == 13 + 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 def test_probe_weights_valid() -> None: