From e9b59dea862099ad7d21132bfddfef324a280189 Mon Sep 17 00:00:00 2001 From: Erin McAuley Date: Wed, 25 Sep 2024 10:38:03 -0400 Subject: [PATCH] feat: add Probe object --- prymer/api/probe.py | 22 ++++++++++++++++ prymer/primer3/primer3.py | 47 ++++++++++++++++++++--------------- tests/primer3/test_primer3.py | 4 ++- 3 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 prymer/api/probe.py diff --git a/prymer/api/probe.py b/prymer/api/probe.py new file mode 100644 index 0000000..79f7f4d --- /dev/null +++ b/prymer/api/probe.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass + +from fgpyo.util.metric import Metric + +from prymer.api.primer import Primer + + +@dataclass(frozen=True, init=True, kw_only=True, slots=True) +class Probe(Primer, Metric["Probe"]): + """Stores the properties of the designed Probe. Inherits `tm`, `penalty`, + `span`, `bases`, and `tail` from `Primer`. + + Attributes: + 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_th: float + self_end_th: float + hairpin_th: float diff --git a/prymer/primer3/primer3.py b/prymer/primer3/primer3.py index 6543699..c7916bf 100644 --- a/prymer/primer3/primer3.py +++ b/prymer/primer3/primer3.py @@ -140,9 +140,9 @@ from fgpyo.util.metric import Metric from prymer.api.primer import Primer -from prymer.api.probe import Probe from prymer.api.primer_like import PrimerLike from prymer.api.primer_pair import PrimerPair +from prymer.api.probe import Probe from prymer.api.span import Span from prymer.api.span import Strand from prymer.api.variant_lookup import SimpleVariant @@ -366,11 +366,12 @@ def design_oligos(self, design_input: Primer3Input) -> Primer3Result: # noqa: C f"Error, trying to use a subprocess that has already been " f"terminated, return code {self._subprocess.returncode}" ) + design_region: Span match design_input.task: case PickHybProbeOnly(): - design_region: Span = design_input.target + design_region = design_input.target case _: - design_region: Span = self._create_design_region( + design_region = self._create_design_region( target_region=design_input.target, max_amplicon_length=design_input.primer_and_amplicon_params.max_amplicon_length, min_primer_length=design_input.primer_and_amplicon_params.min_primer_length, @@ -438,12 +439,13 @@ def primer3_error(message: str) -> None: primer3_error("Primer3 failed") match design_input.task: - case PickHybProbeOnly(): # Probe design + case PickHybProbeOnly(): # Probe design all_probe_results: list[Probe] = Primer3._build_probes( design_input=design_input, design_results=primer3_results, design_region=design_region, - unmasked_design_seq=soft_masked) + unmasked_design_seq=soft_masked, + ) return Primer3._assemble_single_designs( design_input=design_input, @@ -483,10 +485,10 @@ def primer3_error(message: str) -> None: @staticmethod def _build_probes( - design_input: Primer3Input, - design_results: dict[str, str], - design_region: Span, - unmasked_design_seq: str, + design_input: Primer3Input, + design_results: dict[str, str], + design_region: Span, + unmasked_design_seq: str, ) -> list[Probe]: count: int = _check_design_results(design_input, design_results) task_key = design_input.task.task_type @@ -585,9 +587,13 @@ def _build_primers( return primers @staticmethod - def _assemble_single_designs(design_input: Primer3Input, design_results: dict[str, str], unfiltered_designs: Union[list[Primer], list[Probe]] + def _assemble_single_designs( + design_input: Primer3Input, + design_results: dict[str, str], + unfiltered_designs: Union[list[Primer], list[Probe]], ) -> Primer3Result: - """Screens oligo designs (primers or probes) emitted by Primer3 for acceptable dinucleotide runs and extracts failure reasons for failed designs.""" + """Screens oligo designs (primers or probes) emitted by Primer3 for acceptable dinucleotide + runs and extracts failure reasons for failed designs.""" valid_oligo_designs = [ design @@ -607,7 +613,6 @@ def _assemble_single_designs(design_input: Primer3Input, design_results: dict[st ) return design_candidates - @staticmethod def _build_primer_pairs( design_input: Primer3Input, @@ -789,8 +794,9 @@ def _create_design_region( return design_region + def _check_design_results(design_input: Primer3Input, design_results: dict[str, str]) -> int: - """Checks for any additional Primer3 errors and reports out the count of designs emitted by Primer3.""" + """Checks for any additional Primer3 errors and reports out the count of emitted designs.""" count_tag = design_input.task.count_tag maybe_count: Optional[str] = design_results.get(count_tag) if maybe_count is None: # no count tag was found @@ -803,13 +809,14 @@ def _check_design_results(design_input: Primer3Input, design_results: dict[str, return count -def _has_acceptable_dinuc_run(design_input: Primer3Input, oligo_design: Union[Primer, Probe]) -> bool: + +def _has_acceptable_dinuc_run( + design_input: Primer3Input, oligo_design: Union[Primer, Probe] +) -> bool: + max_dinuc_bases: int if type(oligo_design) is Primer: - max_dinuc_bases: int = design_input.primer_and_amplicon_params.primer_max_dinuc_bases + max_dinuc_bases = design_input.primer_and_amplicon_params.primer_max_dinuc_bases elif type(oligo_design) is Probe: - max_dinuc_bases: int = design_input.probe_params.probe_max_dinuc_bases + max_dinuc_bases = design_input.probe_params.probe_max_dinuc_bases - return ( - oligo_design.longest_dinucleotide_run_length() - <= max_dinuc_bases - ) + return oligo_design.longest_dinucleotide_run_length() <= max_dinuc_bases diff --git a/tests/primer3/test_primer3.py b/tests/primer3/test_primer3.py index 223ccba..bc6faed 100644 --- a/tests/primer3/test_primer3.py +++ b/tests/primer3/test_primer3.py @@ -12,9 +12,10 @@ from prymer.api.span import Span from prymer.api.span import Strand from prymer.api.variant_lookup import cached -from prymer.primer3.primer3 import Primer3, _has_acceptable_dinuc_run +from prymer.primer3.primer3 import Primer3 from prymer.primer3.primer3 import Primer3Failure from prymer.primer3.primer3 import Primer3Result +from prymer.primer3.primer3 import _has_acceptable_dinuc_run from prymer.primer3.primer3_input import Primer3Input from prymer.primer3.primer3_parameters import PrimerAndAmpliconParameters from prymer.primer3.primer3_parameters import ProbeParameters @@ -180,6 +181,7 @@ def test_internal_probe_valid_designs( valid_probes = designer.design_oligos(design_input=design_input) print(valid_probes) + def test_left_primer_valid_designs( genome_ref: Path, single_primer_params: PrimerAndAmpliconParameters,