Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge Primer*Weights into Primer3Parameters, remove Primer3Input #112

Merged
merged 7 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions prymer/primer3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from prymer.primer3.primer3_failure_reason import Primer3FailureReason
from prymer.primer3.primer3_input import Primer3Input
from prymer.primer3.primer3_input_tag import Primer3InputTag
from prymer.primer3.primer3_parameters import PrimerAndAmpliconParameters
from prymer.primer3.primer3_parameters import AmpliconParameters
from prymer.primer3.primer3_parameters import ProbeParameters
from prymer.primer3.primer3_task import DesignLeftPrimersTask
from prymer.primer3.primer3_task import DesignPrimerPairsTask
Expand All @@ -24,7 +24,7 @@
"DesignPrimerPairsTask",
"DesignRightPrimersTask",
"PickHybProbeOnly",
"PrimerAndAmpliconParameters",
"AmpliconParameters",
"ProbeParameters",
"ProbeWeights",
"PrimerAndAmpliconWeights",
Expand Down
52 changes: 28 additions & 24 deletions prymer/primer3/primer3.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,21 @@
```

The `design()` method on `Primer3` is used to design the primers given a
[`Primer3Input`][prymer.primer3.primer3_input.Primer3Input]. The latter includes all the
[`Primer3Parameters`][prymer.primer3.primer3_input.Primer3Parameters]. The latter includes all the
parameters and target region.

```python
>>> from prymer.primer3.primer3_parameters import PrimerAndAmpliconParameters
>>> from prymer.primer3.primer3_parameters import AmpliconParameters
>>> from prymer import MinOptMax
>>> target = Span(refname="chr1", start=201, end=250, strand=Strand.POSITIVE)
>>> params = PrimerAndAmpliconParameters( \
>>> params = AmpliconParameters( \
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), \
primer_tms=MinOptMax(min=63.0, max=67.0, opt=65.0), \
primer_gcs=MinOptMax(min=30.0, max=65.0, opt=45.0), \
)
>>> design_input = Primer3Input( \
>>> design_input = AmpliconParameters( \
target=target, \
primer_and_amplicon_params=params, \
task=DesignLeftPrimersTask(), \
Expand Down Expand Up @@ -138,8 +138,9 @@
from prymer.model import PrimerPair
from prymer.model import Span
from prymer.model import Strand
from prymer.primer3 import AmpliconParameters, ProbeParameters
from prymer.primer3.primer3_failure_reason import Primer3FailureReason
from prymer.primer3.primer3_input import Primer3Input
from prymer.primer3.primer3_parameters import Primer3Parameters
from prymer.primer3.primer3_input_tag import Primer3InputTag
from prymer.primer3.primer3_task import DesignLeftPrimersTask
from prymer.primer3.primer3_task import DesignPrimerPairsTask
Expand Down Expand Up @@ -292,7 +293,7 @@ def get_design_sequences(self, region: Span) -> tuple[str, str]:

@staticmethod
def _screen_pair_results(
design_input: Primer3Input, designed_primer_pairs: list[PrimerPair]
design_input: AmpliconParameters, designed_primer_pairs: list[PrimerPair]
) -> tuple[list[PrimerPair], list[Oligo]]:
"""Screens primer pair designs emitted by Primer3 for dinucleotide run length.

Expand All @@ -310,21 +311,21 @@ def _screen_pair_results(
valid: bool = True
if (
primer_pair.left_primer.longest_dinucleotide_run_length
> design_input.primer_and_amplicon_params.primer_max_dinuc_bases
> design_input.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.primer_and_amplicon_params.primer_max_dinuc_bases
> design_input.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
if valid: # if neither failed, append the pair to a list of valid designs
valid_primer_pair_designs.append(primer_pair)
return valid_primer_pair_designs, dinuc_pair_failures

def design(self, design_input: Primer3Input) -> Primer3Result: # noqa: C901
def design(self, design_input: Primer3Parameters) -> Primer3Result: # noqa: C901
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would need to take:

amplicon_parameters: AmpliconParameters | None, probe_parameter: ProbeParameters | None

What you have works for the current set of use cases, but one primer3 task we haven't implemented yet supports designing primer pairs and probes concurrently, and then you need both sets of parameters.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I leave it for now, and we can decide later if we want to have two parameters to design, or to create a third parameters class:

class AmpliconAndProbeParameters(Primer3Parameters):
    amplicon_params: AmpliconParams
    probe_params: ProbeParams

"""Designs primers, primer pairs, and/or internal probes given a target region.

Args:
Expand All @@ -342,19 +343,21 @@ def design(self, design_input: Primer3Input) -> Primer3Result: # noqa: C901
design_region: Span
match design_input.task:
case PickHybProbeOnly():
if design_input.target.length < design_input.probe_params.probe_sizes.min:
input = typing.cast(ProbeParameters, design_input)
if input.target.length < input.probe_sizes.min:
raise ValueError(
"Target region required to be at least as large as the"
" minimal probe size: "
f"target length: {design_input.target.length}, "
f"minimal probe size: {design_input.probe_params.probe_sizes.min}"
f"target length: {input.target.length}, "
f"minimal probe size: {input.probe_sizes.min}"
)
design_region = design_input.target
case DesignRightPrimersTask() | DesignLeftPrimersTask() | DesignPrimerPairsTask():
input = typing.cast(AmpliconParameters, design_input)
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,
target_region=input.target,
max_amplicon_length=input.max_amplicon_length,
min_primer_length=input.min_primer_length,
)
case _ as unreachable:
assert_never(unreachable) # pragma: no cover
Expand Down Expand Up @@ -403,14 +406,15 @@ def design(self, design_input: Primer3Input) -> Primer3Result: # noqa: C901

match design_input.task:
case DesignPrimerPairsTask(): # Primer pair design
input = typing.cast(AmpliconParameters, design_input)
all_pair_results: list[PrimerPair] = Primer3._build_primer_pairs(
design_input=design_input,
design_input=input,
design_results=primer3_results,
design_region=design_region,
unmasked_design_seq=soft_masked,
)
return Primer3._assemble_primer_pairs(
design_input=design_input,
design_input=input,
design_results=primer3_results,
unfiltered_designs=all_pair_results,
)
Expand All @@ -435,7 +439,7 @@ def design(self, design_input: Primer3Input) -> Primer3Result: # noqa: C901

@staticmethod
def _build_oligos(
design_input: Primer3Input,
design_input: Primer3Parameters,
design_results: dict[str, Any],
design_region: Span,
design_task: Union[DesignLeftPrimersTask, DesignRightPrimersTask, PickHybProbeOnly],
Expand Down Expand Up @@ -515,7 +519,7 @@ def _build_oligos(

@staticmethod
def _assemble_single_designs(
design_input: Primer3Input,
design_input: Primer3Parameters,
design_results: dict[str, str],
unfiltered_designs: list[Oligo],
) -> Primer3Result:
Expand All @@ -540,7 +544,7 @@ def _assemble_single_designs(

@staticmethod
def _build_primer_pairs(
design_input: Primer3Input,
design_input: Primer3Parameters,
design_results: dict[str, Any],
design_region: Span,
unmasked_design_seq: str,
Expand Down Expand Up @@ -603,7 +607,7 @@ def _build_primer_pair(num: int, primer_pair: tuple[Oligo, Oligo]) -> PrimerPair

@staticmethod
def _assemble_primer_pairs(
design_input: Primer3Input,
design_input: AmpliconParameters,
design_results: dict[str, Any],
unfiltered_designs: list[PrimerPair],
) -> Primer3Result:
Expand Down Expand Up @@ -718,7 +722,7 @@ def _create_design_region(
return design_region


def _check_design_results(design_input: Primer3Input, design_results: dict[str, str]) -> int:
def _check_design_results(design_input: Primer3Parameters, design_results: dict[str, str]) -> int:
"""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)
Expand All @@ -733,7 +737,7 @@ def _check_design_results(design_input: Primer3Input, design_results: dict[str,
return count


def _has_acceptable_dinuc_run(design_input: Primer3Input, oligo_design: Oligo) -> bool:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I inlined this

def _has_acceptable_dinuc_run(design_input: Primer3Parameters, oligo_design: Oligo) -> bool:
"""
True if the design's longest dinucleotide run is no more than the stipulated maximum.

Expand All @@ -744,7 +748,7 @@ def _has_acceptable_dinuc_run(design_input: Primer3Input, oligo_design: Oligo) -
`ProbeParameters.probe_max_dinuc_bases`.

Args:
design_input: the Primer3Input object that wraps task-specific and design-specific params
design_input: the Primer3Parameters object that wraps task-specific and design-specific params
oligo_design: the design candidate

Returns:
Expand Down
183 changes: 0 additions & 183 deletions prymer/primer3/primer3_input.py

This file was deleted.

Loading
Loading