Skip to content

primer3

Classes

DesignLeftPrimersTask

Bases: Primer3Task

Stores task-specific characteristics for designing left primers.

Source code in prymer/primer3/primer3_task.py
class DesignLeftPrimersTask(Primer3Task, task_type=TaskType.LEFT):
    """Stores task-specific characteristics for designing left primers."""

    @classmethod
    def _to_input_tags(cls, target: Span, design_region: Span) -> dict[Primer3InputTag, Any]:
        return {
            Primer3InputTag.PRIMER_TASK: "pick_primer_list",
            Primer3InputTag.PRIMER_PICK_LEFT_PRIMER: 1,
            Primer3InputTag.PRIMER_PICK_RIGHT_PRIMER: 0,
            Primer3InputTag.PRIMER_PICK_INTERNAL_OLIGO: 0,
            Primer3InputTag.SEQUENCE_INCLUDED_REGION: f"1,{target.start - design_region.start}",
        }

    @property
    def requires_primer_amplicon_params(self) -> bool:
        return True

    @property
    def requires_probe_params(self) -> bool:
        return False

DesignPrimerPairsTask

Bases: Primer3Task

Stores task-specific Primer3 settings for designing primer pairs

Source code in prymer/primer3/primer3_task.py
class DesignPrimerPairsTask(Primer3Task, task_type=TaskType.PAIR):
    """Stores task-specific Primer3 settings for designing primer pairs"""

    @classmethod
    def _to_input_tags(cls, target: Span, design_region: Span) -> dict[Primer3InputTag, Any]:
        return {
            Primer3InputTag.PRIMER_TASK: "generic",
            Primer3InputTag.PRIMER_PICK_LEFT_PRIMER: 1,
            Primer3InputTag.PRIMER_PICK_RIGHT_PRIMER: 1,
            Primer3InputTag.PRIMER_PICK_INTERNAL_OLIGO: 0,
            Primer3InputTag.SEQUENCE_TARGET: f"{target.start - design_region.start + 1},"
            f"{target.length}",
        }

    @property
    def requires_primer_amplicon_params(self) -> bool:
        return True

    @property
    def requires_probe_params(self) -> bool:
        return False

DesignRightPrimersTask

Bases: Primer3Task

Stores task-specific characteristics for designing right primers

Source code in prymer/primer3/primer3_task.py
class DesignRightPrimersTask(Primer3Task, task_type=TaskType.RIGHT):
    """Stores task-specific characteristics for designing right primers"""

    @classmethod
    def _to_input_tags(cls, target: Span, design_region: Span) -> dict[Primer3InputTag, Any]:
        start = target.end - design_region.start + 1
        length = design_region.end - target.end
        return {
            Primer3InputTag.PRIMER_TASK: "pick_primer_list",
            Primer3InputTag.PRIMER_PICK_LEFT_PRIMER: 0,
            Primer3InputTag.PRIMER_PICK_RIGHT_PRIMER: 1,
            Primer3InputTag.PRIMER_PICK_INTERNAL_OLIGO: 0,
            Primer3InputTag.SEQUENCE_INCLUDED_REGION: f"{start},{length}",
        }

    @property
    def requires_primer_amplicon_params(self) -> bool:
        return True

    @property
    def requires_probe_params(self) -> bool:
        return False

PickHybProbeOnly

Bases: Primer3Task

Stores task-specific characteristics for designing an internal hybridization probe.

Source code in prymer/primer3/primer3_task.py
class PickHybProbeOnly(Primer3Task, task_type=TaskType.INTERNAL):
    """Stores task-specific characteristics for designing an internal hybridization probe."""

    @classmethod
    def _to_input_tags(cls, target: Span, design_region: Span) -> dict[Primer3InputTag, Any]:
        return {
            Primer3InputTag.PRIMER_TASK: "generic",
            Primer3InputTag.PRIMER_PICK_LEFT_PRIMER: 0,
            Primer3InputTag.PRIMER_PICK_RIGHT_PRIMER: 0,
            Primer3InputTag.PRIMER_PICK_INTERNAL_OLIGO: 1,
        }

    @property
    def requires_primer_amplicon_params(self) -> bool:
        return False

    @property
    def requires_probe_params(self) -> bool:
        return True

Primer3

Bases: ExecutableRunner

Enables interaction with command line tool, primer3.

Attributes:

Name Type Description
_fasta

file handle to the open reference genome file

_dict SequenceDictionary

the sequence dictionary that corresponds to the provided reference genome file

Source code in prymer/primer3/primer3.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
class Primer3(ExecutableRunner):
    """
    Enables interaction with command line tool, primer3.

    Attributes:
        _fasta: file handle to the open reference genome file
        _dict: the sequence dictionary that corresponds to the provided reference genome file
    """

    def __init__(
        self,
        genome_fasta: Path,
        executable: Optional[str] = None,
        variant_lookup: Optional[VariantLookup] = None,
    ) -> None:
        """
        Args:
            genome_fasta: Path to reference genome .fasta file
            executable: string representation of the path to primer3_core
            variant_lookup: VariantLookup object to facilitate hard-masking variants

        Assumes the sequence dictionary is located adjacent to the .fasta file and has the same
        base name with a .dict suffix.

        """
        executable_path = ExecutableRunner.validate_executable_path(
            executable="primer3_core" if executable is None else executable
        )
        command: list[str] = [f"{executable_path}"]

        self.variant_lookup = variant_lookup
        self._fasta = pysam.FastaFile(filename=f"{genome_fasta}")

        dict_path = genome_fasta.with_suffix(".dict")
        # TODO: This is a placeholder while waiting for #160  to be resolved
        # https://github.com/fulcrumgenomics/fgpyo/pull/160
        with reader(dict_path, file_type=sam.SamFileType.SAM) as fh:
            self._dict: SequenceDictionary = SequenceDictionary.from_sam(header=fh.header)

        super().__init__(command=command, stderr=subprocess.STDOUT)

    def close(self) -> bool:
        """Closes fasta file regardless of underlying subprocess status.
        Logs an error if the underlying subprocess is not successfully closed.

        Returns:
            True: if the subprocess was terminated successfully
            False: if the subprocess failed to terminate or was not already running
        """
        self._fasta.close()
        subprocess_close = super().close()
        if not subprocess_close:
            logging.getLogger(__name__).debug("Did not successfully close underlying subprocess")
        return subprocess_close

    def get_design_sequences(self, region: Span) -> tuple[str, str]:
        """Extracts the reference sequence that corresponds to the design region.

        Args:
            region: the region of the genome to be extracted

        Returns:
            A tuple of two sequences: the sequence for the region, and the sequence for the region
            with variants hard-masked as Ns

        """
        # pysam.fetch: 0-based, half-open intervals
        soft_masked = self._fasta.fetch(
            reference=region.refname, start=region.start - 1, end=region.end
        )

        if self.variant_lookup is None:
            hard_masked = soft_masked
            return soft_masked, hard_masked

        overlapping_variants: list[SimpleVariant] = self.variant_lookup.query(
            refname=region.refname, start=region.start, end=region.end
        )
        positions: list[int] = []
        for variant in overlapping_variants:
            # FIXME
            positions.extend(range(variant.pos, variant.end + 1))

        filtered_positions = [pos for pos in positions if region.start <= pos <= region.end]
        soft_masked_list = list(soft_masked)
        for pos in filtered_positions:
            soft_masked_list[region.get_offset(pos)] = (
                "N"  # get relative coord of filtered position and mask to N
            )
        # convert list back to string
        hard_masked = "".join(soft_masked_list)
        return soft_masked, hard_masked

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

        Args:
            design_input: the target region, design task, specifications, and scoring penalties
            designed_primer_pairs: the unfiltered primer pair designs emitted by Primer3

        Returns:
            valid_primer_pair_designs: primer pairs within specifications
            dinuc_pair_failures: single primer designs that failed the `max_dinuc_bases` threshold
        """
        valid_primer_pair_designs: list[PrimerPair] = []
        dinuc_pair_failures: list[Oligo] = []
        for primer_pair in designed_primer_pairs:
            valid: bool = True
            if (
                primer_pair.left_primer.longest_dinucleotide_run_length()
                > 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.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
            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
        """Designs primers, primer pairs, and/or internal probes given a target region.

        Args:
            design_input: encapsulates the target region, design task, specifications, and scoring
                penalties

        Returns:
            Primer3Result containing both the valid and failed designs emitted by Primer3

        Raises:
            RuntimeError: if underlying subprocess is not alive
            ValueError: if Primer3 returns errors or does not return output
            ValueError: if Primer3 output is malformed
            ValueError: if an unknown design task is given
        """

        if not self.is_alive:
            raise RuntimeError(
                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():
                if design_input.target.length < design_input.probe_params.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}"
                    )
                design_region = design_input.target
            case DesignRightPrimersTask() | DesignLeftPrimersTask() | DesignPrimerPairsTask():
                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,
                )
            case _ as unreachable:
                assert_never(unreachable)  # pragma: no cover

        soft_masked, hard_masked = self.get_design_sequences(design_region)
        # use 1-base coords, explain primer designs, use hard-masked sequence, and compute
        # thermodynamic attributes
        global_primer3_params = {
            Primer3InputTag.PRIMER_FIRST_BASE_INDEX: 1,
            Primer3InputTag.PRIMER_EXPLAIN_FLAG: 1,
            Primer3InputTag.SEQUENCE_TEMPLATE: hard_masked,
            Primer3InputTag.PRIMER_THERMODYNAMIC_OLIGO_ALIGNMENT: 1,
        }

        assembled_primer3_tags = {
            **global_primer3_params,
            **design_input.to_input_tags(design_region=design_region),
        }
        # Submit inputs to primer3
        for tag, value in assembled_primer3_tags.items():
            self._subprocess.stdin.write(f"{tag}={value}")
            self._subprocess.stdin.write("\n")
        self._subprocess.stdin.write("=\n")
        self._subprocess.stdin.flush()

        error_lines: list[str] = []  # list of errors as reported by primer3
        primer3_results: dict[str, str] = {}  # key-value pairs of results reported by Primer3

        def primer3_error(message: str) -> None:
            """Formats the Primer3 error and raises a ValueError."""
            error_message = f"{message}: "
            # add in any reported PRIMER_ERROR
            if "PRIMER_ERROR" in primer3_results:
                error_message += primer3_results["PRIMER_ERROR"]
            # add in any error lines
            if len(error_lines) > 0:
                error_message += "\n".join(f"\t\t{e}" for e in error_lines)
            # raise the exception now
            raise ValueError(error_message)

        while True:
            # Get the next line.  Since we want to distinguish between empty lines, which we ignore,
            # and the end-of-file, which is just an empty string, check for an empty string before
            # stripping the line of any trailing newline or carriage return characters.
            line: str = self._subprocess.stdout.readline()
            if line == "":  # EOF
                primer3_error("Primer3 exited prematurely")
            line = line.rstrip("\r\n")

            if line == "=":  # stop when we find the line just "="
                break
            elif line == "":  # ignore empty lines
                continue
            elif "=" not in line:  # error lines do not have the equals character in them, usually
                error_lines.append(line)
            else:  # parse and store the result
                key, value = line.split("=", maxsplit=1)
                # Because Primer3 will emit both the input given and the output generated, we
                # discard the input that is echo'ed back by looking for tags (keys)
                # that do not match any Primer3InputTag
                if not any(key == item.value for item in Primer3InputTag):
                    primer3_results[key] = value

        # Check for any errors.  Typically, these are in error_lines, but also the results can
        # contain the PRIMER_ERROR key.
        if "PRIMER_ERROR" in primer3_results or len(error_lines) > 0:
            primer3_error("Primer3 failed")

        match design_input.task:
            case DesignPrimerPairsTask():  # Primer pair design
                all_pair_results: list[PrimerPair] = Primer3._build_primer_pairs(
                    design_input=design_input,
                    design_results=primer3_results,
                    design_region=design_region,
                    unmasked_design_seq=soft_masked,
                )
                return Primer3._assemble_primer_pairs(
                    design_input=design_input,
                    design_results=primer3_results,
                    unfiltered_designs=all_pair_results,
                )

            case DesignLeftPrimersTask() | DesignRightPrimersTask() | PickHybProbeOnly():
                # Single primer or probe design
                all_single_results: list[Oligo] = Primer3._build_oligos(
                    design_input=design_input,
                    design_results=primer3_results,
                    design_region=design_region,
                    design_task=design_input.task,
                    unmasked_design_seq=soft_masked,
                )
                return Primer3._assemble_single_designs(
                    design_input=design_input,
                    design_results=primer3_results,
                    unfiltered_designs=all_single_results,
                )

            case _ as unreachable:
                assert_never(unreachable)

    @staticmethod
    def _build_oligos(
        design_input: Primer3Input,
        design_results: dict[str, str],
        design_region: Span,
        design_task: Union[DesignLeftPrimersTask, DesignRightPrimersTask, PickHybProbeOnly],
        unmasked_design_seq: str,
    ) -> list[Oligo]:
        """
        Builds a list of single oligos from Primer3 output.

        Args:
            design_input: the target region, design task, specifications, and scoring penalties
            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

        Returns:
            oligos: a list of unsorted and unfiltered primer designs emitted by Primer3

        Raises:
            ValueError: if Primer3 does not return primer designs
        """
        count: int = _check_design_results(design_input, design_results)

        primers: list[Oligo] = []
        for idx in range(count):
            key = f"PRIMER_{design_task.task_type}_{idx}"
            str_position, str_length = design_results[key].split(",", maxsplit=1)
            position, length = int(str_position), int(str_length)  # position is 1-based

            match design_task:
                case DesignLeftPrimersTask() | PickHybProbeOnly():
                    span = design_region.get_subspan(
                        offset=position - 1, subspan_length=length, strand=Strand.POSITIVE
                    )
                case DesignRightPrimersTask():
                    start = position - length + 1  # start is 1-based
                    span = design_region.get_subspan(
                        offset=start - 1, subspan_length=length, strand=Strand.NEGATIVE
                    )
                case _ as unreachable:
                    assert_never(unreachable)  # pragma: no cover

            slice_offset = design_region.get_offset(span.start)
            slice_end = design_region.get_offset(span.end) + 1

            # remake the primer sequence from the un-masked genome sequence just in case
            bases = unmasked_design_seq[slice_offset:slice_end]
            if span.strand == Strand.NEGATIVE:
                bases = reverse_complement(bases)
            # assemble Primer3-emitted results into `Oligo` objects
            # if thermodynamic melting temperatures are missing, raise KeyError
            try:
                primers.append(
                    Oligo(
                        bases=bases,
                        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,
                        tm_homodimer=float(
                            design_results[f"PRIMER_{design_task.task_type}_{idx}_SELF_ANY_TH"]
                        ),
                        tm_3p_anchored_homodimer=float(
                            design_results[f"PRIMER_{design_task.task_type}_{idx}_SELF_END_TH"],
                        ),
                        tm_secondary_structure=float(
                            design_results[f"PRIMER_{design_task.task_type}_{idx}_HAIRPIN_TH"]
                        ),
                    )
                )
            except KeyError as e:
                raise KeyError(
                    f"Did not find a required field in Primer3-emitted results: {e}"
                ) from e
        return primers

    @staticmethod
    def _assemble_single_designs(
        design_input: Primer3Input,
        design_results: dict[str, str],
        unfiltered_designs: list[Oligo],
    ) -> Primer3Result:
        """Screens oligo designs (primers or probes) emitted by Primer3 for acceptable dinucleotide
        runs and extracts failure reasons for failed designs."""

        valid_designs = [
            design
            for design in unfiltered_designs
            if _has_acceptable_dinuc_run(oligo_design=design, design_input=design_input)
        ]
        dinuc_failures = [
            design
            for design in unfiltered_designs
            if not _has_acceptable_dinuc_run(oligo_design=design, design_input=design_input)
        ]

        failure_strings = [design_results[f"PRIMER_{design_input.task.task_type}_EXPLAIN"]]
        failures = Primer3._build_failures(dinuc_failures, failure_strings)
        design_candidates: Primer3Result = Primer3Result(designs=valid_designs, failures=failures)
        return design_candidates

    @staticmethod
    def _build_primer_pairs(
        design_input: Primer3Input,
        design_results: dict[str, str],
        design_region: Span,
        unmasked_design_seq: str,
    ) -> list[PrimerPair]:
        """
        Builds a list of primer pairs from single primer designs emitted from Primer3.

        Args:
            design_input: the target region, design task, specifications, and scoring penalties
            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

        Returns:
            primer_pairs: a list of unsorted and unfiltered paired primer designs emitted by Primer3

        Raises:
            ValueError: if Primer3 does not return the same number of left and right designs
        """
        left_primers = Primer3._build_oligos(
            design_input=design_input,
            design_results=design_results,
            design_region=design_region,
            design_task=DesignLeftPrimersTask(),
            unmasked_design_seq=unmasked_design_seq,
        )

        right_primers = Primer3._build_oligos(
            design_input=design_input,
            design_results=design_results,
            design_region=design_region,
            design_task=DesignRightPrimersTask(),
            unmasked_design_seq=unmasked_design_seq,
        )

        def _build_primer_pair(num: int, primer_pair: tuple[Oligo, Oligo]) -> PrimerPair:
            """Builds the `PrimerPair` object from input left and right primers."""
            left_primer = primer_pair[0]
            right_primer = primer_pair[1]
            amplicon = replace(left_primer.span, end=right_primer.span.end)
            slice_offset = design_region.get_offset(amplicon.start)
            slice_end = slice_offset + amplicon.length

            return PrimerPair(
                left_primer=left_primer,
                right_primer=right_primer,
                amplicon_tm=float(design_results[f"PRIMER_PAIR_{num}_PRODUCT_TM"]),
                penalty=float(design_results[f"PRIMER_PAIR_{num}_PENALTY"]),
                amplicon_sequence=unmasked_design_seq[slice_offset:slice_end],
            )

        #  Primer3 returns an equal number of left and right primers during primer pair design
        if len(left_primers) != len(right_primers):
            raise ValueError("Primer3 returned a different number of left and right primers.")
        primer_pairs: list[PrimerPair] = [
            _build_primer_pair(num, primer_pair)
            for num, primer_pair in enumerate(zip(left_primers, right_primers, strict=True))
        ]
        return primer_pairs

    @staticmethod
    def _assemble_primer_pairs(
        design_input: Primer3Input,
        design_results: dict[str, str],
        unfiltered_designs: list[PrimerPair],
    ) -> Primer3Result:
        """Helper function to organize primer pairs into valid and failed designs.

        Wraps `Primer3._screen_pair_results()` and `Primer3._build_failures()` to filter out designs
        with dinucleotide runs that are too long and extract additional failure reasons emitted by
        Primer3.

        Args:
            design_input: encapsulates the target region, design task, specifications,
             and scoring penalties
            unfiltered_designs: list of primer pairs emitted from Primer3
             design_results: key-value pairs of results reported by Primer3

        Returns:
            primer_designs: a `Primer3Result` that encapsulates valid and failed designs
        """
        valid_primer_pair_designs: list[PrimerPair]
        dinuc_pair_failures: list[Oligo]
        valid_primer_pair_designs, dinuc_pair_failures = Primer3._screen_pair_results(
            design_input=design_input, designed_primer_pairs=unfiltered_designs
        )

        failure_strings = [
            design_results["PRIMER_PAIR_EXPLAIN"],
            design_results["PRIMER_LEFT_EXPLAIN"],
            design_results["PRIMER_RIGHT_EXPLAIN"],
        ]
        pair_failures = Primer3._build_failures(dinuc_pair_failures, failure_strings)
        primer_designs = Primer3Result(designs=valid_primer_pair_designs, failures=pair_failures)

        return primer_designs

    @staticmethod
    def _build_failures(
        dinuc_failures: list[Oligo],
        failure_strings: list[str],
    ) -> list[Primer3Failure]:
        """Extracts the reasons why designs that were considered by Primer3 failed
         (when there were failures).

        The set of failures is returned sorted from those with most
        failures to those with least.

        Args:
            dinuc_failures: primer designs with a dinucleotide run longer than the allowed maximum
            failure_strings: explanations (strings) emitted by Primer3 about failed designs


        Returns:
            a list of Primer3Failure objects
        """

        by_fail_count: Counter[Primer3FailureReason] = Primer3FailureReason.parse_failures(
            *failure_strings
        )
        # Count how many individual primers failed for dinuc runs
        num_dinuc_failures = len(set(dinuc_failures))
        if num_dinuc_failures > 0:
            by_fail_count[Primer3FailureReason.LONG_DINUC] = num_dinuc_failures
        return [Primer3Failure(reason, count) for reason, count in by_fail_count.most_common()]

    def _create_design_region(
        self,
        target_region: Span,
        max_amplicon_length: int,
        min_primer_length: int,
    ) -> Span:
        """
        Construct a design region surrounding the target region.

        The target region is padded on both sides by the maximum amplicon length, minus the length
        of the target region itself.

        If the target region cannot be padded by at least the minimum primer length on both sides,
        a `ValueError` is raised.

        Raises:
            ValueError: If the target region is too large to be padded.

        """
        # Pad the target region on both sides by the maximum amplicon length (minus the length of
        # the target). This ensures that the design region covers the complete window of potentially
        # valid primer pairs.
        padding: int = max_amplicon_length - target_region.length

        # Apply the padding, ensuring that we don't run out-of-bounds on the target contig.
        contig_length: int = self._dict[target_region.refname].length
        design_start: int = max(1, target_region.start - padding)
        design_end: int = min(target_region.end + padding, contig_length)

        # Validate that our design window includes sufficient space for a primer to be designed on
        # each side of the target region.
        left_design_window: int = target_region.start - design_start
        right_design_window: int = design_end - target_region.end
        if left_design_window < min_primer_length or right_design_window < min_primer_length:
            raise ValueError(
                f"Target region {target_region} exceeds the maximum size compatible with a "
                f"maximum amplicon length of {max_amplicon_length} and a minimum primer length of "
                f"{min_primer_length}. The maximum amplicon length should exceed the length of "
                "the target region by at least twice the minimum primer length."
            )

        # Return the validated design region.
        design_region: Span = replace(
            target_region,
            start=design_start,
            end=design_end,
        )

        return design_region

Functions

__init__
__init__(
    genome_fasta: Path,
    executable: Optional[str] = None,
    variant_lookup: Optional[VariantLookup] = None,
) -> None

Parameters:

Name Type Description Default
genome_fasta Path

Path to reference genome .fasta file

required
executable Optional[str]

string representation of the path to primer3_core

None
variant_lookup Optional[VariantLookup]

VariantLookup object to facilitate hard-masking variants

None

Assumes the sequence dictionary is located adjacent to the .fasta file and has the same base name with a .dict suffix.

Source code in prymer/primer3/primer3.py
def __init__(
    self,
    genome_fasta: Path,
    executable: Optional[str] = None,
    variant_lookup: Optional[VariantLookup] = None,
) -> None:
    """
    Args:
        genome_fasta: Path to reference genome .fasta file
        executable: string representation of the path to primer3_core
        variant_lookup: VariantLookup object to facilitate hard-masking variants

    Assumes the sequence dictionary is located adjacent to the .fasta file and has the same
    base name with a .dict suffix.

    """
    executable_path = ExecutableRunner.validate_executable_path(
        executable="primer3_core" if executable is None else executable
    )
    command: list[str] = [f"{executable_path}"]

    self.variant_lookup = variant_lookup
    self._fasta = pysam.FastaFile(filename=f"{genome_fasta}")

    dict_path = genome_fasta.with_suffix(".dict")
    # TODO: This is a placeholder while waiting for #160  to be resolved
    # https://github.com/fulcrumgenomics/fgpyo/pull/160
    with reader(dict_path, file_type=sam.SamFileType.SAM) as fh:
        self._dict: SequenceDictionary = SequenceDictionary.from_sam(header=fh.header)

    super().__init__(command=command, stderr=subprocess.STDOUT)
close
close() -> bool

Closes fasta file regardless of underlying subprocess status. Logs an error if the underlying subprocess is not successfully closed.

Returns:

Name Type Description
True bool

if the subprocess was terminated successfully

False bool

if the subprocess failed to terminate or was not already running

Source code in prymer/primer3/primer3.py
def close(self) -> bool:
    """Closes fasta file regardless of underlying subprocess status.
    Logs an error if the underlying subprocess is not successfully closed.

    Returns:
        True: if the subprocess was terminated successfully
        False: if the subprocess failed to terminate or was not already running
    """
    self._fasta.close()
    subprocess_close = super().close()
    if not subprocess_close:
        logging.getLogger(__name__).debug("Did not successfully close underlying subprocess")
    return subprocess_close
design
design(design_input: Primer3Input) -> Primer3Result

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

Parameters:

Name Type Description Default
design_input Primer3Input

encapsulates the target region, design task, specifications, and scoring penalties

required

Returns:

Type Description
Primer3Result

Primer3Result containing both the valid and failed designs emitted by Primer3

Raises:

Type Description
RuntimeError

if underlying subprocess is not alive

ValueError

if Primer3 returns errors or does not return output

ValueError

if Primer3 output is malformed

ValueError

if an unknown design task is given

Source code in prymer/primer3/primer3.py
def design(self, design_input: Primer3Input) -> Primer3Result:  # noqa: C901
    """Designs primers, primer pairs, and/or internal probes given a target region.

    Args:
        design_input: encapsulates the target region, design task, specifications, and scoring
            penalties

    Returns:
        Primer3Result containing both the valid and failed designs emitted by Primer3

    Raises:
        RuntimeError: if underlying subprocess is not alive
        ValueError: if Primer3 returns errors or does not return output
        ValueError: if Primer3 output is malformed
        ValueError: if an unknown design task is given
    """

    if not self.is_alive:
        raise RuntimeError(
            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():
            if design_input.target.length < design_input.probe_params.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}"
                )
            design_region = design_input.target
        case DesignRightPrimersTask() | DesignLeftPrimersTask() | DesignPrimerPairsTask():
            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,
            )
        case _ as unreachable:
            assert_never(unreachable)  # pragma: no cover

    soft_masked, hard_masked = self.get_design_sequences(design_region)
    # use 1-base coords, explain primer designs, use hard-masked sequence, and compute
    # thermodynamic attributes
    global_primer3_params = {
        Primer3InputTag.PRIMER_FIRST_BASE_INDEX: 1,
        Primer3InputTag.PRIMER_EXPLAIN_FLAG: 1,
        Primer3InputTag.SEQUENCE_TEMPLATE: hard_masked,
        Primer3InputTag.PRIMER_THERMODYNAMIC_OLIGO_ALIGNMENT: 1,
    }

    assembled_primer3_tags = {
        **global_primer3_params,
        **design_input.to_input_tags(design_region=design_region),
    }
    # Submit inputs to primer3
    for tag, value in assembled_primer3_tags.items():
        self._subprocess.stdin.write(f"{tag}={value}")
        self._subprocess.stdin.write("\n")
    self._subprocess.stdin.write("=\n")
    self._subprocess.stdin.flush()

    error_lines: list[str] = []  # list of errors as reported by primer3
    primer3_results: dict[str, str] = {}  # key-value pairs of results reported by Primer3

    def primer3_error(message: str) -> None:
        """Formats the Primer3 error and raises a ValueError."""
        error_message = f"{message}: "
        # add in any reported PRIMER_ERROR
        if "PRIMER_ERROR" in primer3_results:
            error_message += primer3_results["PRIMER_ERROR"]
        # add in any error lines
        if len(error_lines) > 0:
            error_message += "\n".join(f"\t\t{e}" for e in error_lines)
        # raise the exception now
        raise ValueError(error_message)

    while True:
        # Get the next line.  Since we want to distinguish between empty lines, which we ignore,
        # and the end-of-file, which is just an empty string, check for an empty string before
        # stripping the line of any trailing newline or carriage return characters.
        line: str = self._subprocess.stdout.readline()
        if line == "":  # EOF
            primer3_error("Primer3 exited prematurely")
        line = line.rstrip("\r\n")

        if line == "=":  # stop when we find the line just "="
            break
        elif line == "":  # ignore empty lines
            continue
        elif "=" not in line:  # error lines do not have the equals character in them, usually
            error_lines.append(line)
        else:  # parse and store the result
            key, value = line.split("=", maxsplit=1)
            # Because Primer3 will emit both the input given and the output generated, we
            # discard the input that is echo'ed back by looking for tags (keys)
            # that do not match any Primer3InputTag
            if not any(key == item.value for item in Primer3InputTag):
                primer3_results[key] = value

    # Check for any errors.  Typically, these are in error_lines, but also the results can
    # contain the PRIMER_ERROR key.
    if "PRIMER_ERROR" in primer3_results or len(error_lines) > 0:
        primer3_error("Primer3 failed")

    match design_input.task:
        case DesignPrimerPairsTask():  # Primer pair design
            all_pair_results: list[PrimerPair] = Primer3._build_primer_pairs(
                design_input=design_input,
                design_results=primer3_results,
                design_region=design_region,
                unmasked_design_seq=soft_masked,
            )
            return Primer3._assemble_primer_pairs(
                design_input=design_input,
                design_results=primer3_results,
                unfiltered_designs=all_pair_results,
            )

        case DesignLeftPrimersTask() | DesignRightPrimersTask() | PickHybProbeOnly():
            # Single primer or probe design
            all_single_results: list[Oligo] = Primer3._build_oligos(
                design_input=design_input,
                design_results=primer3_results,
                design_region=design_region,
                design_task=design_input.task,
                unmasked_design_seq=soft_masked,
            )
            return Primer3._assemble_single_designs(
                design_input=design_input,
                design_results=primer3_results,
                unfiltered_designs=all_single_results,
            )

        case _ as unreachable:
            assert_never(unreachable)
get_design_sequences
get_design_sequences(region: Span) -> tuple[str, str]

Extracts the reference sequence that corresponds to the design region.

Parameters:

Name Type Description Default
region Span

the region of the genome to be extracted

required

Returns:

Type Description
str

A tuple of two sequences: the sequence for the region, and the sequence for the region

str

with variants hard-masked as Ns

Source code in prymer/primer3/primer3.py
def get_design_sequences(self, region: Span) -> tuple[str, str]:
    """Extracts the reference sequence that corresponds to the design region.

    Args:
        region: the region of the genome to be extracted

    Returns:
        A tuple of two sequences: the sequence for the region, and the sequence for the region
        with variants hard-masked as Ns

    """
    # pysam.fetch: 0-based, half-open intervals
    soft_masked = self._fasta.fetch(
        reference=region.refname, start=region.start - 1, end=region.end
    )

    if self.variant_lookup is None:
        hard_masked = soft_masked
        return soft_masked, hard_masked

    overlapping_variants: list[SimpleVariant] = self.variant_lookup.query(
        refname=region.refname, start=region.start, end=region.end
    )
    positions: list[int] = []
    for variant in overlapping_variants:
        # FIXME
        positions.extend(range(variant.pos, variant.end + 1))

    filtered_positions = [pos for pos in positions if region.start <= pos <= region.end]
    soft_masked_list = list(soft_masked)
    for pos in filtered_positions:
        soft_masked_list[region.get_offset(pos)] = (
            "N"  # get relative coord of filtered position and mask to N
        )
    # convert list back to string
    hard_masked = "".join(soft_masked_list)
    return soft_masked, hard_masked

Primer3Failure dataclass

Bases: Metric['Primer3Failure']

Encapsulates how many designs failed for a given reason. Extends the fgpyo.util.metric.Metric class, which will facilitate writing out results for primer design QC etc.

Attributes:

Name Type Description
reason Primer3FailureReason

the reason the design failed

count int

how many designs failed

Source code in prymer/primer3/primer3.py
@dataclass(init=True, slots=True, frozen=True)
class Primer3Failure(Metric["Primer3Failure"]):
    """Encapsulates how many designs failed for a given reason.
    Extends the `fgpyo.util.metric.Metric` class, which will facilitate writing out results for
    primer design QC etc.

    Attributes:
        reason: the reason the design failed
        count: how many designs failed
    """

    reason: Primer3FailureReason
    count: int

Primer3FailureReason

Bases: StrEnum

Enum to represent the various reasons Primer3 removes primers and primer pairs.

https://github.com/bioinfo-ut/primer3_masker/blob/

6a40c4c408dc02b95ac02391457cda760092291a/src/libprimer3.c#L5581-L5605

This also contains custom failure values that are not generated by Primer3 but are convenient to have so that post-processing failures can be tracked in the same way that Primer3 failures are. These include LONG_DINUC, SECONDARY_STRUCTURE, and OFF_TARGET_AMPLIFICATION.

Source code in prymer/primer3/primer3_failure_reason.py
@unique
class Primer3FailureReason(StrEnum):
    """
    Enum to represent the various reasons Primer3 removes primers and primer pairs.

    These are taken from: https://github.com/bioinfo-ut/primer3_masker/blob/
      6a40c4c408dc02b95ac02391457cda760092291a/src/libprimer3.c#L5581-L5605

    This also contains custom failure values that are not generated by Primer3 but are convenient
    to have so that post-processing failures can be tracked in the same way that Primer3 failures
    are.  These include `LONG_DINUC`, `SECONDARY_STRUCTURE`, and `OFF_TARGET_AMPLIFICATION`.
    """

    # Failure reasons emitted by Primer3
    GC_CONTENT = "GC content failed"
    GC_CLAMP = "GC clamp failed"
    HAIRPIN_STABILITY = "high hairpin stability"
    HIGH_TM = "high tm"
    LOW_TM = "low tm"
    LOWERCASE_MASKING = "lowercase masking of 3' end"
    LONG_POLY_X = "long poly-x seq"
    PRODUCT_SIZE = "unacceptable product size"
    TOO_MANY_NS = "too many Ns"
    HIGH_ANY_COMPLEMENTARITY = "high any compl"
    HIGH_END_COMPLEMENTARITY = "high end compl"
    # Failure reasons not emitted by Primer3 and beneficial to keep track of
    LONG_DINUC = "long dinucleotide run"
    SECONDARY_STRUCTURE = "undesirable secondary structure"
    OFF_TARGET_AMPLIFICATION = "amplifies off-target regions"

    @classmethod
    def from_reason(cls, str_reason: str) -> Optional["Primer3FailureReason"]:
        """Returns the first `Primer3FailureReason` with the given reason for failure.
        If no failure exists, return `None`."""
        reason: Optional[Primer3FailureReason] = None
        try:
            reason = cls(str_reason)
        except ValueError:
            pass
        return reason

    @staticmethod
    def parse_failures(
        *failures: str,
    ) -> Counter["Primer3FailureReason"]:
        """When Primer3 encounters failures, extracts the reasons why designs that were considered
         by Primer3 failed.

        Args:
            failures: list of strings, with each string an "explanation" emitted by Primer3 about
                why the design failed.  Each string may be a comma delimited of failures, or a
                single failure.

        Returns:
            a `Counter` of each `Primer3FailureReason` reason.
        """

        failure_regex = r"^ ?(.+) ([0-9]+)$"
        by_fail_count: Counter[Primer3FailureReason] = Counter()
        # parse all failure strings and merge together counts for the same kinds of failures
        for failure in failures:
            split_failures = [fail.strip() for fail in failure.split(",")]
            for item in split_failures:
                result = re.match(failure_regex, item)
                if result is None:
                    continue
                reason = result.group(1)
                count = int(result.group(2))
                if reason in ["ok", "considered"]:
                    continue
                std_reason = Primer3FailureReason.from_reason(reason)
                if std_reason is None:
                    logging.getLogger(__name__).debug(f"Unknown Primer3 failure reason: {reason}")
                by_fail_count[std_reason] += count

        return by_fail_count

Functions

from_reason classmethod
from_reason(
    str_reason: str,
) -> Optional[Primer3FailureReason]

Returns the first Primer3FailureReason with the given reason for failure. If no failure exists, return None.

Source code in prymer/primer3/primer3_failure_reason.py
@classmethod
def from_reason(cls, str_reason: str) -> Optional["Primer3FailureReason"]:
    """Returns the first `Primer3FailureReason` with the given reason for failure.
    If no failure exists, return `None`."""
    reason: Optional[Primer3FailureReason] = None
    try:
        reason = cls(str_reason)
    except ValueError:
        pass
    return reason
parse_failures staticmethod
parse_failures(
    *failures: str,
) -> Counter[Primer3FailureReason]

When Primer3 encounters failures, extracts the reasons why designs that were considered by Primer3 failed.

Parameters:

Name Type Description Default
failures str

list of strings, with each string an "explanation" emitted by Primer3 about why the design failed. Each string may be a comma delimited of failures, or a single failure.

()

Returns:

Type Description
Counter[Primer3FailureReason]

a Counter of each Primer3FailureReason reason.

Source code in prymer/primer3/primer3_failure_reason.py
@staticmethod
def parse_failures(
    *failures: str,
) -> Counter["Primer3FailureReason"]:
    """When Primer3 encounters failures, extracts the reasons why designs that were considered
     by Primer3 failed.

    Args:
        failures: list of strings, with each string an "explanation" emitted by Primer3 about
            why the design failed.  Each string may be a comma delimited of failures, or a
            single failure.

    Returns:
        a `Counter` of each `Primer3FailureReason` reason.
    """

    failure_regex = r"^ ?(.+) ([0-9]+)$"
    by_fail_count: Counter[Primer3FailureReason] = Counter()
    # parse all failure strings and merge together counts for the same kinds of failures
    for failure in failures:
        split_failures = [fail.strip() for fail in failure.split(",")]
        for item in split_failures:
            result = re.match(failure_regex, item)
            if result is None:
                continue
            reason = result.group(1)
            count = int(result.group(2))
            if reason in ["ok", "considered"]:
                continue
            std_reason = Primer3FailureReason.from_reason(reason)
            if std_reason is None:
                logging.getLogger(__name__).debug(f"Unknown Primer3 failure reason: {reason}")
            by_fail_count[std_reason] += count

    return by_fail_count

Primer3Input dataclass

Assembles necessary inputs for Primer3 to orchestrate primer, primer pair, and/or internal probe design.

At least one set of design parameters (either PrimerAndAmpliconParameters or ProbeParameters) must be specified.

If PrimerAndAmpliconParameters is provided but PrimerAndAmpliconWeights is not provided, default PrimerAndAmpliconWeights will be used.

Similarly, if ProbeParameters is provided but ProbeWeights is not provided, default ProbeWeights will be used.

Please see primer3_parameters.py for details on the defaults.

Raises:

Type Description
ValueError

if neither the primer or probe parameters are specified

Source code in prymer/primer3/primer3_input.py
@dataclass(frozen=True, init=True, slots=True)
class Primer3Input:
    """Assembles necessary inputs for Primer3 to orchestrate primer, primer pair, and/or internal
    probe design.

    At least one set of design parameters (either `PrimerAndAmpliconParameters`
    or `ProbeParameters`) must be specified.

    If `PrimerAndAmpliconParameters` is provided but `PrimerAndAmpliconWeights` is not provided,
    default `PrimerAndAmpliconWeights` will be used.

    Similarly, if `ProbeParameters` is provided but `ProbeWeights` is not provided, default
    `ProbeWeights` will be used.

    Please see primer3_parameters.py for details on the defaults.


    Raises:
        ValueError: if neither the primer or  probe parameters are specified
    """

    target: Span
    task: Primer3TaskType
    primer_and_amplicon_params: Optional[PrimerAndAmpliconParameters] = None
    probe_params: Optional[ProbeParameters] = None
    primer_weights: Optional[PrimerAndAmpliconWeights] = None
    probe_weights: Optional[ProbeWeights] = None

    def __post_init__(self) -> None:
        # check for at least one set of params
        # for the set of params given, check that weights were given; use defaults if not given
        if self.primer_and_amplicon_params is None and self.probe_params is None:
            raise ValueError(
                "Primer3 requires at least one set of parameters"
                " for either primer or probe design"
            )

        if self.primer_and_amplicon_params is not None and self.primer_weights is None:
            object.__setattr__(self, "primer_weights", PrimerAndAmpliconWeights())

        if self.probe_params is not None and self.probe_weights is None:
            object.__setattr__(self, "probe_weights", ProbeWeights())

    def to_input_tags(self, design_region: Span) -> dict[Primer3InputTag, Any]:
        """Assembles `Primer3InputTag` and values for input to `Primer3`

        The target region must be wholly contained within design region.

        Args:
            design_region: the design region, which wholly contains the target region, in which
                    primers are to be designed.

        Returns:
            a mapping of `Primer3InputTag`s to associated value
        """
        primer3_task_params = self.task.to_input_tags(
            design_region=design_region, target=self.target
        )
        assembled_tags: dict[Primer3InputTag, Any] = {**primer3_task_params}

        optional_attributes = {
            field.name: getattr(self, field.name)
            for field in fields(self)
            if field.default is not MISSING
        }
        for settings in optional_attributes.values():
            if settings is not None:
                assembled_tags.update(settings.to_input_tags())

        return assembled_tags

Functions

to_input_tags
to_input_tags(
    design_region: Span,
) -> dict[Primer3InputTag, Any]

Assembles Primer3InputTag and values for input to Primer3

The target region must be wholly contained within design region.

Parameters:

Name Type Description Default
design_region Span

the design region, which wholly contains the target region, in which primers are to be designed.

required

Returns:

Type Description
dict[Primer3InputTag, Any]

a mapping of Primer3InputTags to associated value

Source code in prymer/primer3/primer3_input.py
def to_input_tags(self, design_region: Span) -> dict[Primer3InputTag, Any]:
    """Assembles `Primer3InputTag` and values for input to `Primer3`

    The target region must be wholly contained within design region.

    Args:
        design_region: the design region, which wholly contains the target region, in which
                primers are to be designed.

    Returns:
        a mapping of `Primer3InputTag`s to associated value
    """
    primer3_task_params = self.task.to_input_tags(
        design_region=design_region, target=self.target
    )
    assembled_tags: dict[Primer3InputTag, Any] = {**primer3_task_params}

    optional_attributes = {
        field.name: getattr(self, field.name)
        for field in fields(self)
        if field.default is not MISSING
    }
    for settings in optional_attributes.values():
        if settings is not None:
            assembled_tags.update(settings.to_input_tags())

    return assembled_tags

Primer3InputTag

Bases: UppercaseStrEnum

Enumeration of Primer3 input tags.

Please see the Primer3 manual for additional details

https://primer3.org/manual.html#commandLineTags

This class represents two categories of Primer3 input tags
  • SEQUENCE_ tags are those that control sequence-specific attributes of Primer3 jobs. These can be modified after each query submitted to Primer3.
  • PRIMER_ tags are those that describe more general parameters of Primer3 jobs. These attributes persist across queries to Primer3 unless they are explicitly reset. Errors in these "global" input tags are fatal.
Source code in prymer/primer3/primer3_input_tag.py
@unique
class Primer3InputTag(UppercaseStrEnum):
    """
    Enumeration of Primer3 input tags.

    Please see the Primer3 manual for additional details:
     https://primer3.org/manual.html#commandLineTags

    This class represents two categories of Primer3 input tags:
     * `SEQUENCE_` tags are those that control sequence-specific attributes of Primer3 jobs.
       These can be modified after each query submitted to Primer3.
     * `PRIMER_` tags are those that describe more general parameters of Primer3 jobs.
       These attributes persist across queries to Primer3 unless they are explicitly reset.
       Errors in these "global" input tags are fatal.
    """

    # Sequence input tags; query-specific
    SEQUENCE_EXCLUDED_REGION = auto()
    SEQUENCE_INCLUDED_REGION = auto()
    SEQUENCE_PRIMER_REVCOMP = auto()
    SEQUENCE_FORCE_LEFT_END = auto()
    SEQUENCE_INTERNAL_EXCLUDED_REGION = auto()
    SEQUENCE_QUALITY = auto()
    SEQUENCE_FORCE_LEFT_START = auto()
    SEQUENCE_INTERNAL_OLIGO = auto()
    SEQUENCE_START_CODON_POSITION = auto()
    SEQUENCE_FORCE_RIGHT_END = auto()
    SEQUENCE_OVERLAP_JUNCTION_LIST = auto()
    SEQUENCE_TARGET = auto()
    SEQUENCE_FORCE_RIGHT_START = auto()
    SEQUENCE_PRIMER = auto()
    SEQUENCE_TEMPLATE = auto()
    SEQUENCE_ID = auto()
    SEQUENCE_PRIMER_PAIR_OK_REGION_LIST = auto()

    # Global input tags; will persist across primer3 queries
    PRIMER_DNA_CONC = auto()
    PRIMER_MAX_END_GC = auto()
    PRIMER_PAIR_WT_PRODUCT_SIZE_LT = auto()
    PRIMER_DNTP_CONC = auto()
    PRIMER_MAX_END_STABILITY = auto()
    PRIMER_PAIR_WT_PRODUCT_TM_GT = auto()
    PRIMER_EXPLAIN_FLAG = auto()
    PRIMER_MAX_GC = auto()
    PRIMER_PAIR_WT_PRODUCT_TM_LT = auto()
    PRIMER_FIRST_BASE_INDEX = auto()
    PRIMER_MAX_HAIRPIN_TH = auto()
    PRIMER_PAIR_WT_PR_PENALTY = auto()
    PRIMER_GC_CLAMP = auto()
    PRIMER_MAX_LIBRARY_MISPRIMING = auto()
    PRIMER_PAIR_WT_TEMPLATE_MISPRIMING = auto()
    PRIMER_INSIDE_PENALTY = auto()
    PRIMER_MAX_NS_ACCEPTED = auto()
    PRIMER_PAIR_WT_TEMPLATE_MISPRIMING_TH = auto()
    PRIMER_INTERNAL_DNA_CONC = auto()
    PRIMER_MAX_POLY_X = auto()
    PRIMER_PICK_ANYWAY = auto()
    PRIMER_INTERNAL_DNTP_CONC = auto()
    PRIMER_MAX_SELF_ANY = auto()
    PRIMER_PICK_INTERNAL_OLIGO = auto()
    PRIMER_INTERNAL_MAX_GC = auto()
    PRIMER_MAX_SELF_ANY_TH = auto()
    PRIMER_PICK_LEFT_PRIMER = auto()
    PRIMER_INTERNAL_MAX_HAIRPIN_TH = auto()
    PRIMER_MAX_SELF_END = auto()
    PRIMER_PICK_RIGHT_PRIMER = auto()
    PRIMER_INTERNAL_MAX_LIBRARY_MISHYB = auto()
    PRIMER_MAX_SELF_END_TH = auto()
    PRIMER_PRODUCT_MAX_TM = auto()
    PRIMER_INTERNAL_MAX_NS_ACCEPTED = auto()
    PRIMER_MAX_SIZE = auto()
    PRIMER_PRODUCT_MIN_TM = auto()
    PRIMER_INTERNAL_MAX_POLY_X = auto()
    PRIMER_MAX_TEMPLATE_MISPRIMING = auto()
    PRIMER_PRODUCT_OPT_SIZE = auto()
    PRIMER_INTERNAL_MAX_SELF_ANY = auto()
    PRIMER_MAX_TEMPLATE_MISPRIMING_TH = auto()
    PRIMER_PRODUCT_OPT_TM = auto()
    PRIMER_INTERNAL_MAX_SELF_ANY_TH = auto()
    PRIMER_MAX_TM = auto()
    PRIMER_PRODUCT_SIZE_RANGE = auto()
    PRIMER_INTERNAL_MAX_SELF_END = auto()
    PRIMER_MIN_3_PRIME_OVERLAP_OF_JUNCTION = auto()
    PRIMER_QUALITY_RANGE_MAX = auto()
    PRIMER_INTERNAL_MAX_SELF_END_TH = auto()
    PRIMER_MIN_5_PRIME_OVERLAP_OF_JUNCTION = auto()
    PRIMER_QUALITY_RANGE_MIN = auto()
    PRIMER_INTERNAL_MAX_SIZE = auto()
    PRIMER_MIN_END_QUALITY = auto()
    PRIMER_SALT_CORRECTIONS = auto()
    PRIMER_INTERNAL_MAX_TM = auto()
    PRIMER_MIN_GC = auto()
    PRIMER_SALT_DIVALENT = auto()
    PRIMER_INTERNAL_MIN_GC = auto()
    PRIMER_MIN_LEFT_THREE_PRIME_DISTANCE = auto()
    PRIMER_SALT_MONOVALENT = auto()
    PRIMER_INTERNAL_MIN_QUALITY = auto()
    PRIMER_MIN_QUALITY = auto()
    PRIMER_SEQUENCING_ACCURACY = auto()
    PRIMER_INTERNAL_MIN_SIZE = auto()
    PRIMER_MIN_RIGHT_THREE_PRIME_DISTANCE = auto()
    PRIMER_SEQUENCING_INTERVAL = auto()
    PRIMER_INTERNAL_MIN_TM = auto()
    PRIMER_MIN_SIZE = auto()
    PRIMER_SEQUENCING_LEAD = auto()
    PRIMER_INTERNAL_MISHYB_LIBRARY = auto()
    PRIMER_MIN_THREE_PRIME_DISTANCE = auto()
    PRIMER_SEQUENCING_SPACING = auto()
    PRIMER_INTERNAL_MUST_MATCH_FIVE_PRIME = auto()
    PRIMER_MIN_TM = auto()
    PRIMER_TASK = auto()
    PRIMER_INTERNAL_MUST_MATCH_THREE_PRIME = auto()
    PRIMER_MISPRIMING_LIBRARY = auto()
    PRIMER_THERMODYNAMIC_OLIGO_ALIGNMENT = auto()
    PRIMER_INTERNAL_OPT_GC_PERCENT = auto()
    PRIMER_MUST_MATCH_FIVE_PRIME = auto()
    PRIMER_THERMODYNAMIC_PARAMETERS_PATH = auto()
    PRIMER_INTERNAL_OPT_SIZE = auto()
    PRIMER_MUST_MATCH_THREE_PRIME = auto()
    PRIMER_THERMODYNAMIC_TEMPLATE_ALIGNMENT = auto()
    PRIMER_INTERNAL_OPT_TM = auto()
    PRIMER_NUM_RETURN = auto()
    PRIMER_TM_FORMULA = auto()
    PRIMER_INTERNAL_SALT_DIVALENT = auto()
    PRIMER_OPT_GC_PERCENT = auto()
    PRIMER_WT_END_QUAL = auto()
    PRIMER_INTERNAL_SALT_MONOVALENT = auto()
    PRIMER_OPT_SIZE = auto()
    PRIMER_WT_END_STABILITY = auto()
    PRIMER_INTERNAL_WT_END_QUAL = auto()
    PRIMER_OPT_TM = auto()
    PRIMER_WT_GC_PERCENT_GT = auto()
    PRIMER_INTERNAL_WT_GC_PERCENT_GT = auto()
    PRIMER_OUTSIDE_PENALTY = auto()
    PRIMER_WT_GC_PERCENT_LT = auto()
    PRIMER_INTERNAL_WT_GC_PERCENT_LT = auto()
    PRIMER_PAIR_MAX_COMPL_ANY = auto()
    PRIMER_WT_HAIRPIN_TH = auto()
    PRIMER_INTERNAL_WT_HAIRPIN_TH = auto()
    PRIMER_PAIR_MAX_COMPL_ANY_TH = auto()
    PRIMER_WT_LIBRARY_MISPRIMING = auto()
    PRIMER_INTERNAL_WT_LIBRARY_MISHYB = auto()
    PRIMER_PAIR_MAX_COMPL_END = auto()
    PRIMER_WT_NUM_NS = auto()
    PRIMER_INTERNAL_WT_NUM_NS = auto()
    PRIMER_PAIR_MAX_COMPL_END_TH = auto()
    PRIMER_WT_POS_PENALTY = auto()
    PRIMER_INTERNAL_WT_SELF_ANY = auto()
    PRIMER_PAIR_MAX_DIFF_TM = auto()
    PRIMER_WT_SELF_ANY = auto()
    PRIMER_INTERNAL_WT_SELF_ANY_TH = auto()
    PRIMER_PAIR_MAX_LIBRARY_MISPRIMING = auto()
    PRIMER_WT_SELF_ANY_TH = auto()
    PRIMER_INTERNAL_WT_SELF_END = auto()
    PRIMER_PAIR_MAX_TEMPLATE_MISPRIMING = auto()
    PRIMER_WT_SELF_END = auto()
    PRIMER_INTERNAL_WT_SELF_END_TH = auto()
    PRIMER_PAIR_MAX_TEMPLATE_MISPRIMING_TH = auto()
    PRIMER_WT_SELF_END_TH = auto()
    PRIMER_INTERNAL_WT_SEQ_QUAL = auto()
    PRIMER_PAIR_WT_COMPL_ANY = auto()
    PRIMER_WT_SEQ_QUAL = auto()
    PRIMER_INTERNAL_WT_SIZE_GT = auto()
    PRIMER_PAIR_WT_COMPL_ANY_TH = auto()
    PRIMER_WT_SIZE_GT = auto()
    PRIMER_INTERNAL_WT_SIZE_LT = auto()
    PRIMER_PAIR_WT_COMPL_END = auto()
    PRIMER_WT_SIZE_LT = auto()
    PRIMER_INTERNAL_WT_TM_GT = auto()
    PRIMER_PAIR_WT_COMPL_END_TH = auto()
    PRIMER_WT_TEMPLATE_MISPRIMING = auto()
    PRIMER_INTERNAL_WT_TM_LT = auto()
    PRIMER_PAIR_WT_DIFF_TM = auto()
    PRIMER_WT_TEMPLATE_MISPRIMING_TH = auto()
    PRIMER_LIBERAL_BASE = auto()
    PRIMER_PAIR_WT_IO_PENALTY = auto()
    PRIMER_WT_TM_GT = auto()
    PRIMER_LIB_AMBIGUITY_CODES_CONSENSUS = auto()
    PRIMER_PAIR_WT_LIBRARY_MISPRIMING = auto()
    PRIMER_WT_TM_LT = auto()
    PRIMER_LOWERCASE_MASKING = auto()
    PRIMER_PAIR_WT_PRODUCT_SIZE_GT = auto()

Primer3Result dataclass

Bases: Generic[OligoLikeType]

Encapsulates Primer3 design results (both valid designs and failures).

Attributes:

Name Type Description
designs list[OligoLikeType]

filtered for out-of-spec characteristics and ordered (by objective function score) list of primer pairs or single oligos that were returned by Primer3

failures list[Primer3Failure]

ordered list of Primer3Failures detailing design failure reasons and corresponding count

Source code in prymer/primer3/primer3.py
@dataclass(init=True, slots=True, frozen=True)
class Primer3Result(Generic[OligoLikeType]):
    """Encapsulates Primer3 design results (both valid designs and failures).

    Attributes:
        designs: filtered for out-of-spec characteristics and ordered (by objective function score)
            list of primer pairs or single oligos that were returned by Primer3
        failures: ordered list of Primer3Failures detailing design failure reasons and corresponding
            count
    """

    designs: list[OligoLikeType]
    failures: list[Primer3Failure]

    def as_primer_result(self) -> "Primer3Result[Oligo]":
        """Returns this Primer3Result assuming the design results are of type `Primer`."""
        if len(self.designs) > 0 and not isinstance(self.designs[0], Oligo):
            raise ValueError("Cannot call `as_primer_result` on `PrimerPair` results")
        return typing.cast(Primer3Result[Oligo], self)

    def as_primer_pair_result(self) -> "Primer3Result[PrimerPair]":
        """Returns this Primer3Result assuming the design results are of type `PrimerPair`."""
        if len(self.designs) > 0 and not isinstance(self.designs[0], PrimerPair):
            raise ValueError("Cannot call `as_primer_pair_result` on `Oligo` results")
        return typing.cast(Primer3Result[PrimerPair], self)

    def primers(self) -> list[Oligo]:
        """Returns the design results as a list `Primer`s"""
        try:
            return self.as_primer_result().designs
        except ValueError as ex:
            raise ValueError("Cannot call `primers` on `PrimerPair` results") from ex

    def primer_pairs(self) -> list[PrimerPair]:
        """Returns the design results as a list `PrimerPair`s"""
        try:
            return self.as_primer_pair_result().designs
        except ValueError as ex:
            raise ValueError("Cannot call `primer_pairs` on `Oligo` results") from ex

Functions

as_primer_pair_result
as_primer_pair_result() -> Primer3Result[PrimerPair]

Returns this Primer3Result assuming the design results are of type PrimerPair.

Source code in prymer/primer3/primer3.py
def as_primer_pair_result(self) -> "Primer3Result[PrimerPair]":
    """Returns this Primer3Result assuming the design results are of type `PrimerPair`."""
    if len(self.designs) > 0 and not isinstance(self.designs[0], PrimerPair):
        raise ValueError("Cannot call `as_primer_pair_result` on `Oligo` results")
    return typing.cast(Primer3Result[PrimerPair], self)
as_primer_result
as_primer_result() -> Primer3Result[Oligo]

Returns this Primer3Result assuming the design results are of type Primer.

Source code in prymer/primer3/primer3.py
def as_primer_result(self) -> "Primer3Result[Oligo]":
    """Returns this Primer3Result assuming the design results are of type `Primer`."""
    if len(self.designs) > 0 and not isinstance(self.designs[0], Oligo):
        raise ValueError("Cannot call `as_primer_result` on `PrimerPair` results")
    return typing.cast(Primer3Result[Oligo], self)
primer_pairs
primer_pairs() -> list[PrimerPair]

Returns the design results as a list PrimerPairs

Source code in prymer/primer3/primer3.py
def primer_pairs(self) -> list[PrimerPair]:
    """Returns the design results as a list `PrimerPair`s"""
    try:
        return self.as_primer_pair_result().designs
    except ValueError as ex:
        raise ValueError("Cannot call `primer_pairs` on `Oligo` results") from ex
primers
primers() -> list[Oligo]

Returns the design results as a list Primers

Source code in prymer/primer3/primer3.py
def primers(self) -> list[Oligo]:
    """Returns the design results as a list `Primer`s"""
    try:
        return self.as_primer_result().designs
    except ValueError as ex:
        raise ValueError("Cannot call `primers` on `PrimerPair` results") from ex

PrimerAndAmpliconParameters dataclass

Holds common primer and amplicon design options that Primer3 uses to inform primer design.

Attributes:

Name Type Description
amplicon_sizes MinOptMax[int]

the min, optimal, and max amplicon size

amplicon_tms MinOptMax[float]

the min, optimal, and max amplicon melting temperatures

primer_sizes MinOptMax[int]

the min, optimal, and max primer size

primer_tms MinOptMax[float]

the min, optimal, and max primer melting temperatures

primer_gcs MinOptMax[float]

the min and maximal GC content for individual primers

gc_clamp tuple[int, int]

the min and max number of Gs and Cs in the 3' most N bases

primer_max_polyX int

the max homopolymer length acceptable within a primer

primer_max_Ns int

the max number of ambiguous bases acceptable within a primer

primer_max_dinuc_bases int

the maximal number of bases in a dinucleotide run in a primer

avoid_masked_bases bool

whether Primer3 should avoid designing primers in soft-masked regions

number_primers_return int

the number of primers to return

primer_max_homodimer_tm Optional[float]

the max melting temperature acceptable for self-complementarity

primer_max_3p_homodimer_tm Optional[float]

the max melting temperature acceptable for self-complementarity anchored at the 3' end

primer_max_hairpin_tm Optional[float]

the max melting temperature acceptable for secondary structure

Please see the Primer3 manual for additional details: https://primer3.org/manual.html#globalTags

Each of primer_max_homodimer_tm, primer_max_3p_homodimer_tm, and primer_max_hairpin_tm is optional. If these attributes are not provided, the default value will be set to 10 degrees lower than the minimal melting temperature specified for the primer. This matches the Primer3 manual.

If these values are provided, users should provide the absolute value of the melting temperature threshold (i.e. when provided, values should be specified independent of primer design.)

Source code in prymer/primer3/primer3_parameters.py
@dataclass(frozen=True, init=True, slots=True)
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
        amplicon_tms: the min, optimal, and max amplicon melting temperatures
        primer_sizes: the min, optimal, and max primer size
        primer_tms: the min, optimal, and max primer melting temperatures
        primer_gcs: the min and maximal GC content for individual primers
        gc_clamp: the min and max number of Gs and Cs in the 3' most N bases
        primer_max_polyX: the max homopolymer length acceptable within a primer
        primer_max_Ns: the max number of ambiguous bases acceptable within a primer
        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_homodimer_tm: the max melting temperature acceptable for self-complementarity
        primer_max_3p_homodimer_tm: the max melting temperature acceptable for self-complementarity
            anchored at the 3' end
        primer_max_hairpin_tm: the max melting temperature acceptable for secondary structure

    Please see the Primer3 manual for additional details: https://primer3.org/manual.html#globalTags

    Each of `primer_max_homodimer_tm`, `primer_max_3p_homodimer_tm`, and `primer_max_hairpin_tm` is
    optional. If these attributes are not provided, the default value will be set to 10 degrees
    lower than the minimal melting temperature specified for the primer. This matches the Primer3
    manual.

    If these values are provided, users should provide the absolute value of the
    melting temperature threshold (i.e. when provided, values should be specified independent
    of primer design.)
    """

    amplicon_sizes: MinOptMax[int]
    amplicon_tms: MinOptMax[float]
    primer_sizes: MinOptMax[int]
    primer_tms: MinOptMax[float]
    primer_gcs: MinOptMax[float]
    gc_clamp: tuple[int, int] = (0, 5)
    primer_max_polyX: int = 5
    primer_max_Ns: int = 1
    primer_max_dinuc_bases: int = 6
    avoid_masked_bases: bool = True
    number_primers_return: int = 5
    primer_max_homodimer_tm: Optional[float] = None
    primer_max_3p_homodimer_tm: Optional[float] = None
    primer_max_hairpin_tm: Optional[float] = None

    def __post_init__(self) -> None:
        if self.primer_max_dinuc_bases % 2 == 1:
            raise ValueError("Primer Max Dinuc Bases must be an even number of bases")
        if not isinstance(self.amplicon_sizes.min, int) or not isinstance(
            self.primer_sizes.min, int
        ):
            raise TypeError("Amplicon sizes and primer sizes must be integers")
        if self.gc_clamp[0] > self.gc_clamp[1]:
            raise ValueError("Min primer GC-clamp must be <= max primer GC-clamp")
        # if thermo attributes are not provided, default them to `self.primer_tms.min - 10.0`
        default_thermo_max: float = self.primer_tms.min - 10.0
        thermo_max_fields = [
            "primer_max_homodimer_tm",
            "primer_max_3p_homodimer_tm",
            "primer_max_hairpin_tm",
        ]
        for field in fields(self):
            if field.name in thermo_max_fields and getattr(self, field.name) is None:
                object.__setattr__(self, field.name, default_thermo_max)

    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_PRODUCT_OPT_SIZE: self.amplicon_sizes.opt,
            Primer3InputTag.PRIMER_PRODUCT_SIZE_RANGE: (
                f"{self.amplicon_sizes.min}-{self.amplicon_sizes.max}"
            ),
            Primer3InputTag.PRIMER_PRODUCT_MIN_TM: self.amplicon_tms.min,
            Primer3InputTag.PRIMER_PRODUCT_OPT_TM: self.amplicon_tms.opt,
            Primer3InputTag.PRIMER_PRODUCT_MAX_TM: self.amplicon_tms.max,
            Primer3InputTag.PRIMER_MIN_SIZE: self.primer_sizes.min,
            Primer3InputTag.PRIMER_OPT_SIZE: self.primer_sizes.opt,
            Primer3InputTag.PRIMER_MAX_SIZE: self.primer_sizes.max,
            Primer3InputTag.PRIMER_MIN_TM: self.primer_tms.min,
            Primer3InputTag.PRIMER_OPT_TM: self.primer_tms.opt,
            Primer3InputTag.PRIMER_MAX_TM: self.primer_tms.max,
            Primer3InputTag.PRIMER_MIN_GC: self.primer_gcs.min,
            Primer3InputTag.PRIMER_OPT_GC_PERCENT: self.primer_gcs.opt,
            Primer3InputTag.PRIMER_MAX_GC: self.primer_gcs.max,
            Primer3InputTag.PRIMER_GC_CLAMP: self.gc_clamp[0],
            Primer3InputTag.PRIMER_MAX_END_GC: self.gc_clamp[1],
            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_TH: self.primer_max_homodimer_tm,
            Primer3InputTag.PRIMER_MAX_SELF_END_TH: self.primer_max_3p_homodimer_tm,
            Primer3InputTag.PRIMER_MAX_HAIRPIN_TH: self.primer_max_hairpin_tm,
        }

        return mapped_dict

    @property
    def max_amplicon_length(self) -> int:
        """Max amplicon length"""
        return int(self.amplicon_sizes.max)

    @property
    def max_primer_length(self) -> int:
        """Max primer length"""
        return int(self.primer_sizes.max)

    @property
    def min_primer_length(self) -> int:
        """Minimum primer length."""
        return int(self.primer_sizes.min)

Attributes

max_amplicon_length property
max_amplicon_length: int

Max amplicon length

max_primer_length property
max_primer_length: int

Max primer length

min_primer_length property
min_primer_length: int

Minimum primer length.

Functions

to_input_tags
to_input_tags() -> dict[Primer3InputTag, Any]

Converts input params to Primer3InputTag to feed directly into Primer3.

Source code in prymer/primer3/primer3_parameters.py
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_PRODUCT_OPT_SIZE: self.amplicon_sizes.opt,
        Primer3InputTag.PRIMER_PRODUCT_SIZE_RANGE: (
            f"{self.amplicon_sizes.min}-{self.amplicon_sizes.max}"
        ),
        Primer3InputTag.PRIMER_PRODUCT_MIN_TM: self.amplicon_tms.min,
        Primer3InputTag.PRIMER_PRODUCT_OPT_TM: self.amplicon_tms.opt,
        Primer3InputTag.PRIMER_PRODUCT_MAX_TM: self.amplicon_tms.max,
        Primer3InputTag.PRIMER_MIN_SIZE: self.primer_sizes.min,
        Primer3InputTag.PRIMER_OPT_SIZE: self.primer_sizes.opt,
        Primer3InputTag.PRIMER_MAX_SIZE: self.primer_sizes.max,
        Primer3InputTag.PRIMER_MIN_TM: self.primer_tms.min,
        Primer3InputTag.PRIMER_OPT_TM: self.primer_tms.opt,
        Primer3InputTag.PRIMER_MAX_TM: self.primer_tms.max,
        Primer3InputTag.PRIMER_MIN_GC: self.primer_gcs.min,
        Primer3InputTag.PRIMER_OPT_GC_PERCENT: self.primer_gcs.opt,
        Primer3InputTag.PRIMER_MAX_GC: self.primer_gcs.max,
        Primer3InputTag.PRIMER_GC_CLAMP: self.gc_clamp[0],
        Primer3InputTag.PRIMER_MAX_END_GC: self.gc_clamp[1],
        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_TH: self.primer_max_homodimer_tm,
        Primer3InputTag.PRIMER_MAX_SELF_END_TH: self.primer_max_3p_homodimer_tm,
        Primer3InputTag.PRIMER_MAX_HAIRPIN_TH: self.primer_max_hairpin_tm,
    }

    return mapped_dict

PrimerAndAmpliconWeights dataclass

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.

Some of these settings depart from the default settings enumerated in the Primer3 manual. Please see the Primer3 manual for additional details: https://primer3.org/manual.html#globalTags

Attributes:

Name Type Description
product_size_lt float

weight for products shorter than PrimerAndAmpliconParameters.amplicon_sizes.opt

product_size_gt float

weight for products longer than PrimerAndAmpliconParameters.amplicon_sizes.opt

product_tm_lt float

weight for products with a Tm lower than PrimerAndAmpliconParameters.amplicon_tms.opt

product_tm_gt float

weight for products with a Tm greater than PrimerAndAmpliconParameters.amplicon_tms.opt

primer_end_stability float

penalty for the calculated maximum stability for the last five 3' bases of primer

primer_gc_lt float

penalty for primers with GC percent lower than PrimerAndAmpliconParameters.primer_gcs.opt

primer_gc_gt float

weight for primers with GC percent higher than PrimerAndAmpliconParameters.primer_gcs.opt

primer_homodimer_wt float

penalty for the individual primer self binding value as specified in PrimerAndAmpliconParameters.primer_max_homodimer_tm

primer_3p_homodimer_wt float

weight for the 3'-anchored primer self binding value as specified in PrimerAndAmpliconParameters.primer_max_3p_homodimer_tm

primer_secondary_structure_wt float

penalty weight for the primer hairpin structure melting temperature as defined in PrimerAndAmpliconParameters.PRIMER_MAX_HAIRPIN_TH

Source code in prymer/primer3/primer3_weights.py
@dataclass(frozen=True, init=True, slots=True)
class PrimerAndAmpliconWeights:
    """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.

    Some of these settings depart from the default settings enumerated in the Primer3 manual.
    Please see the Primer3 manual for additional details:
        https://primer3.org/manual.html#globalTags

    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: weight for primers with GC percent higher than
            `PrimerAndAmpliconParameters.primer_gcs.opt`
        primer_homodimer_wt: penalty for the individual primer self binding value as specified
            in `PrimerAndAmpliconParameters.primer_max_homodimer_tm`
        primer_3p_homodimer_wt: weight for the 3'-anchored primer self binding value as specified in
            `PrimerAndAmpliconParameters.primer_max_3p_homodimer_tm`
        primer_secondary_structure_wt: penalty weight for the primer hairpin structure melting
            temperature as defined in `PrimerAndAmpliconParameters.PRIMER_MAX_HAIRPIN_TH`

    """

    product_size_lt: float = 1.0
    product_size_gt: float = 1.0
    product_tm_lt: float = 0.0
    product_tm_gt: float = 0.0
    primer_end_stability: float = 0.25
    primer_gc_lt: float = 0.25
    primer_gc_gt: float = 0.25
    primer_self_any: float = 0.1
    primer_self_end: float = 0.1
    primer_size_lt: float = 0.5
    primer_size_gt: float = 0.1
    primer_tm_lt: float = 1.0
    primer_tm_gt: float = 1.0
    primer_homodimer_wt: float = 0.0
    primer_3p_homodimer_wt: float = 0.0
    primer_secondary_structure_wt: float = 0.0

    def to_input_tags(self) -> dict[Primer3InputTag, Any]:
        """Maps weights to Primer3InputTag to feed directly into Primer3."""
        mapped_dict = {
            Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_LT: self.product_size_lt,
            Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_GT: self.product_size_gt,
            Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_TM_LT: self.product_tm_lt,
            Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_TM_GT: self.product_tm_gt,
            Primer3InputTag.PRIMER_WT_END_STABILITY: self.primer_end_stability,
            Primer3InputTag.PRIMER_WT_GC_PERCENT_LT: self.primer_gc_lt,
            Primer3InputTag.PRIMER_WT_GC_PERCENT_GT: self.primer_gc_gt,
            Primer3InputTag.PRIMER_WT_SELF_ANY: self.primer_self_any,
            Primer3InputTag.PRIMER_WT_SELF_END: self.primer_self_end,
            Primer3InputTag.PRIMER_WT_SIZE_LT: self.primer_size_lt,
            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_homodimer_wt,
            Primer3InputTag.PRIMER_WT_SELF_END_TH: self.primer_3p_homodimer_wt,
            Primer3InputTag.PRIMER_WT_HAIRPIN_TH: self.primer_secondary_structure_wt,
        }
        return mapped_dict

Functions

to_input_tags
to_input_tags() -> dict[Primer3InputTag, Any]

Maps weights to Primer3InputTag to feed directly into Primer3.

Source code in prymer/primer3/primer3_weights.py
def to_input_tags(self) -> dict[Primer3InputTag, Any]:
    """Maps weights to Primer3InputTag to feed directly into Primer3."""
    mapped_dict = {
        Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_LT: self.product_size_lt,
        Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_GT: self.product_size_gt,
        Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_TM_LT: self.product_tm_lt,
        Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_TM_GT: self.product_tm_gt,
        Primer3InputTag.PRIMER_WT_END_STABILITY: self.primer_end_stability,
        Primer3InputTag.PRIMER_WT_GC_PERCENT_LT: self.primer_gc_lt,
        Primer3InputTag.PRIMER_WT_GC_PERCENT_GT: self.primer_gc_gt,
        Primer3InputTag.PRIMER_WT_SELF_ANY: self.primer_self_any,
        Primer3InputTag.PRIMER_WT_SELF_END: self.primer_self_end,
        Primer3InputTag.PRIMER_WT_SIZE_LT: self.primer_size_lt,
        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_homodimer_wt,
        Primer3InputTag.PRIMER_WT_SELF_END_TH: self.primer_3p_homodimer_wt,
        Primer3InputTag.PRIMER_WT_HAIRPIN_TH: self.primer_secondary_structure_wt,
    }
    return mapped_dict

ProbeParameters dataclass

Holds common primer design options that Primer3 uses to inform internal probe design.

Attributes:

Name Type Description
probe_sizes MinOptMax[int]

the min, optimal, and max probe size

probe_tms MinOptMax[float]

the min, optimal, and max probe melting temperatures

probe_gcs MinOptMax[float]

the min and max GC content for individual probes

number_probes_return int

the number of probes to return

probe_max_dinuc_bases int

the max number of bases in a dinucleotide run in a probe

probe_max_polyX int

the max homopolymer length acceptable within a probe

probe_max_Ns int

the max number of ambiguous bases acceptable within a probe

probe_max_homodimer_tm Optional[float]

the max melting temperature acceptable for self-complementarity

probe_max_3p_homodimer_tm Optional[float]

the max melting temperature acceptable for self-complementarity anchored at the 3' end

probe_max_hairpin_tm Optional[float]

the max melting temperature acceptable for secondary structure

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

While Primer3 supports alignment based and thermodynamic methods for simulating hybridizations, prymer enforces the use of the thermodynamic approach. This is slightly more computationally expensive but superior in their ability to limit problematic oligo self-complementarity (e.g., primer-dimers, nonspecific binding of probes)

If they are not provided, probe_max_self_any_thermo, probe_max_self_end_thermo, and probe_max_hairpin_thermo will be set to default values as specified in the Primer3 manual. The default value is 10 degrees lower than the minimal melting temperature specified for probe design (i.e. when not provided, values are specified relative to the probe design). If these values are provided, users should provide the absolute value of the melting temperature threshold (i.e. when provided, values should be specified as independent of probe design.)

Source code in prymer/primer3/primer3_parameters.py
@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 max GC content for individual probes
        number_probes_return: the number of probes to return
        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_homodimer_tm: the max melting temperature acceptable for self-complementarity
        probe_max_3p_homodimer_tm: the max melting temperature acceptable for self-complementarity
            anchored at the 3' end
        probe_max_hairpin_tm: the max melting temperature acceptable for secondary structure

    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

    While Primer3 supports alignment based and thermodynamic methods for simulating hybridizations,
    prymer enforces the use of the thermodynamic approach. This is slightly more computationally
    expensive but superior in their ability to limit problematic oligo self-complementarity
    (e.g., primer-dimers, nonspecific binding of probes)

    If they are not provided, `probe_max_self_any_thermo`, `probe_max_self_end_thermo`, and
    `probe_max_hairpin_thermo` will be set to default values as specified in the Primer3 manual.
    The default value is 10 degrees lower than the minimal melting temperature specified for
    probe design (i.e. when not provided, values are specified relative to the probe design).
    If these values are provided, users should provide the absolute value of the
    melting temperature threshold (i.e. when provided, values should be specified as independent
    of probe design.)

    """

    probe_sizes: MinOptMax[int]
    probe_tms: MinOptMax[float]
    probe_gcs: MinOptMax[float]
    number_probes_return: int = 5
    probe_max_dinuc_bases: int = 4
    probe_max_polyX: int = 5
    probe_max_Ns: int = 0
    probe_max_homodimer_tm: Optional[float] = None
    probe_max_3p_homodimer_tm: Optional[float] = None
    probe_max_hairpin_tm: Optional[float] = None

    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_max_dinuc_bases % 2 == 1:
            raise ValueError("Max threshold for dinucleotide bases must be an even number of bases")
        # if thermo attributes are not provided, default them to `self.probe_tms.min - 10.0`
        default_thermo_max: float = self.probe_tms.min - 10.0
        thermo_max_fields = [
            "probe_max_homodimer_tm",
            "probe_max_3p_homodimer_tm",
            "probe_max_hairpin_tm",
        ]
        for field in fields(self):
            if field.name in thermo_max_fields and getattr(self, field.name) is None:
                object.__setattr__(self, field.name, default_thermo_max)

    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_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_TH: self.probe_max_homodimer_tm,
            Primer3InputTag.PRIMER_INTERNAL_MAX_SELF_END_TH: self.probe_max_3p_homodimer_tm,
            Primer3InputTag.PRIMER_INTERNAL_MAX_HAIRPIN_TH: self.probe_max_hairpin_tm,
            Primer3InputTag.PRIMER_NUM_RETURN: self.number_probes_return,
        }

        return mapped_dict

Functions

to_input_tags
to_input_tags() -> dict[Primer3InputTag, Any]

Converts input params to Primer3InputTag to feed directly into Primer3.

Source code in prymer/primer3/primer3_parameters.py
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_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_TH: self.probe_max_homodimer_tm,
        Primer3InputTag.PRIMER_INTERNAL_MAX_SELF_END_TH: self.probe_max_3p_homodimer_tm,
        Primer3InputTag.PRIMER_INTERNAL_MAX_HAIRPIN_TH: self.probe_max_hairpin_tm,
        Primer3InputTag.PRIMER_NUM_RETURN: self.number_probes_return,
    }

    return mapped_dict

ProbeWeights dataclass

Holds the probe-specific weights that Primer3 uses to adjust design penalties.

Attributes:

Name Type Description
probe_size_lt float

penalty for probes shorter than ProbeParameters.probe_sizes.opt

probe_size_gt float

penalty for probes longer than ProbeParameters.probe_sizes.opt

probe_tm_lt float

penalty for probes with a Tm lower than ProbeParameters.probe_tms.opt

probe_tm_gt float

penalty for probes with a Tm greater than ProbeParameters.probe_tms.opt

probe_gc_lt float

penalty for probes with GC content lower than ProbeParameters.probe_gcs.opt

probe_gc_gt float

penalty for probes with GC content greater than ProbeParameters.probe_gcs.opt

probe_wt_self_any_th float

penalty for probe self-complementarity as defined in ProbeParameters.probe_max_self_any_thermo

probe_wt_self_end float

penalty for probe 3' complementarity as defined in ProbeParameters.probe_max_self_end_thermo

probe_wt_hairpin_th float

penalty for the most stable primer hairpin structure value as defined in ProbeParameters.probe_max_hairpin_thermo

Each of these defaults are taken from the Primer3 manual. More details can be found here: https://primer3.org/manual.html

Source code in prymer/primer3/primer3_weights.py
@dataclass(frozen=True, init=True, slots=True)
class ProbeWeights:
    """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_th: penalty for probe self-complementarity as defined in
            `ProbeParameters.probe_max_self_any_thermo`
        probe_wt_self_end: penalty for probe 3' complementarity as defined in
            `ProbeParameters.probe_max_self_end_thermo`
        probe_wt_hairpin_th: penalty for the most stable primer hairpin structure value as defined
            in `ProbeParameters.probe_max_hairpin_thermo`

    Each of these defaults are taken from the Primer3 manual. More details can be found here:
    https://primer3.org/manual.html

    """

    probe_size_lt: float = 1.0
    probe_size_gt: float = 1.0
    probe_tm_lt: float = 1.0
    probe_tm_gt: float = 1.0
    probe_gc_lt: float = 0.0
    probe_gc_gt: float = 0.0
    probe_homodimer_wt: float = 0.0
    probe_3p_homodimer_wt: float = 0.0
    probe_secondary_structure_wt: float = 0.0

    def to_input_tags(self) -> dict[Primer3InputTag, Any]:
        """Maps weights to Primer3InputTag to feed directly into Primer3."""
        mapped_dict = {
            Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_LT: self.probe_size_lt,
            Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_GT: self.probe_size_gt,
            Primer3InputTag.PRIMER_INTERNAL_WT_TM_LT: self.probe_tm_lt,
            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_TH: self.probe_homodimer_wt,
            Primer3InputTag.PRIMER_INTERNAL_WT_SELF_END_TH: self.probe_3p_homodimer_wt,
            Primer3InputTag.PRIMER_INTERNAL_WT_HAIRPIN_TH: self.probe_secondary_structure_wt,
        }
        return mapped_dict

Functions

to_input_tags
to_input_tags() -> dict[Primer3InputTag, Any]

Maps weights to Primer3InputTag to feed directly into Primer3.

Source code in prymer/primer3/primer3_weights.py
def to_input_tags(self) -> dict[Primer3InputTag, Any]:
    """Maps weights to Primer3InputTag to feed directly into Primer3."""
    mapped_dict = {
        Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_LT: self.probe_size_lt,
        Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_GT: self.probe_size_gt,
        Primer3InputTag.PRIMER_INTERNAL_WT_TM_LT: self.probe_tm_lt,
        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_TH: self.probe_homodimer_wt,
        Primer3InputTag.PRIMER_INTERNAL_WT_SELF_END_TH: self.probe_3p_homodimer_wt,
        Primer3InputTag.PRIMER_INTERNAL_WT_HAIRPIN_TH: self.probe_secondary_structure_wt,
    }
    return mapped_dict

Modules

primer3

Primer3 Class and Methods

This module contains the Primer3 class, a class to facilitate exchange of input and output data with Primer3, a command line tool.

Similar to the NtThermoAlign and BwaAlnInteractive classes in the prymer library, the Primer3 class extends the ExecutableRunner base class to initiate an underlying subprocess, read and write input and output data, and gracefully terminate any remaining subprocesses.

Examples

The genome FASTA must be provided to the Primer3 constructor, such that design and target nucleotide sequences can be retrieved. The full path to the primer3 executable can provided, otherwise it is assumed to be on the PATH. Furthermore, optionally a VariantLookup may be provided to hard-mask the design and target regions as to avoid design primers over polymorphic sites.

>>> from pathlib import Path
>>> from prymer.api.variant_lookup import VariantLookup, VariantOverlapDetector
>>> genome_fasta = Path("./tests/primer3/data/miniref.fa")
>>> genome_vcf = Path("./tests/primer3/data/miniref.variants.vcf.gz")
>>> variant_lookup: VariantLookup = VariantOverlapDetector(vcf_paths=[genome_vcf], min_maf=0.01, include_missing_mafs=False)
>>> designer = Primer3(genome_fasta=genome_fasta, variant_lookup=variant_lookup)

The get_design_sequences() method on Primer3 is used to retrieve the soft and hard masked sequences for a given region. The hard-masked sequence replaces bases with Ns that overlap polymorphic sites found in the VariantLookup provided in the constructor.

>>> design_region = Span(refname="chr2", start=9095, end=9120)
>>> soft_masked, hard_masked = designer.get_design_sequences(region=design_region)
>>> soft_masked
'AGTTACATTACAAAAGGCAGATTTCA'
>>> hard_masked
'AGTTANNNTACAAAAGGCAGATTTCA'

The design() method on Primer3 is used to design the primers given a Primer3Input. The latter includes all the parameters and target region.

>>> from prymer.primer3.primer3_parameters import PrimerAndAmpliconParameters
>>> from prymer.api import MinOptMax
>>> target = Span(refname="chr1", start=201, end=250, strand=Strand.POSITIVE)
>>> 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),     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(     target=target,     primer_and_amplicon_params=params,     task=DesignLeftPrimersTask(), )
>>> left_result = designer.design(design_input=design_input)

The left_result returns the Primer3Result container class. It contains two attributes: 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 detailing design failure reasons and corresponding count.

In this case, there are two failures reasons:

>>> for failure in left_result.failures:     print(failure)
Primer3Failure(reason=<Primer3FailureReason.HIGH_TM: 'high tm'>, count=406)
Primer3Failure(reason=<Primer3FailureReason.GC_CONTENT: 'GC content failed'>, count=91)

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 or PrimerPair).

>>> for primer in left_result.primers():     print(primer)
TCTGAACAGGACGAACTGGATTTCCTCAT   65.686  1.953897        chr1:163-191:+
CTCTGAACAGGACGAACTGGATTTCCTCAT  66.152  2.293213        chr1:162-191:+
TCTGAACAGGACGAACTGGATTTCCTCATG  66.33   2.514048        chr1:163-192:+
AACAGGACGAACTGGATTTCCTCATGGAA   66.099  2.524986        chr1:167-195:+
CTGAACAGGACGAACTGGATTTCCTCATG   65.47   2.556859        chr1:164-192:+

Finally, the designer should be closed to terminate the sub-process:

>>> designer.close()
True

Primer3 is also context a manager, and so can be used with a with clause:

>>> with Primer3(genome_fasta=genome_fasta) as designer:     pass  # use designer here!

Attributes

OligoLikeType module-attribute
OligoLikeType = TypeVar('OligoLikeType', bound=OligoLike)

Type variable for a Primer3Result, which must implement OligoLike

Classes

Primer3

Bases: ExecutableRunner

Enables interaction with command line tool, primer3.

Attributes:

Name Type Description
_fasta

file handle to the open reference genome file

_dict SequenceDictionary

the sequence dictionary that corresponds to the provided reference genome file

Source code in prymer/primer3/primer3.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
class Primer3(ExecutableRunner):
    """
    Enables interaction with command line tool, primer3.

    Attributes:
        _fasta: file handle to the open reference genome file
        _dict: the sequence dictionary that corresponds to the provided reference genome file
    """

    def __init__(
        self,
        genome_fasta: Path,
        executable: Optional[str] = None,
        variant_lookup: Optional[VariantLookup] = None,
    ) -> None:
        """
        Args:
            genome_fasta: Path to reference genome .fasta file
            executable: string representation of the path to primer3_core
            variant_lookup: VariantLookup object to facilitate hard-masking variants

        Assumes the sequence dictionary is located adjacent to the .fasta file and has the same
        base name with a .dict suffix.

        """
        executable_path = ExecutableRunner.validate_executable_path(
            executable="primer3_core" if executable is None else executable
        )
        command: list[str] = [f"{executable_path}"]

        self.variant_lookup = variant_lookup
        self._fasta = pysam.FastaFile(filename=f"{genome_fasta}")

        dict_path = genome_fasta.with_suffix(".dict")
        # TODO: This is a placeholder while waiting for #160  to be resolved
        # https://github.com/fulcrumgenomics/fgpyo/pull/160
        with reader(dict_path, file_type=sam.SamFileType.SAM) as fh:
            self._dict: SequenceDictionary = SequenceDictionary.from_sam(header=fh.header)

        super().__init__(command=command, stderr=subprocess.STDOUT)

    def close(self) -> bool:
        """Closes fasta file regardless of underlying subprocess status.
        Logs an error if the underlying subprocess is not successfully closed.

        Returns:
            True: if the subprocess was terminated successfully
            False: if the subprocess failed to terminate or was not already running
        """
        self._fasta.close()
        subprocess_close = super().close()
        if not subprocess_close:
            logging.getLogger(__name__).debug("Did not successfully close underlying subprocess")
        return subprocess_close

    def get_design_sequences(self, region: Span) -> tuple[str, str]:
        """Extracts the reference sequence that corresponds to the design region.

        Args:
            region: the region of the genome to be extracted

        Returns:
            A tuple of two sequences: the sequence for the region, and the sequence for the region
            with variants hard-masked as Ns

        """
        # pysam.fetch: 0-based, half-open intervals
        soft_masked = self._fasta.fetch(
            reference=region.refname, start=region.start - 1, end=region.end
        )

        if self.variant_lookup is None:
            hard_masked = soft_masked
            return soft_masked, hard_masked

        overlapping_variants: list[SimpleVariant] = self.variant_lookup.query(
            refname=region.refname, start=region.start, end=region.end
        )
        positions: list[int] = []
        for variant in overlapping_variants:
            # FIXME
            positions.extend(range(variant.pos, variant.end + 1))

        filtered_positions = [pos for pos in positions if region.start <= pos <= region.end]
        soft_masked_list = list(soft_masked)
        for pos in filtered_positions:
            soft_masked_list[region.get_offset(pos)] = (
                "N"  # get relative coord of filtered position and mask to N
            )
        # convert list back to string
        hard_masked = "".join(soft_masked_list)
        return soft_masked, hard_masked

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

        Args:
            design_input: the target region, design task, specifications, and scoring penalties
            designed_primer_pairs: the unfiltered primer pair designs emitted by Primer3

        Returns:
            valid_primer_pair_designs: primer pairs within specifications
            dinuc_pair_failures: single primer designs that failed the `max_dinuc_bases` threshold
        """
        valid_primer_pair_designs: list[PrimerPair] = []
        dinuc_pair_failures: list[Oligo] = []
        for primer_pair in designed_primer_pairs:
            valid: bool = True
            if (
                primer_pair.left_primer.longest_dinucleotide_run_length()
                > 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.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
            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
        """Designs primers, primer pairs, and/or internal probes given a target region.

        Args:
            design_input: encapsulates the target region, design task, specifications, and scoring
                penalties

        Returns:
            Primer3Result containing both the valid and failed designs emitted by Primer3

        Raises:
            RuntimeError: if underlying subprocess is not alive
            ValueError: if Primer3 returns errors or does not return output
            ValueError: if Primer3 output is malformed
            ValueError: if an unknown design task is given
        """

        if not self.is_alive:
            raise RuntimeError(
                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():
                if design_input.target.length < design_input.probe_params.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}"
                    )
                design_region = design_input.target
            case DesignRightPrimersTask() | DesignLeftPrimersTask() | DesignPrimerPairsTask():
                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,
                )
            case _ as unreachable:
                assert_never(unreachable)  # pragma: no cover

        soft_masked, hard_masked = self.get_design_sequences(design_region)
        # use 1-base coords, explain primer designs, use hard-masked sequence, and compute
        # thermodynamic attributes
        global_primer3_params = {
            Primer3InputTag.PRIMER_FIRST_BASE_INDEX: 1,
            Primer3InputTag.PRIMER_EXPLAIN_FLAG: 1,
            Primer3InputTag.SEQUENCE_TEMPLATE: hard_masked,
            Primer3InputTag.PRIMER_THERMODYNAMIC_OLIGO_ALIGNMENT: 1,
        }

        assembled_primer3_tags = {
            **global_primer3_params,
            **design_input.to_input_tags(design_region=design_region),
        }
        # Submit inputs to primer3
        for tag, value in assembled_primer3_tags.items():
            self._subprocess.stdin.write(f"{tag}={value}")
            self._subprocess.stdin.write("\n")
        self._subprocess.stdin.write("=\n")
        self._subprocess.stdin.flush()

        error_lines: list[str] = []  # list of errors as reported by primer3
        primer3_results: dict[str, str] = {}  # key-value pairs of results reported by Primer3

        def primer3_error(message: str) -> None:
            """Formats the Primer3 error and raises a ValueError."""
            error_message = f"{message}: "
            # add in any reported PRIMER_ERROR
            if "PRIMER_ERROR" in primer3_results:
                error_message += primer3_results["PRIMER_ERROR"]
            # add in any error lines
            if len(error_lines) > 0:
                error_message += "\n".join(f"\t\t{e}" for e in error_lines)
            # raise the exception now
            raise ValueError(error_message)

        while True:
            # Get the next line.  Since we want to distinguish between empty lines, which we ignore,
            # and the end-of-file, which is just an empty string, check for an empty string before
            # stripping the line of any trailing newline or carriage return characters.
            line: str = self._subprocess.stdout.readline()
            if line == "":  # EOF
                primer3_error("Primer3 exited prematurely")
            line = line.rstrip("\r\n")

            if line == "=":  # stop when we find the line just "="
                break
            elif line == "":  # ignore empty lines
                continue
            elif "=" not in line:  # error lines do not have the equals character in them, usually
                error_lines.append(line)
            else:  # parse and store the result
                key, value = line.split("=", maxsplit=1)
                # Because Primer3 will emit both the input given and the output generated, we
                # discard the input that is echo'ed back by looking for tags (keys)
                # that do not match any Primer3InputTag
                if not any(key == item.value for item in Primer3InputTag):
                    primer3_results[key] = value

        # Check for any errors.  Typically, these are in error_lines, but also the results can
        # contain the PRIMER_ERROR key.
        if "PRIMER_ERROR" in primer3_results or len(error_lines) > 0:
            primer3_error("Primer3 failed")

        match design_input.task:
            case DesignPrimerPairsTask():  # Primer pair design
                all_pair_results: list[PrimerPair] = Primer3._build_primer_pairs(
                    design_input=design_input,
                    design_results=primer3_results,
                    design_region=design_region,
                    unmasked_design_seq=soft_masked,
                )
                return Primer3._assemble_primer_pairs(
                    design_input=design_input,
                    design_results=primer3_results,
                    unfiltered_designs=all_pair_results,
                )

            case DesignLeftPrimersTask() | DesignRightPrimersTask() | PickHybProbeOnly():
                # Single primer or probe design
                all_single_results: list[Oligo] = Primer3._build_oligos(
                    design_input=design_input,
                    design_results=primer3_results,
                    design_region=design_region,
                    design_task=design_input.task,
                    unmasked_design_seq=soft_masked,
                )
                return Primer3._assemble_single_designs(
                    design_input=design_input,
                    design_results=primer3_results,
                    unfiltered_designs=all_single_results,
                )

            case _ as unreachable:
                assert_never(unreachable)

    @staticmethod
    def _build_oligos(
        design_input: Primer3Input,
        design_results: dict[str, str],
        design_region: Span,
        design_task: Union[DesignLeftPrimersTask, DesignRightPrimersTask, PickHybProbeOnly],
        unmasked_design_seq: str,
    ) -> list[Oligo]:
        """
        Builds a list of single oligos from Primer3 output.

        Args:
            design_input: the target region, design task, specifications, and scoring penalties
            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

        Returns:
            oligos: a list of unsorted and unfiltered primer designs emitted by Primer3

        Raises:
            ValueError: if Primer3 does not return primer designs
        """
        count: int = _check_design_results(design_input, design_results)

        primers: list[Oligo] = []
        for idx in range(count):
            key = f"PRIMER_{design_task.task_type}_{idx}"
            str_position, str_length = design_results[key].split(",", maxsplit=1)
            position, length = int(str_position), int(str_length)  # position is 1-based

            match design_task:
                case DesignLeftPrimersTask() | PickHybProbeOnly():
                    span = design_region.get_subspan(
                        offset=position - 1, subspan_length=length, strand=Strand.POSITIVE
                    )
                case DesignRightPrimersTask():
                    start = position - length + 1  # start is 1-based
                    span = design_region.get_subspan(
                        offset=start - 1, subspan_length=length, strand=Strand.NEGATIVE
                    )
                case _ as unreachable:
                    assert_never(unreachable)  # pragma: no cover

            slice_offset = design_region.get_offset(span.start)
            slice_end = design_region.get_offset(span.end) + 1

            # remake the primer sequence from the un-masked genome sequence just in case
            bases = unmasked_design_seq[slice_offset:slice_end]
            if span.strand == Strand.NEGATIVE:
                bases = reverse_complement(bases)
            # assemble Primer3-emitted results into `Oligo` objects
            # if thermodynamic melting temperatures are missing, raise KeyError
            try:
                primers.append(
                    Oligo(
                        bases=bases,
                        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,
                        tm_homodimer=float(
                            design_results[f"PRIMER_{design_task.task_type}_{idx}_SELF_ANY_TH"]
                        ),
                        tm_3p_anchored_homodimer=float(
                            design_results[f"PRIMER_{design_task.task_type}_{idx}_SELF_END_TH"],
                        ),
                        tm_secondary_structure=float(
                            design_results[f"PRIMER_{design_task.task_type}_{idx}_HAIRPIN_TH"]
                        ),
                    )
                )
            except KeyError as e:
                raise KeyError(
                    f"Did not find a required field in Primer3-emitted results: {e}"
                ) from e
        return primers

    @staticmethod
    def _assemble_single_designs(
        design_input: Primer3Input,
        design_results: dict[str, str],
        unfiltered_designs: list[Oligo],
    ) -> Primer3Result:
        """Screens oligo designs (primers or probes) emitted by Primer3 for acceptable dinucleotide
        runs and extracts failure reasons for failed designs."""

        valid_designs = [
            design
            for design in unfiltered_designs
            if _has_acceptable_dinuc_run(oligo_design=design, design_input=design_input)
        ]
        dinuc_failures = [
            design
            for design in unfiltered_designs
            if not _has_acceptable_dinuc_run(oligo_design=design, design_input=design_input)
        ]

        failure_strings = [design_results[f"PRIMER_{design_input.task.task_type}_EXPLAIN"]]
        failures = Primer3._build_failures(dinuc_failures, failure_strings)
        design_candidates: Primer3Result = Primer3Result(designs=valid_designs, failures=failures)
        return design_candidates

    @staticmethod
    def _build_primer_pairs(
        design_input: Primer3Input,
        design_results: dict[str, str],
        design_region: Span,
        unmasked_design_seq: str,
    ) -> list[PrimerPair]:
        """
        Builds a list of primer pairs from single primer designs emitted from Primer3.

        Args:
            design_input: the target region, design task, specifications, and scoring penalties
            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

        Returns:
            primer_pairs: a list of unsorted and unfiltered paired primer designs emitted by Primer3

        Raises:
            ValueError: if Primer3 does not return the same number of left and right designs
        """
        left_primers = Primer3._build_oligos(
            design_input=design_input,
            design_results=design_results,
            design_region=design_region,
            design_task=DesignLeftPrimersTask(),
            unmasked_design_seq=unmasked_design_seq,
        )

        right_primers = Primer3._build_oligos(
            design_input=design_input,
            design_results=design_results,
            design_region=design_region,
            design_task=DesignRightPrimersTask(),
            unmasked_design_seq=unmasked_design_seq,
        )

        def _build_primer_pair(num: int, primer_pair: tuple[Oligo, Oligo]) -> PrimerPair:
            """Builds the `PrimerPair` object from input left and right primers."""
            left_primer = primer_pair[0]
            right_primer = primer_pair[1]
            amplicon = replace(left_primer.span, end=right_primer.span.end)
            slice_offset = design_region.get_offset(amplicon.start)
            slice_end = slice_offset + amplicon.length

            return PrimerPair(
                left_primer=left_primer,
                right_primer=right_primer,
                amplicon_tm=float(design_results[f"PRIMER_PAIR_{num}_PRODUCT_TM"]),
                penalty=float(design_results[f"PRIMER_PAIR_{num}_PENALTY"]),
                amplicon_sequence=unmasked_design_seq[slice_offset:slice_end],
            )

        #  Primer3 returns an equal number of left and right primers during primer pair design
        if len(left_primers) != len(right_primers):
            raise ValueError("Primer3 returned a different number of left and right primers.")
        primer_pairs: list[PrimerPair] = [
            _build_primer_pair(num, primer_pair)
            for num, primer_pair in enumerate(zip(left_primers, right_primers, strict=True))
        ]
        return primer_pairs

    @staticmethod
    def _assemble_primer_pairs(
        design_input: Primer3Input,
        design_results: dict[str, str],
        unfiltered_designs: list[PrimerPair],
    ) -> Primer3Result:
        """Helper function to organize primer pairs into valid and failed designs.

        Wraps `Primer3._screen_pair_results()` and `Primer3._build_failures()` to filter out designs
        with dinucleotide runs that are too long and extract additional failure reasons emitted by
        Primer3.

        Args:
            design_input: encapsulates the target region, design task, specifications,
             and scoring penalties
            unfiltered_designs: list of primer pairs emitted from Primer3
             design_results: key-value pairs of results reported by Primer3

        Returns:
            primer_designs: a `Primer3Result` that encapsulates valid and failed designs
        """
        valid_primer_pair_designs: list[PrimerPair]
        dinuc_pair_failures: list[Oligo]
        valid_primer_pair_designs, dinuc_pair_failures = Primer3._screen_pair_results(
            design_input=design_input, designed_primer_pairs=unfiltered_designs
        )

        failure_strings = [
            design_results["PRIMER_PAIR_EXPLAIN"],
            design_results["PRIMER_LEFT_EXPLAIN"],
            design_results["PRIMER_RIGHT_EXPLAIN"],
        ]
        pair_failures = Primer3._build_failures(dinuc_pair_failures, failure_strings)
        primer_designs = Primer3Result(designs=valid_primer_pair_designs, failures=pair_failures)

        return primer_designs

    @staticmethod
    def _build_failures(
        dinuc_failures: list[Oligo],
        failure_strings: list[str],
    ) -> list[Primer3Failure]:
        """Extracts the reasons why designs that were considered by Primer3 failed
         (when there were failures).

        The set of failures is returned sorted from those with most
        failures to those with least.

        Args:
            dinuc_failures: primer designs with a dinucleotide run longer than the allowed maximum
            failure_strings: explanations (strings) emitted by Primer3 about failed designs


        Returns:
            a list of Primer3Failure objects
        """

        by_fail_count: Counter[Primer3FailureReason] = Primer3FailureReason.parse_failures(
            *failure_strings
        )
        # Count how many individual primers failed for dinuc runs
        num_dinuc_failures = len(set(dinuc_failures))
        if num_dinuc_failures > 0:
            by_fail_count[Primer3FailureReason.LONG_DINUC] = num_dinuc_failures
        return [Primer3Failure(reason, count) for reason, count in by_fail_count.most_common()]

    def _create_design_region(
        self,
        target_region: Span,
        max_amplicon_length: int,
        min_primer_length: int,
    ) -> Span:
        """
        Construct a design region surrounding the target region.

        The target region is padded on both sides by the maximum amplicon length, minus the length
        of the target region itself.

        If the target region cannot be padded by at least the minimum primer length on both sides,
        a `ValueError` is raised.

        Raises:
            ValueError: If the target region is too large to be padded.

        """
        # Pad the target region on both sides by the maximum amplicon length (minus the length of
        # the target). This ensures that the design region covers the complete window of potentially
        # valid primer pairs.
        padding: int = max_amplicon_length - target_region.length

        # Apply the padding, ensuring that we don't run out-of-bounds on the target contig.
        contig_length: int = self._dict[target_region.refname].length
        design_start: int = max(1, target_region.start - padding)
        design_end: int = min(target_region.end + padding, contig_length)

        # Validate that our design window includes sufficient space for a primer to be designed on
        # each side of the target region.
        left_design_window: int = target_region.start - design_start
        right_design_window: int = design_end - target_region.end
        if left_design_window < min_primer_length or right_design_window < min_primer_length:
            raise ValueError(
                f"Target region {target_region} exceeds the maximum size compatible with a "
                f"maximum amplicon length of {max_amplicon_length} and a minimum primer length of "
                f"{min_primer_length}. The maximum amplicon length should exceed the length of "
                "the target region by at least twice the minimum primer length."
            )

        # Return the validated design region.
        design_region: Span = replace(
            target_region,
            start=design_start,
            end=design_end,
        )

        return design_region
Functions
__init__
__init__(
    genome_fasta: Path,
    executable: Optional[str] = None,
    variant_lookup: Optional[VariantLookup] = None,
) -> None

Parameters:

Name Type Description Default
genome_fasta Path

Path to reference genome .fasta file

required
executable Optional[str]

string representation of the path to primer3_core

None
variant_lookup Optional[VariantLookup]

VariantLookup object to facilitate hard-masking variants

None

Assumes the sequence dictionary is located adjacent to the .fasta file and has the same base name with a .dict suffix.

Source code in prymer/primer3/primer3.py
def __init__(
    self,
    genome_fasta: Path,
    executable: Optional[str] = None,
    variant_lookup: Optional[VariantLookup] = None,
) -> None:
    """
    Args:
        genome_fasta: Path to reference genome .fasta file
        executable: string representation of the path to primer3_core
        variant_lookup: VariantLookup object to facilitate hard-masking variants

    Assumes the sequence dictionary is located adjacent to the .fasta file and has the same
    base name with a .dict suffix.

    """
    executable_path = ExecutableRunner.validate_executable_path(
        executable="primer3_core" if executable is None else executable
    )
    command: list[str] = [f"{executable_path}"]

    self.variant_lookup = variant_lookup
    self._fasta = pysam.FastaFile(filename=f"{genome_fasta}")

    dict_path = genome_fasta.with_suffix(".dict")
    # TODO: This is a placeholder while waiting for #160  to be resolved
    # https://github.com/fulcrumgenomics/fgpyo/pull/160
    with reader(dict_path, file_type=sam.SamFileType.SAM) as fh:
        self._dict: SequenceDictionary = SequenceDictionary.from_sam(header=fh.header)

    super().__init__(command=command, stderr=subprocess.STDOUT)
close
close() -> bool

Closes fasta file regardless of underlying subprocess status. Logs an error if the underlying subprocess is not successfully closed.

Returns:

Name Type Description
True bool

if the subprocess was terminated successfully

False bool

if the subprocess failed to terminate or was not already running

Source code in prymer/primer3/primer3.py
def close(self) -> bool:
    """Closes fasta file regardless of underlying subprocess status.
    Logs an error if the underlying subprocess is not successfully closed.

    Returns:
        True: if the subprocess was terminated successfully
        False: if the subprocess failed to terminate or was not already running
    """
    self._fasta.close()
    subprocess_close = super().close()
    if not subprocess_close:
        logging.getLogger(__name__).debug("Did not successfully close underlying subprocess")
    return subprocess_close
design
design(design_input: Primer3Input) -> Primer3Result

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

Parameters:

Name Type Description Default
design_input Primer3Input

encapsulates the target region, design task, specifications, and scoring penalties

required

Returns:

Type Description
Primer3Result

Primer3Result containing both the valid and failed designs emitted by Primer3

Raises:

Type Description
RuntimeError

if underlying subprocess is not alive

ValueError

if Primer3 returns errors or does not return output

ValueError

if Primer3 output is malformed

ValueError

if an unknown design task is given

Source code in prymer/primer3/primer3.py
def design(self, design_input: Primer3Input) -> Primer3Result:  # noqa: C901
    """Designs primers, primer pairs, and/or internal probes given a target region.

    Args:
        design_input: encapsulates the target region, design task, specifications, and scoring
            penalties

    Returns:
        Primer3Result containing both the valid and failed designs emitted by Primer3

    Raises:
        RuntimeError: if underlying subprocess is not alive
        ValueError: if Primer3 returns errors or does not return output
        ValueError: if Primer3 output is malformed
        ValueError: if an unknown design task is given
    """

    if not self.is_alive:
        raise RuntimeError(
            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():
            if design_input.target.length < design_input.probe_params.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}"
                )
            design_region = design_input.target
        case DesignRightPrimersTask() | DesignLeftPrimersTask() | DesignPrimerPairsTask():
            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,
            )
        case _ as unreachable:
            assert_never(unreachable)  # pragma: no cover

    soft_masked, hard_masked = self.get_design_sequences(design_region)
    # use 1-base coords, explain primer designs, use hard-masked sequence, and compute
    # thermodynamic attributes
    global_primer3_params = {
        Primer3InputTag.PRIMER_FIRST_BASE_INDEX: 1,
        Primer3InputTag.PRIMER_EXPLAIN_FLAG: 1,
        Primer3InputTag.SEQUENCE_TEMPLATE: hard_masked,
        Primer3InputTag.PRIMER_THERMODYNAMIC_OLIGO_ALIGNMENT: 1,
    }

    assembled_primer3_tags = {
        **global_primer3_params,
        **design_input.to_input_tags(design_region=design_region),
    }
    # Submit inputs to primer3
    for tag, value in assembled_primer3_tags.items():
        self._subprocess.stdin.write(f"{tag}={value}")
        self._subprocess.stdin.write("\n")
    self._subprocess.stdin.write("=\n")
    self._subprocess.stdin.flush()

    error_lines: list[str] = []  # list of errors as reported by primer3
    primer3_results: dict[str, str] = {}  # key-value pairs of results reported by Primer3

    def primer3_error(message: str) -> None:
        """Formats the Primer3 error and raises a ValueError."""
        error_message = f"{message}: "
        # add in any reported PRIMER_ERROR
        if "PRIMER_ERROR" in primer3_results:
            error_message += primer3_results["PRIMER_ERROR"]
        # add in any error lines
        if len(error_lines) > 0:
            error_message += "\n".join(f"\t\t{e}" for e in error_lines)
        # raise the exception now
        raise ValueError(error_message)

    while True:
        # Get the next line.  Since we want to distinguish between empty lines, which we ignore,
        # and the end-of-file, which is just an empty string, check for an empty string before
        # stripping the line of any trailing newline or carriage return characters.
        line: str = self._subprocess.stdout.readline()
        if line == "":  # EOF
            primer3_error("Primer3 exited prematurely")
        line = line.rstrip("\r\n")

        if line == "=":  # stop when we find the line just "="
            break
        elif line == "":  # ignore empty lines
            continue
        elif "=" not in line:  # error lines do not have the equals character in them, usually
            error_lines.append(line)
        else:  # parse and store the result
            key, value = line.split("=", maxsplit=1)
            # Because Primer3 will emit both the input given and the output generated, we
            # discard the input that is echo'ed back by looking for tags (keys)
            # that do not match any Primer3InputTag
            if not any(key == item.value for item in Primer3InputTag):
                primer3_results[key] = value

    # Check for any errors.  Typically, these are in error_lines, but also the results can
    # contain the PRIMER_ERROR key.
    if "PRIMER_ERROR" in primer3_results or len(error_lines) > 0:
        primer3_error("Primer3 failed")

    match design_input.task:
        case DesignPrimerPairsTask():  # Primer pair design
            all_pair_results: list[PrimerPair] = Primer3._build_primer_pairs(
                design_input=design_input,
                design_results=primer3_results,
                design_region=design_region,
                unmasked_design_seq=soft_masked,
            )
            return Primer3._assemble_primer_pairs(
                design_input=design_input,
                design_results=primer3_results,
                unfiltered_designs=all_pair_results,
            )

        case DesignLeftPrimersTask() | DesignRightPrimersTask() | PickHybProbeOnly():
            # Single primer or probe design
            all_single_results: list[Oligo] = Primer3._build_oligos(
                design_input=design_input,
                design_results=primer3_results,
                design_region=design_region,
                design_task=design_input.task,
                unmasked_design_seq=soft_masked,
            )
            return Primer3._assemble_single_designs(
                design_input=design_input,
                design_results=primer3_results,
                unfiltered_designs=all_single_results,
            )

        case _ as unreachable:
            assert_never(unreachable)
get_design_sequences
get_design_sequences(region: Span) -> tuple[str, str]

Extracts the reference sequence that corresponds to the design region.

Parameters:

Name Type Description Default
region Span

the region of the genome to be extracted

required

Returns:

Type Description
str

A tuple of two sequences: the sequence for the region, and the sequence for the region

str

with variants hard-masked as Ns

Source code in prymer/primer3/primer3.py
def get_design_sequences(self, region: Span) -> tuple[str, str]:
    """Extracts the reference sequence that corresponds to the design region.

    Args:
        region: the region of the genome to be extracted

    Returns:
        A tuple of two sequences: the sequence for the region, and the sequence for the region
        with variants hard-masked as Ns

    """
    # pysam.fetch: 0-based, half-open intervals
    soft_masked = self._fasta.fetch(
        reference=region.refname, start=region.start - 1, end=region.end
    )

    if self.variant_lookup is None:
        hard_masked = soft_masked
        return soft_masked, hard_masked

    overlapping_variants: list[SimpleVariant] = self.variant_lookup.query(
        refname=region.refname, start=region.start, end=region.end
    )
    positions: list[int] = []
    for variant in overlapping_variants:
        # FIXME
        positions.extend(range(variant.pos, variant.end + 1))

    filtered_positions = [pos for pos in positions if region.start <= pos <= region.end]
    soft_masked_list = list(soft_masked)
    for pos in filtered_positions:
        soft_masked_list[region.get_offset(pos)] = (
            "N"  # get relative coord of filtered position and mask to N
        )
    # convert list back to string
    hard_masked = "".join(soft_masked_list)
    return soft_masked, hard_masked
Primer3Failure dataclass

Bases: Metric['Primer3Failure']

Encapsulates how many designs failed for a given reason. Extends the fgpyo.util.metric.Metric class, which will facilitate writing out results for primer design QC etc.

Attributes:

Name Type Description
reason Primer3FailureReason

the reason the design failed

count int

how many designs failed

Source code in prymer/primer3/primer3.py
@dataclass(init=True, slots=True, frozen=True)
class Primer3Failure(Metric["Primer3Failure"]):
    """Encapsulates how many designs failed for a given reason.
    Extends the `fgpyo.util.metric.Metric` class, which will facilitate writing out results for
    primer design QC etc.

    Attributes:
        reason: the reason the design failed
        count: how many designs failed
    """

    reason: Primer3FailureReason
    count: int
Primer3Result dataclass

Bases: Generic[OligoLikeType]

Encapsulates Primer3 design results (both valid designs and failures).

Attributes:

Name Type Description
designs list[OligoLikeType]

filtered for out-of-spec characteristics and ordered (by objective function score) list of primer pairs or single oligos that were returned by Primer3

failures list[Primer3Failure]

ordered list of Primer3Failures detailing design failure reasons and corresponding count

Source code in prymer/primer3/primer3.py
@dataclass(init=True, slots=True, frozen=True)
class Primer3Result(Generic[OligoLikeType]):
    """Encapsulates Primer3 design results (both valid designs and failures).

    Attributes:
        designs: filtered for out-of-spec characteristics and ordered (by objective function score)
            list of primer pairs or single oligos that were returned by Primer3
        failures: ordered list of Primer3Failures detailing design failure reasons and corresponding
            count
    """

    designs: list[OligoLikeType]
    failures: list[Primer3Failure]

    def as_primer_result(self) -> "Primer3Result[Oligo]":
        """Returns this Primer3Result assuming the design results are of type `Primer`."""
        if len(self.designs) > 0 and not isinstance(self.designs[0], Oligo):
            raise ValueError("Cannot call `as_primer_result` on `PrimerPair` results")
        return typing.cast(Primer3Result[Oligo], self)

    def as_primer_pair_result(self) -> "Primer3Result[PrimerPair]":
        """Returns this Primer3Result assuming the design results are of type `PrimerPair`."""
        if len(self.designs) > 0 and not isinstance(self.designs[0], PrimerPair):
            raise ValueError("Cannot call `as_primer_pair_result` on `Oligo` results")
        return typing.cast(Primer3Result[PrimerPair], self)

    def primers(self) -> list[Oligo]:
        """Returns the design results as a list `Primer`s"""
        try:
            return self.as_primer_result().designs
        except ValueError as ex:
            raise ValueError("Cannot call `primers` on `PrimerPair` results") from ex

    def primer_pairs(self) -> list[PrimerPair]:
        """Returns the design results as a list `PrimerPair`s"""
        try:
            return self.as_primer_pair_result().designs
        except ValueError as ex:
            raise ValueError("Cannot call `primer_pairs` on `Oligo` results") from ex
Functions
as_primer_pair_result
as_primer_pair_result() -> Primer3Result[PrimerPair]

Returns this Primer3Result assuming the design results are of type PrimerPair.

Source code in prymer/primer3/primer3.py
def as_primer_pair_result(self) -> "Primer3Result[PrimerPair]":
    """Returns this Primer3Result assuming the design results are of type `PrimerPair`."""
    if len(self.designs) > 0 and not isinstance(self.designs[0], PrimerPair):
        raise ValueError("Cannot call `as_primer_pair_result` on `Oligo` results")
    return typing.cast(Primer3Result[PrimerPair], self)
as_primer_result
as_primer_result() -> Primer3Result[Oligo]

Returns this Primer3Result assuming the design results are of type Primer.

Source code in prymer/primer3/primer3.py
def as_primer_result(self) -> "Primer3Result[Oligo]":
    """Returns this Primer3Result assuming the design results are of type `Primer`."""
    if len(self.designs) > 0 and not isinstance(self.designs[0], Oligo):
        raise ValueError("Cannot call `as_primer_result` on `PrimerPair` results")
    return typing.cast(Primer3Result[Oligo], self)
primer_pairs
primer_pairs() -> list[PrimerPair]

Returns the design results as a list PrimerPairs

Source code in prymer/primer3/primer3.py
def primer_pairs(self) -> list[PrimerPair]:
    """Returns the design results as a list `PrimerPair`s"""
    try:
        return self.as_primer_pair_result().designs
    except ValueError as ex:
        raise ValueError("Cannot call `primer_pairs` on `Oligo` results") from ex
primers
primers() -> list[Oligo]

Returns the design results as a list Primers

Source code in prymer/primer3/primer3.py
def primers(self) -> list[Oligo]:
    """Returns the design results as a list `Primer`s"""
    try:
        return self.as_primer_result().designs
    except ValueError as ex:
        raise ValueError("Cannot call `primers` on `PrimerPair` results") from ex

primer3_failure_reason

Primer3FailureReason Class

This module contains a Primer3FailureReason class. Based on user-specified criteria, Primer3 will disqualify primer designs if the characteristics of the design are outside an allowable range of parameters.

Failure reason strings are documented in the Primer3 source code, accessible here (at the time of Primer3FailureReason implementation): https://github.com/bioinfo-ut/primer3_masker/blob/master/src/libprimer3.c#L5581-L5604

Example Primer3 failure reasons emitted

Primer3 shall return a comma-delimited list of failure explanations. They will have key PRIMER_LEFT_EXPLAIN for designing individual left primers, PRIMER_RIGHT_EXPLAIN for designing individual right primers, and PRIMER_PAIR_EXPLAIN for designing primer pairs.

For individual primers:

>>> failure_string = 'considered 160, too many Ns 20, low tm 127, ok 13'
>>> Primer3FailureReason.parse_failures(failure_string)
Counter({<Primer3FailureReason.LOW_TM: 'low tm'>: 127, <Primer3FailureReason.TOO_MANY_NS: 'too many Ns'>: 20})
>>> failure_string = 'considered 238, low tm 164, high tm 12, high hairpin stability 23, ok 39'
>>> Primer3FailureReason.parse_failures(failure_string)
Counter({<Primer3FailureReason.LOW_TM: 'low tm'>: 164, <Primer3FailureReason.HAIRPIN_STABILITY: 'high hairpin stability'>: 23, <Primer3FailureReason.HIGH_TM: 'high tm'>: 12})
>>> failure_string = 'considered 166, unacceptable product size 161, ok 5'
>>> Primer3FailureReason.parse_failures(failure_string)
Counter({<Primer3FailureReason.PRODUCT_SIZE: 'unacceptable product size'>: 161})

Classes

Primer3FailureReason

Bases: StrEnum

Enum to represent the various reasons Primer3 removes primers and primer pairs.

https://github.com/bioinfo-ut/primer3_masker/blob/

6a40c4c408dc02b95ac02391457cda760092291a/src/libprimer3.c#L5581-L5605

This also contains custom failure values that are not generated by Primer3 but are convenient to have so that post-processing failures can be tracked in the same way that Primer3 failures are. These include LONG_DINUC, SECONDARY_STRUCTURE, and OFF_TARGET_AMPLIFICATION.

Source code in prymer/primer3/primer3_failure_reason.py
@unique
class Primer3FailureReason(StrEnum):
    """
    Enum to represent the various reasons Primer3 removes primers and primer pairs.

    These are taken from: https://github.com/bioinfo-ut/primer3_masker/blob/
      6a40c4c408dc02b95ac02391457cda760092291a/src/libprimer3.c#L5581-L5605

    This also contains custom failure values that are not generated by Primer3 but are convenient
    to have so that post-processing failures can be tracked in the same way that Primer3 failures
    are.  These include `LONG_DINUC`, `SECONDARY_STRUCTURE`, and `OFF_TARGET_AMPLIFICATION`.
    """

    # Failure reasons emitted by Primer3
    GC_CONTENT = "GC content failed"
    GC_CLAMP = "GC clamp failed"
    HAIRPIN_STABILITY = "high hairpin stability"
    HIGH_TM = "high tm"
    LOW_TM = "low tm"
    LOWERCASE_MASKING = "lowercase masking of 3' end"
    LONG_POLY_X = "long poly-x seq"
    PRODUCT_SIZE = "unacceptable product size"
    TOO_MANY_NS = "too many Ns"
    HIGH_ANY_COMPLEMENTARITY = "high any compl"
    HIGH_END_COMPLEMENTARITY = "high end compl"
    # Failure reasons not emitted by Primer3 and beneficial to keep track of
    LONG_DINUC = "long dinucleotide run"
    SECONDARY_STRUCTURE = "undesirable secondary structure"
    OFF_TARGET_AMPLIFICATION = "amplifies off-target regions"

    @classmethod
    def from_reason(cls, str_reason: str) -> Optional["Primer3FailureReason"]:
        """Returns the first `Primer3FailureReason` with the given reason for failure.
        If no failure exists, return `None`."""
        reason: Optional[Primer3FailureReason] = None
        try:
            reason = cls(str_reason)
        except ValueError:
            pass
        return reason

    @staticmethod
    def parse_failures(
        *failures: str,
    ) -> Counter["Primer3FailureReason"]:
        """When Primer3 encounters failures, extracts the reasons why designs that were considered
         by Primer3 failed.

        Args:
            failures: list of strings, with each string an "explanation" emitted by Primer3 about
                why the design failed.  Each string may be a comma delimited of failures, or a
                single failure.

        Returns:
            a `Counter` of each `Primer3FailureReason` reason.
        """

        failure_regex = r"^ ?(.+) ([0-9]+)$"
        by_fail_count: Counter[Primer3FailureReason] = Counter()
        # parse all failure strings and merge together counts for the same kinds of failures
        for failure in failures:
            split_failures = [fail.strip() for fail in failure.split(",")]
            for item in split_failures:
                result = re.match(failure_regex, item)
                if result is None:
                    continue
                reason = result.group(1)
                count = int(result.group(2))
                if reason in ["ok", "considered"]:
                    continue
                std_reason = Primer3FailureReason.from_reason(reason)
                if std_reason is None:
                    logging.getLogger(__name__).debug(f"Unknown Primer3 failure reason: {reason}")
                by_fail_count[std_reason] += count

        return by_fail_count
Functions
from_reason classmethod
from_reason(
    str_reason: str,
) -> Optional[Primer3FailureReason]

Returns the first Primer3FailureReason with the given reason for failure. If no failure exists, return None.

Source code in prymer/primer3/primer3_failure_reason.py
@classmethod
def from_reason(cls, str_reason: str) -> Optional["Primer3FailureReason"]:
    """Returns the first `Primer3FailureReason` with the given reason for failure.
    If no failure exists, return `None`."""
    reason: Optional[Primer3FailureReason] = None
    try:
        reason = cls(str_reason)
    except ValueError:
        pass
    return reason
parse_failures staticmethod
parse_failures(
    *failures: str,
) -> Counter[Primer3FailureReason]

When Primer3 encounters failures, extracts the reasons why designs that were considered by Primer3 failed.

Parameters:

Name Type Description Default
failures str

list of strings, with each string an "explanation" emitted by Primer3 about why the design failed. Each string may be a comma delimited of failures, or a single failure.

()

Returns:

Type Description
Counter[Primer3FailureReason]

a Counter of each Primer3FailureReason reason.

Source code in prymer/primer3/primer3_failure_reason.py
@staticmethod
def parse_failures(
    *failures: str,
) -> Counter["Primer3FailureReason"]:
    """When Primer3 encounters failures, extracts the reasons why designs that were considered
     by Primer3 failed.

    Args:
        failures: list of strings, with each string an "explanation" emitted by Primer3 about
            why the design failed.  Each string may be a comma delimited of failures, or a
            single failure.

    Returns:
        a `Counter` of each `Primer3FailureReason` reason.
    """

    failure_regex = r"^ ?(.+) ([0-9]+)$"
    by_fail_count: Counter[Primer3FailureReason] = Counter()
    # parse all failure strings and merge together counts for the same kinds of failures
    for failure in failures:
        split_failures = [fail.strip() for fail in failure.split(",")]
        for item in split_failures:
            result = re.match(failure_regex, item)
            if result is None:
                continue
            reason = result.group(1)
            count = int(result.group(2))
            if reason in ["ok", "considered"]:
                continue
            std_reason = Primer3FailureReason.from_reason(reason)
            if std_reason is None:
                logging.getLogger(__name__).debug(f"Unknown Primer3 failure reason: {reason}")
            by_fail_count[std_reason] += count

    return by_fail_count

primer3_input

Primer3Input Class and Methods

This module contains the Primer3Input class. The class wraps together different helper classes to assemble user-specified criteria and parameters for input to Primer3.

The module uses:

  1. PrimerAndAmpliconParameters to specify user-specified criteria for primer design
  2. ProbeParameters to specify user-specified criteria for probe design
  3. PrimerAndAmpliconWeights to establish penalties based on those criteria
  4. ProbeWeights to specify penalties based on probe design criteria
  5. Primer3Task to organize task-specific logic.
  6. [Span](index.md#prymer.api.span.Span] to specify the target region.

The Primer3Input.to_input_tags(] method The main purpose of this class is to generate the Primer3InputTagss required by Primer3 for specifying how to design the primers, returned by the to_input_tags(] method.

Examples

The following examples builds the Primer3 tags for designing left primers:

```python

from prymer.api import MinOptMax, Strand from prymer.primer3 import DesignLeftPrimersTask target = Span(refname="chr1", start=201, end=250, strand=Strand.POSITIVE) design_region = Span(refname="chr1", start=150, end=300, strand=Strand.POSITIVE) 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), 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(target=target, primer_and_amplicon_params=params, task=DesignLeftPrimersTask() )

for tag, value in design_input.to_input_tags(design_region=design_region).items(): print(f"{tag.value} -> {value}") PRIMER_TASK -> pick_primer_list PRIMER_PICK_LEFT_PRIMER -> 1 PRIMER_PICK_RIGHT_PRIMER -> 0 PRIMER_PICK_INTERNAL_OLIGO -> 0 SEQUENCE_INCLUDED_REGION -> 1,51 PRIMER_PRODUCT_OPT_SIZE -> 200 PRIMER_PRODUCT_SIZE_RANGE -> 100-250 PRIMER_PRODUCT_MIN_TM -> 55.0 PRIMER_PRODUCT_OPT_TM -> 70.0 PRIMER_PRODUCT_MAX_TM -> 100.0 PRIMER_MIN_SIZE -> 29 PRIMER_OPT_SIZE -> 30 PRIMER_MAX_SIZE -> 31 PRIMER_MIN_TM -> 63.0 PRIMER_OPT_TM -> 65.0 PRIMER_MAX_TM -> 67.0 PRIMER_MIN_GC -> 30.0 PRIMER_OPT_GC_PERCENT -> 45.0 PRIMER_MAX_GC -> 65.0 PRIMER_GC_CLAMP -> 0 PRIMER_MAX_END_GC -> 5 PRIMER_MAX_POLY_X -> 5 PRIMER_MAX_NS_ACCEPTED -> 1 PRIMER_LOWERCASE_MASKING -> 1 PRIMER_NUM_RETURN -> 5 PRIMER_MAX_SELF_ANY_TH -> 53.0 PRIMER_MAX_SELF_END_TH -> 53.0 PRIMER_MAX_HAIRPIN_TH -> 53.0 PRIMER_PAIR_WT_PRODUCT_SIZE_LT -> 1.0 PRIMER_PAIR_WT_PRODUCT_SIZE_GT -> 1.0 PRIMER_PAIR_WT_PRODUCT_TM_LT -> 0.0 PRIMER_PAIR_WT_PRODUCT_TM_GT -> 0.0 PRIMER_WT_END_STABILITY -> 0.25 PRIMER_WT_GC_PERCENT_LT -> 0.25 PRIMER_WT_GC_PERCENT_GT -> 0.25 PRIMER_WT_SELF_ANY -> 0.1 PRIMER_WT_SELF_END -> 0.1 PRIMER_WT_SIZE_LT -> 0.5 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

Attributes

Classes

Primer3Input dataclass

Assembles necessary inputs for Primer3 to orchestrate primer, primer pair, and/or internal probe design.

At least one set of design parameters (either PrimerAndAmpliconParameters or ProbeParameters) must be specified.

If PrimerAndAmpliconParameters is provided but PrimerAndAmpliconWeights is not provided, default PrimerAndAmpliconWeights will be used.

Similarly, if ProbeParameters is provided but ProbeWeights is not provided, default ProbeWeights will be used.

Please see primer3_parameters.py for details on the defaults.

Raises:

Type Description
ValueError

if neither the primer or probe parameters are specified

Source code in prymer/primer3/primer3_input.py
@dataclass(frozen=True, init=True, slots=True)
class Primer3Input:
    """Assembles necessary inputs for Primer3 to orchestrate primer, primer pair, and/or internal
    probe design.

    At least one set of design parameters (either `PrimerAndAmpliconParameters`
    or `ProbeParameters`) must be specified.

    If `PrimerAndAmpliconParameters` is provided but `PrimerAndAmpliconWeights` is not provided,
    default `PrimerAndAmpliconWeights` will be used.

    Similarly, if `ProbeParameters` is provided but `ProbeWeights` is not provided, default
    `ProbeWeights` will be used.

    Please see primer3_parameters.py for details on the defaults.


    Raises:
        ValueError: if neither the primer or  probe parameters are specified
    """

    target: Span
    task: Primer3TaskType
    primer_and_amplicon_params: Optional[PrimerAndAmpliconParameters] = None
    probe_params: Optional[ProbeParameters] = None
    primer_weights: Optional[PrimerAndAmpliconWeights] = None
    probe_weights: Optional[ProbeWeights] = None

    def __post_init__(self) -> None:
        # check for at least one set of params
        # for the set of params given, check that weights were given; use defaults if not given
        if self.primer_and_amplicon_params is None and self.probe_params is None:
            raise ValueError(
                "Primer3 requires at least one set of parameters"
                " for either primer or probe design"
            )

        if self.primer_and_amplicon_params is not None and self.primer_weights is None:
            object.__setattr__(self, "primer_weights", PrimerAndAmpliconWeights())

        if self.probe_params is not None and self.probe_weights is None:
            object.__setattr__(self, "probe_weights", ProbeWeights())

    def to_input_tags(self, design_region: Span) -> dict[Primer3InputTag, Any]:
        """Assembles `Primer3InputTag` and values for input to `Primer3`

        The target region must be wholly contained within design region.

        Args:
            design_region: the design region, which wholly contains the target region, in which
                    primers are to be designed.

        Returns:
            a mapping of `Primer3InputTag`s to associated value
        """
        primer3_task_params = self.task.to_input_tags(
            design_region=design_region, target=self.target
        )
        assembled_tags: dict[Primer3InputTag, Any] = {**primer3_task_params}

        optional_attributes = {
            field.name: getattr(self, field.name)
            for field in fields(self)
            if field.default is not MISSING
        }
        for settings in optional_attributes.values():
            if settings is not None:
                assembled_tags.update(settings.to_input_tags())

        return assembled_tags
Functions
to_input_tags
to_input_tags(
    design_region: Span,
) -> dict[Primer3InputTag, Any]

Assembles Primer3InputTag and values for input to Primer3

The target region must be wholly contained within design region.

Parameters:

Name Type Description Default
design_region Span

the design region, which wholly contains the target region, in which primers are to be designed.

required

Returns:

Type Description
dict[Primer3InputTag, Any]

a mapping of Primer3InputTags to associated value

Source code in prymer/primer3/primer3_input.py
def to_input_tags(self, design_region: Span) -> dict[Primer3InputTag, Any]:
    """Assembles `Primer3InputTag` and values for input to `Primer3`

    The target region must be wholly contained within design region.

    Args:
        design_region: the design region, which wholly contains the target region, in which
                primers are to be designed.

    Returns:
        a mapping of `Primer3InputTag`s to associated value
    """
    primer3_task_params = self.task.to_input_tags(
        design_region=design_region, target=self.target
    )
    assembled_tags: dict[Primer3InputTag, Any] = {**primer3_task_params}

    optional_attributes = {
        field.name: getattr(self, field.name)
        for field in fields(self)
        if field.default is not MISSING
    }
    for settings in optional_attributes.values():
        if settings is not None:
            assembled_tags.update(settings.to_input_tags())

    return assembled_tags

primer3_input_tag

Primer3InputTag Class and Methods

This module contains a class, Primer3InputTag, to standardize user input to Primer3. Settings that are controlled here include general parameters that apply to a singular Primer3 session as well as query-specific settings that can be changed in between primer queries.

Classes

Primer3InputTag

Bases: UppercaseStrEnum

Enumeration of Primer3 input tags.

Please see the Primer3 manual for additional details

https://primer3.org/manual.html#commandLineTags

This class represents two categories of Primer3 input tags
  • SEQUENCE_ tags are those that control sequence-specific attributes of Primer3 jobs. These can be modified after each query submitted to Primer3.
  • PRIMER_ tags are those that describe more general parameters of Primer3 jobs. These attributes persist across queries to Primer3 unless they are explicitly reset. Errors in these "global" input tags are fatal.
Source code in prymer/primer3/primer3_input_tag.py
@unique
class Primer3InputTag(UppercaseStrEnum):
    """
    Enumeration of Primer3 input tags.

    Please see the Primer3 manual for additional details:
     https://primer3.org/manual.html#commandLineTags

    This class represents two categories of Primer3 input tags:
     * `SEQUENCE_` tags are those that control sequence-specific attributes of Primer3 jobs.
       These can be modified after each query submitted to Primer3.
     * `PRIMER_` tags are those that describe more general parameters of Primer3 jobs.
       These attributes persist across queries to Primer3 unless they are explicitly reset.
       Errors in these "global" input tags are fatal.
    """

    # Sequence input tags; query-specific
    SEQUENCE_EXCLUDED_REGION = auto()
    SEQUENCE_INCLUDED_REGION = auto()
    SEQUENCE_PRIMER_REVCOMP = auto()
    SEQUENCE_FORCE_LEFT_END = auto()
    SEQUENCE_INTERNAL_EXCLUDED_REGION = auto()
    SEQUENCE_QUALITY = auto()
    SEQUENCE_FORCE_LEFT_START = auto()
    SEQUENCE_INTERNAL_OLIGO = auto()
    SEQUENCE_START_CODON_POSITION = auto()
    SEQUENCE_FORCE_RIGHT_END = auto()
    SEQUENCE_OVERLAP_JUNCTION_LIST = auto()
    SEQUENCE_TARGET = auto()
    SEQUENCE_FORCE_RIGHT_START = auto()
    SEQUENCE_PRIMER = auto()
    SEQUENCE_TEMPLATE = auto()
    SEQUENCE_ID = auto()
    SEQUENCE_PRIMER_PAIR_OK_REGION_LIST = auto()

    # Global input tags; will persist across primer3 queries
    PRIMER_DNA_CONC = auto()
    PRIMER_MAX_END_GC = auto()
    PRIMER_PAIR_WT_PRODUCT_SIZE_LT = auto()
    PRIMER_DNTP_CONC = auto()
    PRIMER_MAX_END_STABILITY = auto()
    PRIMER_PAIR_WT_PRODUCT_TM_GT = auto()
    PRIMER_EXPLAIN_FLAG = auto()
    PRIMER_MAX_GC = auto()
    PRIMER_PAIR_WT_PRODUCT_TM_LT = auto()
    PRIMER_FIRST_BASE_INDEX = auto()
    PRIMER_MAX_HAIRPIN_TH = auto()
    PRIMER_PAIR_WT_PR_PENALTY = auto()
    PRIMER_GC_CLAMP = auto()
    PRIMER_MAX_LIBRARY_MISPRIMING = auto()
    PRIMER_PAIR_WT_TEMPLATE_MISPRIMING = auto()
    PRIMER_INSIDE_PENALTY = auto()
    PRIMER_MAX_NS_ACCEPTED = auto()
    PRIMER_PAIR_WT_TEMPLATE_MISPRIMING_TH = auto()
    PRIMER_INTERNAL_DNA_CONC = auto()
    PRIMER_MAX_POLY_X = auto()
    PRIMER_PICK_ANYWAY = auto()
    PRIMER_INTERNAL_DNTP_CONC = auto()
    PRIMER_MAX_SELF_ANY = auto()
    PRIMER_PICK_INTERNAL_OLIGO = auto()
    PRIMER_INTERNAL_MAX_GC = auto()
    PRIMER_MAX_SELF_ANY_TH = auto()
    PRIMER_PICK_LEFT_PRIMER = auto()
    PRIMER_INTERNAL_MAX_HAIRPIN_TH = auto()
    PRIMER_MAX_SELF_END = auto()
    PRIMER_PICK_RIGHT_PRIMER = auto()
    PRIMER_INTERNAL_MAX_LIBRARY_MISHYB = auto()
    PRIMER_MAX_SELF_END_TH = auto()
    PRIMER_PRODUCT_MAX_TM = auto()
    PRIMER_INTERNAL_MAX_NS_ACCEPTED = auto()
    PRIMER_MAX_SIZE = auto()
    PRIMER_PRODUCT_MIN_TM = auto()
    PRIMER_INTERNAL_MAX_POLY_X = auto()
    PRIMER_MAX_TEMPLATE_MISPRIMING = auto()
    PRIMER_PRODUCT_OPT_SIZE = auto()
    PRIMER_INTERNAL_MAX_SELF_ANY = auto()
    PRIMER_MAX_TEMPLATE_MISPRIMING_TH = auto()
    PRIMER_PRODUCT_OPT_TM = auto()
    PRIMER_INTERNAL_MAX_SELF_ANY_TH = auto()
    PRIMER_MAX_TM = auto()
    PRIMER_PRODUCT_SIZE_RANGE = auto()
    PRIMER_INTERNAL_MAX_SELF_END = auto()
    PRIMER_MIN_3_PRIME_OVERLAP_OF_JUNCTION = auto()
    PRIMER_QUALITY_RANGE_MAX = auto()
    PRIMER_INTERNAL_MAX_SELF_END_TH = auto()
    PRIMER_MIN_5_PRIME_OVERLAP_OF_JUNCTION = auto()
    PRIMER_QUALITY_RANGE_MIN = auto()
    PRIMER_INTERNAL_MAX_SIZE = auto()
    PRIMER_MIN_END_QUALITY = auto()
    PRIMER_SALT_CORRECTIONS = auto()
    PRIMER_INTERNAL_MAX_TM = auto()
    PRIMER_MIN_GC = auto()
    PRIMER_SALT_DIVALENT = auto()
    PRIMER_INTERNAL_MIN_GC = auto()
    PRIMER_MIN_LEFT_THREE_PRIME_DISTANCE = auto()
    PRIMER_SALT_MONOVALENT = auto()
    PRIMER_INTERNAL_MIN_QUALITY = auto()
    PRIMER_MIN_QUALITY = auto()
    PRIMER_SEQUENCING_ACCURACY = auto()
    PRIMER_INTERNAL_MIN_SIZE = auto()
    PRIMER_MIN_RIGHT_THREE_PRIME_DISTANCE = auto()
    PRIMER_SEQUENCING_INTERVAL = auto()
    PRIMER_INTERNAL_MIN_TM = auto()
    PRIMER_MIN_SIZE = auto()
    PRIMER_SEQUENCING_LEAD = auto()
    PRIMER_INTERNAL_MISHYB_LIBRARY = auto()
    PRIMER_MIN_THREE_PRIME_DISTANCE = auto()
    PRIMER_SEQUENCING_SPACING = auto()
    PRIMER_INTERNAL_MUST_MATCH_FIVE_PRIME = auto()
    PRIMER_MIN_TM = auto()
    PRIMER_TASK = auto()
    PRIMER_INTERNAL_MUST_MATCH_THREE_PRIME = auto()
    PRIMER_MISPRIMING_LIBRARY = auto()
    PRIMER_THERMODYNAMIC_OLIGO_ALIGNMENT = auto()
    PRIMER_INTERNAL_OPT_GC_PERCENT = auto()
    PRIMER_MUST_MATCH_FIVE_PRIME = auto()
    PRIMER_THERMODYNAMIC_PARAMETERS_PATH = auto()
    PRIMER_INTERNAL_OPT_SIZE = auto()
    PRIMER_MUST_MATCH_THREE_PRIME = auto()
    PRIMER_THERMODYNAMIC_TEMPLATE_ALIGNMENT = auto()
    PRIMER_INTERNAL_OPT_TM = auto()
    PRIMER_NUM_RETURN = auto()
    PRIMER_TM_FORMULA = auto()
    PRIMER_INTERNAL_SALT_DIVALENT = auto()
    PRIMER_OPT_GC_PERCENT = auto()
    PRIMER_WT_END_QUAL = auto()
    PRIMER_INTERNAL_SALT_MONOVALENT = auto()
    PRIMER_OPT_SIZE = auto()
    PRIMER_WT_END_STABILITY = auto()
    PRIMER_INTERNAL_WT_END_QUAL = auto()
    PRIMER_OPT_TM = auto()
    PRIMER_WT_GC_PERCENT_GT = auto()
    PRIMER_INTERNAL_WT_GC_PERCENT_GT = auto()
    PRIMER_OUTSIDE_PENALTY = auto()
    PRIMER_WT_GC_PERCENT_LT = auto()
    PRIMER_INTERNAL_WT_GC_PERCENT_LT = auto()
    PRIMER_PAIR_MAX_COMPL_ANY = auto()
    PRIMER_WT_HAIRPIN_TH = auto()
    PRIMER_INTERNAL_WT_HAIRPIN_TH = auto()
    PRIMER_PAIR_MAX_COMPL_ANY_TH = auto()
    PRIMER_WT_LIBRARY_MISPRIMING = auto()
    PRIMER_INTERNAL_WT_LIBRARY_MISHYB = auto()
    PRIMER_PAIR_MAX_COMPL_END = auto()
    PRIMER_WT_NUM_NS = auto()
    PRIMER_INTERNAL_WT_NUM_NS = auto()
    PRIMER_PAIR_MAX_COMPL_END_TH = auto()
    PRIMER_WT_POS_PENALTY = auto()
    PRIMER_INTERNAL_WT_SELF_ANY = auto()
    PRIMER_PAIR_MAX_DIFF_TM = auto()
    PRIMER_WT_SELF_ANY = auto()
    PRIMER_INTERNAL_WT_SELF_ANY_TH = auto()
    PRIMER_PAIR_MAX_LIBRARY_MISPRIMING = auto()
    PRIMER_WT_SELF_ANY_TH = auto()
    PRIMER_INTERNAL_WT_SELF_END = auto()
    PRIMER_PAIR_MAX_TEMPLATE_MISPRIMING = auto()
    PRIMER_WT_SELF_END = auto()
    PRIMER_INTERNAL_WT_SELF_END_TH = auto()
    PRIMER_PAIR_MAX_TEMPLATE_MISPRIMING_TH = auto()
    PRIMER_WT_SELF_END_TH = auto()
    PRIMER_INTERNAL_WT_SEQ_QUAL = auto()
    PRIMER_PAIR_WT_COMPL_ANY = auto()
    PRIMER_WT_SEQ_QUAL = auto()
    PRIMER_INTERNAL_WT_SIZE_GT = auto()
    PRIMER_PAIR_WT_COMPL_ANY_TH = auto()
    PRIMER_WT_SIZE_GT = auto()
    PRIMER_INTERNAL_WT_SIZE_LT = auto()
    PRIMER_PAIR_WT_COMPL_END = auto()
    PRIMER_WT_SIZE_LT = auto()
    PRIMER_INTERNAL_WT_TM_GT = auto()
    PRIMER_PAIR_WT_COMPL_END_TH = auto()
    PRIMER_WT_TEMPLATE_MISPRIMING = auto()
    PRIMER_INTERNAL_WT_TM_LT = auto()
    PRIMER_PAIR_WT_DIFF_TM = auto()
    PRIMER_WT_TEMPLATE_MISPRIMING_TH = auto()
    PRIMER_LIBERAL_BASE = auto()
    PRIMER_PAIR_WT_IO_PENALTY = auto()
    PRIMER_WT_TM_GT = auto()
    PRIMER_LIB_AMBIGUITY_CODES_CONSENSUS = auto()
    PRIMER_PAIR_WT_LIBRARY_MISPRIMING = auto()
    PRIMER_WT_TM_LT = auto()
    PRIMER_LOWERCASE_MASKING = auto()
    PRIMER_PAIR_WT_PRODUCT_SIZE_GT = auto()

primer3_parameters

PrimerAndAmpliconParameters and ProbeParameters: Classes and Methods

The PrimerAndAmpliconParameters class stores user input for primer 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, 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: 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 PrimerAndAmpliconParameters converts these parameters into tag-values pairs for use when executing Primer3.

The 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.

Examples
>>> 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),     primer_tms=MinOptMax(min=63.0, max=67.0, opt=65.0),     primer_gcs=MinOptMax(min=30.0, max=65.0, opt=45.0), )
>>> for tag, value in params.to_input_tags().items():     print(f"{tag.value} -> {value}")
PRIMER_PRODUCT_OPT_SIZE -> 200
PRIMER_PRODUCT_SIZE_RANGE -> 100-250
PRIMER_PRODUCT_MIN_TM -> 55.0
PRIMER_PRODUCT_OPT_TM -> 70.0
PRIMER_PRODUCT_MAX_TM -> 100.0
PRIMER_MIN_SIZE -> 29
PRIMER_OPT_SIZE -> 30
PRIMER_MAX_SIZE -> 31
PRIMER_MIN_TM -> 63.0
PRIMER_OPT_TM -> 65.0
PRIMER_MAX_TM -> 67.0
PRIMER_MIN_GC -> 30.0
PRIMER_OPT_GC_PERCENT -> 45.0
PRIMER_MAX_GC -> 65.0
PRIMER_GC_CLAMP -> 0
PRIMER_MAX_END_GC -> 5
PRIMER_MAX_POLY_X -> 5
PRIMER_MAX_NS_ACCEPTED -> 1
PRIMER_LOWERCASE_MASKING -> 1
PRIMER_NUM_RETURN -> 5
PRIMER_MAX_SELF_ANY_TH -> 53.0
PRIMER_MAX_SELF_END_TH -> 53.0
PRIMER_MAX_HAIRPIN_TH -> 53.0

Classes

Primer3Parameters dataclass

Bases: PrimerAndAmpliconParameters

A deprecated alias for PrimerAndAmpliconParameters intended to maintain backwards compatibility with earlier releases of prymer.

Source code in prymer/primer3/primer3_parameters.py
@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,
        stacklevel=2,
    )
PrimerAndAmpliconParameters dataclass

Holds common primer and amplicon design options that Primer3 uses to inform primer design.

Attributes:

Name Type Description
amplicon_sizes MinOptMax[int]

the min, optimal, and max amplicon size

amplicon_tms MinOptMax[float]

the min, optimal, and max amplicon melting temperatures

primer_sizes MinOptMax[int]

the min, optimal, and max primer size

primer_tms MinOptMax[float]

the min, optimal, and max primer melting temperatures

primer_gcs MinOptMax[float]

the min and maximal GC content for individual primers

gc_clamp tuple[int, int]

the min and max number of Gs and Cs in the 3' most N bases

primer_max_polyX int

the max homopolymer length acceptable within a primer

primer_max_Ns int

the max number of ambiguous bases acceptable within a primer

primer_max_dinuc_bases int

the maximal number of bases in a dinucleotide run in a primer

avoid_masked_bases bool

whether Primer3 should avoid designing primers in soft-masked regions

number_primers_return int

the number of primers to return

primer_max_homodimer_tm Optional[float]

the max melting temperature acceptable for self-complementarity

primer_max_3p_homodimer_tm Optional[float]

the max melting temperature acceptable for self-complementarity anchored at the 3' end

primer_max_hairpin_tm Optional[float]

the max melting temperature acceptable for secondary structure

Please see the Primer3 manual for additional details: https://primer3.org/manual.html#globalTags

Each of primer_max_homodimer_tm, primer_max_3p_homodimer_tm, and primer_max_hairpin_tm is optional. If these attributes are not provided, the default value will be set to 10 degrees lower than the minimal melting temperature specified for the primer. This matches the Primer3 manual.

If these values are provided, users should provide the absolute value of the melting temperature threshold (i.e. when provided, values should be specified independent of primer design.)

Source code in prymer/primer3/primer3_parameters.py
@dataclass(frozen=True, init=True, slots=True)
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
        amplicon_tms: the min, optimal, and max amplicon melting temperatures
        primer_sizes: the min, optimal, and max primer size
        primer_tms: the min, optimal, and max primer melting temperatures
        primer_gcs: the min and maximal GC content for individual primers
        gc_clamp: the min and max number of Gs and Cs in the 3' most N bases
        primer_max_polyX: the max homopolymer length acceptable within a primer
        primer_max_Ns: the max number of ambiguous bases acceptable within a primer
        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_homodimer_tm: the max melting temperature acceptable for self-complementarity
        primer_max_3p_homodimer_tm: the max melting temperature acceptable for self-complementarity
            anchored at the 3' end
        primer_max_hairpin_tm: the max melting temperature acceptable for secondary structure

    Please see the Primer3 manual for additional details: https://primer3.org/manual.html#globalTags

    Each of `primer_max_homodimer_tm`, `primer_max_3p_homodimer_tm`, and `primer_max_hairpin_tm` is
    optional. If these attributes are not provided, the default value will be set to 10 degrees
    lower than the minimal melting temperature specified for the primer. This matches the Primer3
    manual.

    If these values are provided, users should provide the absolute value of the
    melting temperature threshold (i.e. when provided, values should be specified independent
    of primer design.)
    """

    amplicon_sizes: MinOptMax[int]
    amplicon_tms: MinOptMax[float]
    primer_sizes: MinOptMax[int]
    primer_tms: MinOptMax[float]
    primer_gcs: MinOptMax[float]
    gc_clamp: tuple[int, int] = (0, 5)
    primer_max_polyX: int = 5
    primer_max_Ns: int = 1
    primer_max_dinuc_bases: int = 6
    avoid_masked_bases: bool = True
    number_primers_return: int = 5
    primer_max_homodimer_tm: Optional[float] = None
    primer_max_3p_homodimer_tm: Optional[float] = None
    primer_max_hairpin_tm: Optional[float] = None

    def __post_init__(self) -> None:
        if self.primer_max_dinuc_bases % 2 == 1:
            raise ValueError("Primer Max Dinuc Bases must be an even number of bases")
        if not isinstance(self.amplicon_sizes.min, int) or not isinstance(
            self.primer_sizes.min, int
        ):
            raise TypeError("Amplicon sizes and primer sizes must be integers")
        if self.gc_clamp[0] > self.gc_clamp[1]:
            raise ValueError("Min primer GC-clamp must be <= max primer GC-clamp")
        # if thermo attributes are not provided, default them to `self.primer_tms.min - 10.0`
        default_thermo_max: float = self.primer_tms.min - 10.0
        thermo_max_fields = [
            "primer_max_homodimer_tm",
            "primer_max_3p_homodimer_tm",
            "primer_max_hairpin_tm",
        ]
        for field in fields(self):
            if field.name in thermo_max_fields and getattr(self, field.name) is None:
                object.__setattr__(self, field.name, default_thermo_max)

    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_PRODUCT_OPT_SIZE: self.amplicon_sizes.opt,
            Primer3InputTag.PRIMER_PRODUCT_SIZE_RANGE: (
                f"{self.amplicon_sizes.min}-{self.amplicon_sizes.max}"
            ),
            Primer3InputTag.PRIMER_PRODUCT_MIN_TM: self.amplicon_tms.min,
            Primer3InputTag.PRIMER_PRODUCT_OPT_TM: self.amplicon_tms.opt,
            Primer3InputTag.PRIMER_PRODUCT_MAX_TM: self.amplicon_tms.max,
            Primer3InputTag.PRIMER_MIN_SIZE: self.primer_sizes.min,
            Primer3InputTag.PRIMER_OPT_SIZE: self.primer_sizes.opt,
            Primer3InputTag.PRIMER_MAX_SIZE: self.primer_sizes.max,
            Primer3InputTag.PRIMER_MIN_TM: self.primer_tms.min,
            Primer3InputTag.PRIMER_OPT_TM: self.primer_tms.opt,
            Primer3InputTag.PRIMER_MAX_TM: self.primer_tms.max,
            Primer3InputTag.PRIMER_MIN_GC: self.primer_gcs.min,
            Primer3InputTag.PRIMER_OPT_GC_PERCENT: self.primer_gcs.opt,
            Primer3InputTag.PRIMER_MAX_GC: self.primer_gcs.max,
            Primer3InputTag.PRIMER_GC_CLAMP: self.gc_clamp[0],
            Primer3InputTag.PRIMER_MAX_END_GC: self.gc_clamp[1],
            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_TH: self.primer_max_homodimer_tm,
            Primer3InputTag.PRIMER_MAX_SELF_END_TH: self.primer_max_3p_homodimer_tm,
            Primer3InputTag.PRIMER_MAX_HAIRPIN_TH: self.primer_max_hairpin_tm,
        }

        return mapped_dict

    @property
    def max_amplicon_length(self) -> int:
        """Max amplicon length"""
        return int(self.amplicon_sizes.max)

    @property
    def max_primer_length(self) -> int:
        """Max primer length"""
        return int(self.primer_sizes.max)

    @property
    def min_primer_length(self) -> int:
        """Minimum primer length."""
        return int(self.primer_sizes.min)
Attributes
max_amplicon_length property
max_amplicon_length: int

Max amplicon length

max_primer_length property
max_primer_length: int

Max primer length

min_primer_length property
min_primer_length: int

Minimum primer length.

Functions
to_input_tags
to_input_tags() -> dict[Primer3InputTag, Any]

Converts input params to Primer3InputTag to feed directly into Primer3.

Source code in prymer/primer3/primer3_parameters.py
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_PRODUCT_OPT_SIZE: self.amplicon_sizes.opt,
        Primer3InputTag.PRIMER_PRODUCT_SIZE_RANGE: (
            f"{self.amplicon_sizes.min}-{self.amplicon_sizes.max}"
        ),
        Primer3InputTag.PRIMER_PRODUCT_MIN_TM: self.amplicon_tms.min,
        Primer3InputTag.PRIMER_PRODUCT_OPT_TM: self.amplicon_tms.opt,
        Primer3InputTag.PRIMER_PRODUCT_MAX_TM: self.amplicon_tms.max,
        Primer3InputTag.PRIMER_MIN_SIZE: self.primer_sizes.min,
        Primer3InputTag.PRIMER_OPT_SIZE: self.primer_sizes.opt,
        Primer3InputTag.PRIMER_MAX_SIZE: self.primer_sizes.max,
        Primer3InputTag.PRIMER_MIN_TM: self.primer_tms.min,
        Primer3InputTag.PRIMER_OPT_TM: self.primer_tms.opt,
        Primer3InputTag.PRIMER_MAX_TM: self.primer_tms.max,
        Primer3InputTag.PRIMER_MIN_GC: self.primer_gcs.min,
        Primer3InputTag.PRIMER_OPT_GC_PERCENT: self.primer_gcs.opt,
        Primer3InputTag.PRIMER_MAX_GC: self.primer_gcs.max,
        Primer3InputTag.PRIMER_GC_CLAMP: self.gc_clamp[0],
        Primer3InputTag.PRIMER_MAX_END_GC: self.gc_clamp[1],
        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_TH: self.primer_max_homodimer_tm,
        Primer3InputTag.PRIMER_MAX_SELF_END_TH: self.primer_max_3p_homodimer_tm,
        Primer3InputTag.PRIMER_MAX_HAIRPIN_TH: self.primer_max_hairpin_tm,
    }

    return mapped_dict
ProbeParameters dataclass

Holds common primer design options that Primer3 uses to inform internal probe design.

Attributes:

Name Type Description
probe_sizes MinOptMax[int]

the min, optimal, and max probe size

probe_tms MinOptMax[float]

the min, optimal, and max probe melting temperatures

probe_gcs MinOptMax[float]

the min and max GC content for individual probes

number_probes_return int

the number of probes to return

probe_max_dinuc_bases int

the max number of bases in a dinucleotide run in a probe

probe_max_polyX int

the max homopolymer length acceptable within a probe

probe_max_Ns int

the max number of ambiguous bases acceptable within a probe

probe_max_homodimer_tm Optional[float]

the max melting temperature acceptable for self-complementarity

probe_max_3p_homodimer_tm Optional[float]

the max melting temperature acceptable for self-complementarity anchored at the 3' end

probe_max_hairpin_tm Optional[float]

the max melting temperature acceptable for secondary structure

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

While Primer3 supports alignment based and thermodynamic methods for simulating hybridizations, prymer enforces the use of the thermodynamic approach. This is slightly more computationally expensive but superior in their ability to limit problematic oligo self-complementarity (e.g., primer-dimers, nonspecific binding of probes)

If they are not provided, probe_max_self_any_thermo, probe_max_self_end_thermo, and probe_max_hairpin_thermo will be set to default values as specified in the Primer3 manual. The default value is 10 degrees lower than the minimal melting temperature specified for probe design (i.e. when not provided, values are specified relative to the probe design). If these values are provided, users should provide the absolute value of the melting temperature threshold (i.e. when provided, values should be specified as independent of probe design.)

Source code in prymer/primer3/primer3_parameters.py
@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 max GC content for individual probes
        number_probes_return: the number of probes to return
        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_homodimer_tm: the max melting temperature acceptable for self-complementarity
        probe_max_3p_homodimer_tm: the max melting temperature acceptable for self-complementarity
            anchored at the 3' end
        probe_max_hairpin_tm: the max melting temperature acceptable for secondary structure

    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

    While Primer3 supports alignment based and thermodynamic methods for simulating hybridizations,
    prymer enforces the use of the thermodynamic approach. This is slightly more computationally
    expensive but superior in their ability to limit problematic oligo self-complementarity
    (e.g., primer-dimers, nonspecific binding of probes)

    If they are not provided, `probe_max_self_any_thermo`, `probe_max_self_end_thermo`, and
    `probe_max_hairpin_thermo` will be set to default values as specified in the Primer3 manual.
    The default value is 10 degrees lower than the minimal melting temperature specified for
    probe design (i.e. when not provided, values are specified relative to the probe design).
    If these values are provided, users should provide the absolute value of the
    melting temperature threshold (i.e. when provided, values should be specified as independent
    of probe design.)

    """

    probe_sizes: MinOptMax[int]
    probe_tms: MinOptMax[float]
    probe_gcs: MinOptMax[float]
    number_probes_return: int = 5
    probe_max_dinuc_bases: int = 4
    probe_max_polyX: int = 5
    probe_max_Ns: int = 0
    probe_max_homodimer_tm: Optional[float] = None
    probe_max_3p_homodimer_tm: Optional[float] = None
    probe_max_hairpin_tm: Optional[float] = None

    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_max_dinuc_bases % 2 == 1:
            raise ValueError("Max threshold for dinucleotide bases must be an even number of bases")
        # if thermo attributes are not provided, default them to `self.probe_tms.min - 10.0`
        default_thermo_max: float = self.probe_tms.min - 10.0
        thermo_max_fields = [
            "probe_max_homodimer_tm",
            "probe_max_3p_homodimer_tm",
            "probe_max_hairpin_tm",
        ]
        for field in fields(self):
            if field.name in thermo_max_fields and getattr(self, field.name) is None:
                object.__setattr__(self, field.name, default_thermo_max)

    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_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_TH: self.probe_max_homodimer_tm,
            Primer3InputTag.PRIMER_INTERNAL_MAX_SELF_END_TH: self.probe_max_3p_homodimer_tm,
            Primer3InputTag.PRIMER_INTERNAL_MAX_HAIRPIN_TH: self.probe_max_hairpin_tm,
            Primer3InputTag.PRIMER_NUM_RETURN: self.number_probes_return,
        }

        return mapped_dict
Functions
to_input_tags
to_input_tags() -> dict[Primer3InputTag, Any]

Converts input params to Primer3InputTag to feed directly into Primer3.

Source code in prymer/primer3/primer3_parameters.py
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_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_TH: self.probe_max_homodimer_tm,
        Primer3InputTag.PRIMER_INTERNAL_MAX_SELF_END_TH: self.probe_max_3p_homodimer_tm,
        Primer3InputTag.PRIMER_INTERNAL_MAX_HAIRPIN_TH: self.probe_max_hairpin_tm,
        Primer3InputTag.PRIMER_NUM_RETURN: self.number_probes_return,
    }

    return mapped_dict

primer3_task

Primer3Task Class and Methods

This module contains a Primer3Task class to provide design-specific parameters to Primer3. The classes are primarily used in Primer3Input.

Primer3 can design single primers ("left" and "right") as well as primer pairs. The design task "type" dictates which type of primers to pick and informs the design region. These parameters are aligned to the correct Primer3 settings and fed directly into Primer3.

Four types of tasks are available:

  1. DesignPrimerPairsTask -- task for designing primer pairs.
  2. DesignLeftPrimersTask -- task for designing primers to the left (5') of the design region on the top/positive strand.
  3. DesignRightPrimersTask -- task for designing primers to the right (3') of the design region on the bottom/negative strand.
  4. PickHybProbeOnly -- task for designing an internal probe for hybridization-based technologies

The main purpose of these classes are to generate the Primer3InputTagss required by Primer3 for specifying how to design the primers, returned by the to_input_tags() method. The target and design region are provided to this method, where the target region is wholly contained within design region. This leaves a left and right primer region respectively, that are the two regions that remain after removing the wholly contained (inner) target regions.

Therefore, the left primers shall be designed from the start of the design region to the start of the target region, while right primers shall be designed from the end of the target region through to the end of the design region.

In addition to the to_input_tags() method, each Primer3TaskType provides a task_type and count_tag class property. The former is a TaskType enumeration that represents the type of design task, while the latter is the tag returned by Primer3 that provides the number of primers returned.

Examples

Suppose we have the following design and target regions:

>>> from prymer.api import Strand
>>> design_region = Span(refname="chr1", start=1, end=500, strand=Strand.POSITIVE)
>>> target = Span(refname="chr1", start=200, end=300, strand=Strand.POSITIVE)
>>> design_region.contains(target)
True

The task for designing primer pairs:

>>> task = DesignPrimerPairsTask()
>>> task.task_type.value
'PAIR'
>>> task.count_tag
'PRIMER_PAIR_NUM_RETURNED'
>>> [(tag.value, value) for tag, value in task.to_input_tags(target=target, design_region=design_region).items()]
[('PRIMER_TASK', 'generic'), ('PRIMER_PICK_LEFT_PRIMER', 1), ('PRIMER_PICK_RIGHT_PRIMER', 1), ('PRIMER_PICK_INTERNAL_OLIGO', 0), ('SEQUENCE_TARGET', '200,101')]

The tasks for designing left primers:

>>> task = DesignLeftPrimersTask()
>>> task.task_type.value
'LEFT'
>>> task.count_tag
'PRIMER_LEFT_NUM_RETURNED'
>>> [(tag.value, value) for tag, value in task.to_input_tags(target=target, design_region=design_region).items()]
[('PRIMER_TASK', 'pick_primer_list'), ('PRIMER_PICK_LEFT_PRIMER', 1), ('PRIMER_PICK_RIGHT_PRIMER', 0), ('PRIMER_PICK_INTERNAL_OLIGO', 0), ('SEQUENCE_INCLUDED_REGION', '1,199')]

The tasks for designing left primers:

>>> task = DesignRightPrimersTask()
>>> task.task_type.value
'RIGHT'
>>> task.count_tag
'PRIMER_RIGHT_NUM_RETURNED'
>>> [(tag.value, value) for tag, value in task.to_input_tags(target=target, design_region=design_region).items()]
[('PRIMER_TASK', 'pick_primer_list'), ('PRIMER_PICK_LEFT_PRIMER', 0), ('PRIMER_PICK_RIGHT_PRIMER', 1), ('PRIMER_PICK_INTERNAL_OLIGO', 0), ('SEQUENCE_INCLUDED_REGION', '300,200')]

Attributes

Primer3TaskType module-attribute
Primer3TaskType: TypeAlias = Union[
    "DesignPrimerPairsTask",
    "DesignLeftPrimersTask",
    "DesignRightPrimersTask",
    "PickHybProbeOnly",
]

Type alias for all Primer3Tasks, to enable exhaustiveness checking.

Classes

DesignLeftPrimersTask

Bases: Primer3Task

Stores task-specific characteristics for designing left primers.

Source code in prymer/primer3/primer3_task.py
class DesignLeftPrimersTask(Primer3Task, task_type=TaskType.LEFT):
    """Stores task-specific characteristics for designing left primers."""

    @classmethod
    def _to_input_tags(cls, target: Span, design_region: Span) -> dict[Primer3InputTag, Any]:
        return {
            Primer3InputTag.PRIMER_TASK: "pick_primer_list",
            Primer3InputTag.PRIMER_PICK_LEFT_PRIMER: 1,
            Primer3InputTag.PRIMER_PICK_RIGHT_PRIMER: 0,
            Primer3InputTag.PRIMER_PICK_INTERNAL_OLIGO: 0,
            Primer3InputTag.SEQUENCE_INCLUDED_REGION: f"1,{target.start - design_region.start}",
        }

    @property
    def requires_primer_amplicon_params(self) -> bool:
        return True

    @property
    def requires_probe_params(self) -> bool:
        return False
DesignPrimerPairsTask

Bases: Primer3Task

Stores task-specific Primer3 settings for designing primer pairs

Source code in prymer/primer3/primer3_task.py
class DesignPrimerPairsTask(Primer3Task, task_type=TaskType.PAIR):
    """Stores task-specific Primer3 settings for designing primer pairs"""

    @classmethod
    def _to_input_tags(cls, target: Span, design_region: Span) -> dict[Primer3InputTag, Any]:
        return {
            Primer3InputTag.PRIMER_TASK: "generic",
            Primer3InputTag.PRIMER_PICK_LEFT_PRIMER: 1,
            Primer3InputTag.PRIMER_PICK_RIGHT_PRIMER: 1,
            Primer3InputTag.PRIMER_PICK_INTERNAL_OLIGO: 0,
            Primer3InputTag.SEQUENCE_TARGET: f"{target.start - design_region.start + 1},"
            f"{target.length}",
        }

    @property
    def requires_primer_amplicon_params(self) -> bool:
        return True

    @property
    def requires_probe_params(self) -> bool:
        return False
DesignRightPrimersTask

Bases: Primer3Task

Stores task-specific characteristics for designing right primers

Source code in prymer/primer3/primer3_task.py
class DesignRightPrimersTask(Primer3Task, task_type=TaskType.RIGHT):
    """Stores task-specific characteristics for designing right primers"""

    @classmethod
    def _to_input_tags(cls, target: Span, design_region: Span) -> dict[Primer3InputTag, Any]:
        start = target.end - design_region.start + 1
        length = design_region.end - target.end
        return {
            Primer3InputTag.PRIMER_TASK: "pick_primer_list",
            Primer3InputTag.PRIMER_PICK_LEFT_PRIMER: 0,
            Primer3InputTag.PRIMER_PICK_RIGHT_PRIMER: 1,
            Primer3InputTag.PRIMER_PICK_INTERNAL_OLIGO: 0,
            Primer3InputTag.SEQUENCE_INCLUDED_REGION: f"{start},{length}",
        }

    @property
    def requires_primer_amplicon_params(self) -> bool:
        return True

    @property
    def requires_probe_params(self) -> bool:
        return False
PickHybProbeOnly

Bases: Primer3Task

Stores task-specific characteristics for designing an internal hybridization probe.

Source code in prymer/primer3/primer3_task.py
class PickHybProbeOnly(Primer3Task, task_type=TaskType.INTERNAL):
    """Stores task-specific characteristics for designing an internal hybridization probe."""

    @classmethod
    def _to_input_tags(cls, target: Span, design_region: Span) -> dict[Primer3InputTag, Any]:
        return {
            Primer3InputTag.PRIMER_TASK: "generic",
            Primer3InputTag.PRIMER_PICK_LEFT_PRIMER: 0,
            Primer3InputTag.PRIMER_PICK_RIGHT_PRIMER: 0,
            Primer3InputTag.PRIMER_PICK_INTERNAL_OLIGO: 1,
        }

    @property
    def requires_primer_amplicon_params(self) -> bool:
        return False

    @property
    def requires_probe_params(self) -> bool:
        return True
Primer3Task

Bases: ABC

Abstract base class from which the other classes derive.

Source code in prymer/primer3/primer3_task.py
class Primer3Task(ABC):
    """Abstract base class from which the other classes derive."""

    @final
    def to_input_tags(self, target: Span, design_region: Span) -> dict[Primer3InputTag, Any]:
        """Returns the input tags specific to this type of task for Primer3.

        Ensures target region is wholly contained within design region. Subclass specific
        implementation aligns the set of input parameters specific to primer pair or
        single primer design.

        This implementation mimics the Template method, a Behavioral Design Pattern in which the
        abstract base class contains a rough skeleton of methods and the derived subclasses
        implement the details of those methods. In this case, each of the derived subclasses
        will first use the base class `to_input_tags()` method to check that the target region is
        wholly contained within the design region. If so, they will implement task-specific logic
        for the `to_input_tags()` method.

        Args:
            target: the target region (to be amplified)
            design_region: the design region, which wholly contains the target region, in which
                primers are to be designed


        Raises:
            ValueError: if the target region is not contained within the design region

        Returns:
            The input tags for Primer3
        """
        if not design_region.contains(target):
            raise ValueError(
                "Target not contained within design region: "
                f"target:{target.__str__()},"
                f"design_region: {design_region.__str__()}"
            )

        return self._to_input_tags(target=target, design_region=design_region)

    task_type: ClassVar[TaskType] = NotImplemented
    """Tracks task type for downstream analysis"""

    count_tag: ClassVar[str] = NotImplemented
    """The tag returned by Primer3 that provides the number of primers returned"""

    @classmethod
    @abstractmethod
    def _to_input_tags(cls, target: Span, design_region: Span) -> dict[Primer3InputTag, Any]:
        """Aligns the set of input parameters specific to primer pair or single primer design"""

    @classmethod
    def __init_subclass__(cls, task_type: TaskType, **kwargs: Any) -> None:
        # See: https://docs.python.org/3/reference/datamodel.html#object.__init_subclass__
        super().__init_subclass__(**kwargs)

        cls.task_type = task_type
        cls.count_tag = f"PRIMER_{task_type}_NUM_RETURNED"
Attributes
count_tag class-attribute
count_tag: str = NotImplemented

The tag returned by Primer3 that provides the number of primers returned

task_type class-attribute
task_type: TaskType = NotImplemented

Tracks task type for downstream analysis

Functions
to_input_tags
to_input_tags(
    target: Span, design_region: Span
) -> dict[Primer3InputTag, Any]

Returns the input tags specific to this type of task for Primer3.

Ensures target region is wholly contained within design region. Subclass specific implementation aligns the set of input parameters specific to primer pair or single primer design.

This implementation mimics the Template method, a Behavioral Design Pattern in which the abstract base class contains a rough skeleton of methods and the derived subclasses implement the details of those methods. In this case, each of the derived subclasses will first use the base class to_input_tags() method to check that the target region is wholly contained within the design region. If so, they will implement task-specific logic for the to_input_tags() method.

Parameters:

Name Type Description Default
target Span

the target region (to be amplified)

required
design_region Span

the design region, which wholly contains the target region, in which primers are to be designed

required

Raises:

Type Description
ValueError

if the target region is not contained within the design region

Returns:

Type Description
dict[Primer3InputTag, Any]

The input tags for Primer3

Source code in prymer/primer3/primer3_task.py
@final
def to_input_tags(self, target: Span, design_region: Span) -> dict[Primer3InputTag, Any]:
    """Returns the input tags specific to this type of task for Primer3.

    Ensures target region is wholly contained within design region. Subclass specific
    implementation aligns the set of input parameters specific to primer pair or
    single primer design.

    This implementation mimics the Template method, a Behavioral Design Pattern in which the
    abstract base class contains a rough skeleton of methods and the derived subclasses
    implement the details of those methods. In this case, each of the derived subclasses
    will first use the base class `to_input_tags()` method to check that the target region is
    wholly contained within the design region. If so, they will implement task-specific logic
    for the `to_input_tags()` method.

    Args:
        target: the target region (to be amplified)
        design_region: the design region, which wholly contains the target region, in which
            primers are to be designed


    Raises:
        ValueError: if the target region is not contained within the design region

    Returns:
        The input tags for Primer3
    """
    if not design_region.contains(target):
        raise ValueError(
            "Target not contained within design region: "
            f"target:{target.__str__()},"
            f"design_region: {design_region.__str__()}"
        )

    return self._to_input_tags(target=target, design_region=design_region)
TaskType

Bases: UppercaseStrEnum

Represents the type of design task: design primer pairs, individual primers (left or right), or an internal hybridization probe.

Source code in prymer/primer3/primer3_task.py
@unique
class TaskType(UppercaseStrEnum):
    """Represents the type of design task: design primer pairs, individual primers
    (left or right), or an internal hybridization probe."""

    # Developer Note: the names of this enum are important, as they are used as-is for the
    # count_tag in `Primer3Task`.

    PAIR = auto()
    LEFT = auto()
    RIGHT = auto()
    INTERNAL = auto()

primer3_weights

Primer3Weights Class and Methods

The PrimerAndAmpliconWeights class holds the penalty weights that Primer3 uses to score primer designs.

The ProbeWeights class holds the penalty weights that Primer3 uses to score internal probe designs.

Primer3 considers the differential between user input (e.g., constraining the optimal primer size to be 18 bp) and the characteristics of a specific primer design (e.g., if the primer size is 19 bp). Depending on the "weight" of that characteristic, Primer3 uses an objective function to score a primer design and help define what an "optimal" design looks like.

By modifying these weights, users can prioritize specific primer design characteristics. Each of the defaults provided here are derived from the Primer3 manual: https://primer3.org/manual.html

Examples of interacting with the PrimerAndAmpliconWeights class

Example:

PrimerAndAmpliconWeights() # default implementation PrimerAndAmpliconWeights(product_size_lt=1.0, product_size_gt=1.0, 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_homodimer_wt=0.0, primer_3p_homodimer_wt=0.0, primer_secondary_structure_wt=0.0) PrimerAndAmpliconWeights(product_size_lt=5.0) PrimerAndAmpliconWeights(product_size_lt=5.0, product_size_gt=1.0, 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_homodimer_wt=0.0, primer_3p_homodimer_wt=0.0, primer_secondary_structure_wt=0.0)

Classes

PrimerAndAmpliconWeights dataclass

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.

Some of these settings depart from the default settings enumerated in the Primer3 manual. Please see the Primer3 manual for additional details: https://primer3.org/manual.html#globalTags

Attributes:

Name Type Description
product_size_lt float

weight for products shorter than PrimerAndAmpliconParameters.amplicon_sizes.opt

product_size_gt float

weight for products longer than PrimerAndAmpliconParameters.amplicon_sizes.opt

product_tm_lt float

weight for products with a Tm lower than PrimerAndAmpliconParameters.amplicon_tms.opt

product_tm_gt float

weight for products with a Tm greater than PrimerAndAmpliconParameters.amplicon_tms.opt

primer_end_stability float

penalty for the calculated maximum stability for the last five 3' bases of primer

primer_gc_lt float

penalty for primers with GC percent lower than PrimerAndAmpliconParameters.primer_gcs.opt

primer_gc_gt float

weight for primers with GC percent higher than PrimerAndAmpliconParameters.primer_gcs.opt

primer_homodimer_wt float

penalty for the individual primer self binding value as specified in PrimerAndAmpliconParameters.primer_max_homodimer_tm

primer_3p_homodimer_wt float

weight for the 3'-anchored primer self binding value as specified in PrimerAndAmpliconParameters.primer_max_3p_homodimer_tm

primer_secondary_structure_wt float

penalty weight for the primer hairpin structure melting temperature as defined in PrimerAndAmpliconParameters.PRIMER_MAX_HAIRPIN_TH

Source code in prymer/primer3/primer3_weights.py
@dataclass(frozen=True, init=True, slots=True)
class PrimerAndAmpliconWeights:
    """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.

    Some of these settings depart from the default settings enumerated in the Primer3 manual.
    Please see the Primer3 manual for additional details:
        https://primer3.org/manual.html#globalTags

    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: weight for primers with GC percent higher than
            `PrimerAndAmpliconParameters.primer_gcs.opt`
        primer_homodimer_wt: penalty for the individual primer self binding value as specified
            in `PrimerAndAmpliconParameters.primer_max_homodimer_tm`
        primer_3p_homodimer_wt: weight for the 3'-anchored primer self binding value as specified in
            `PrimerAndAmpliconParameters.primer_max_3p_homodimer_tm`
        primer_secondary_structure_wt: penalty weight for the primer hairpin structure melting
            temperature as defined in `PrimerAndAmpliconParameters.PRIMER_MAX_HAIRPIN_TH`

    """

    product_size_lt: float = 1.0
    product_size_gt: float = 1.0
    product_tm_lt: float = 0.0
    product_tm_gt: float = 0.0
    primer_end_stability: float = 0.25
    primer_gc_lt: float = 0.25
    primer_gc_gt: float = 0.25
    primer_self_any: float = 0.1
    primer_self_end: float = 0.1
    primer_size_lt: float = 0.5
    primer_size_gt: float = 0.1
    primer_tm_lt: float = 1.0
    primer_tm_gt: float = 1.0
    primer_homodimer_wt: float = 0.0
    primer_3p_homodimer_wt: float = 0.0
    primer_secondary_structure_wt: float = 0.0

    def to_input_tags(self) -> dict[Primer3InputTag, Any]:
        """Maps weights to Primer3InputTag to feed directly into Primer3."""
        mapped_dict = {
            Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_LT: self.product_size_lt,
            Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_GT: self.product_size_gt,
            Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_TM_LT: self.product_tm_lt,
            Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_TM_GT: self.product_tm_gt,
            Primer3InputTag.PRIMER_WT_END_STABILITY: self.primer_end_stability,
            Primer3InputTag.PRIMER_WT_GC_PERCENT_LT: self.primer_gc_lt,
            Primer3InputTag.PRIMER_WT_GC_PERCENT_GT: self.primer_gc_gt,
            Primer3InputTag.PRIMER_WT_SELF_ANY: self.primer_self_any,
            Primer3InputTag.PRIMER_WT_SELF_END: self.primer_self_end,
            Primer3InputTag.PRIMER_WT_SIZE_LT: self.primer_size_lt,
            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_homodimer_wt,
            Primer3InputTag.PRIMER_WT_SELF_END_TH: self.primer_3p_homodimer_wt,
            Primer3InputTag.PRIMER_WT_HAIRPIN_TH: self.primer_secondary_structure_wt,
        }
        return mapped_dict
Functions
to_input_tags
to_input_tags() -> dict[Primer3InputTag, Any]

Maps weights to Primer3InputTag to feed directly into Primer3.

Source code in prymer/primer3/primer3_weights.py
def to_input_tags(self) -> dict[Primer3InputTag, Any]:
    """Maps weights to Primer3InputTag to feed directly into Primer3."""
    mapped_dict = {
        Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_LT: self.product_size_lt,
        Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_SIZE_GT: self.product_size_gt,
        Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_TM_LT: self.product_tm_lt,
        Primer3InputTag.PRIMER_PAIR_WT_PRODUCT_TM_GT: self.product_tm_gt,
        Primer3InputTag.PRIMER_WT_END_STABILITY: self.primer_end_stability,
        Primer3InputTag.PRIMER_WT_GC_PERCENT_LT: self.primer_gc_lt,
        Primer3InputTag.PRIMER_WT_GC_PERCENT_GT: self.primer_gc_gt,
        Primer3InputTag.PRIMER_WT_SELF_ANY: self.primer_self_any,
        Primer3InputTag.PRIMER_WT_SELF_END: self.primer_self_end,
        Primer3InputTag.PRIMER_WT_SIZE_LT: self.primer_size_lt,
        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_homodimer_wt,
        Primer3InputTag.PRIMER_WT_SELF_END_TH: self.primer_3p_homodimer_wt,
        Primer3InputTag.PRIMER_WT_HAIRPIN_TH: self.primer_secondary_structure_wt,
    }
    return mapped_dict
ProbeWeights dataclass

Holds the probe-specific weights that Primer3 uses to adjust design penalties.

Attributes:

Name Type Description
probe_size_lt float

penalty for probes shorter than ProbeParameters.probe_sizes.opt

probe_size_gt float

penalty for probes longer than ProbeParameters.probe_sizes.opt

probe_tm_lt float

penalty for probes with a Tm lower than ProbeParameters.probe_tms.opt

probe_tm_gt float

penalty for probes with a Tm greater than ProbeParameters.probe_tms.opt

probe_gc_lt float

penalty for probes with GC content lower than ProbeParameters.probe_gcs.opt

probe_gc_gt float

penalty for probes with GC content greater than ProbeParameters.probe_gcs.opt

probe_wt_self_any_th float

penalty for probe self-complementarity as defined in ProbeParameters.probe_max_self_any_thermo

probe_wt_self_end float

penalty for probe 3' complementarity as defined in ProbeParameters.probe_max_self_end_thermo

probe_wt_hairpin_th float

penalty for the most stable primer hairpin structure value as defined in ProbeParameters.probe_max_hairpin_thermo

Each of these defaults are taken from the Primer3 manual. More details can be found here: https://primer3.org/manual.html

Source code in prymer/primer3/primer3_weights.py
@dataclass(frozen=True, init=True, slots=True)
class ProbeWeights:
    """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_th: penalty for probe self-complementarity as defined in
            `ProbeParameters.probe_max_self_any_thermo`
        probe_wt_self_end: penalty for probe 3' complementarity as defined in
            `ProbeParameters.probe_max_self_end_thermo`
        probe_wt_hairpin_th: penalty for the most stable primer hairpin structure value as defined
            in `ProbeParameters.probe_max_hairpin_thermo`

    Each of these defaults are taken from the Primer3 manual. More details can be found here:
    https://primer3.org/manual.html

    """

    probe_size_lt: float = 1.0
    probe_size_gt: float = 1.0
    probe_tm_lt: float = 1.0
    probe_tm_gt: float = 1.0
    probe_gc_lt: float = 0.0
    probe_gc_gt: float = 0.0
    probe_homodimer_wt: float = 0.0
    probe_3p_homodimer_wt: float = 0.0
    probe_secondary_structure_wt: float = 0.0

    def to_input_tags(self) -> dict[Primer3InputTag, Any]:
        """Maps weights to Primer3InputTag to feed directly into Primer3."""
        mapped_dict = {
            Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_LT: self.probe_size_lt,
            Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_GT: self.probe_size_gt,
            Primer3InputTag.PRIMER_INTERNAL_WT_TM_LT: self.probe_tm_lt,
            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_TH: self.probe_homodimer_wt,
            Primer3InputTag.PRIMER_INTERNAL_WT_SELF_END_TH: self.probe_3p_homodimer_wt,
            Primer3InputTag.PRIMER_INTERNAL_WT_HAIRPIN_TH: self.probe_secondary_structure_wt,
        }
        return mapped_dict
Functions
to_input_tags
to_input_tags() -> dict[Primer3InputTag, Any]

Maps weights to Primer3InputTag to feed directly into Primer3.

Source code in prymer/primer3/primer3_weights.py
def to_input_tags(self) -> dict[Primer3InputTag, Any]:
    """Maps weights to Primer3InputTag to feed directly into Primer3."""
    mapped_dict = {
        Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_LT: self.probe_size_lt,
        Primer3InputTag.PRIMER_INTERNAL_WT_SIZE_GT: self.probe_size_gt,
        Primer3InputTag.PRIMER_INTERNAL_WT_TM_LT: self.probe_tm_lt,
        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_TH: self.probe_homodimer_wt,
        Primer3InputTag.PRIMER_INTERNAL_WT_SELF_END_TH: self.probe_3p_homodimer_wt,
        Primer3InputTag.PRIMER_INTERNAL_WT_HAIRPIN_TH: self.probe_secondary_structure_wt,
    }
    return mapped_dict